diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
index 1e87cfc1..7d433ba7 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt
@@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class MatrixError(
@Json(name = "errcode") val code: String,
- @Json(name = "error") val message: String
-) {
+ @Json(name = "error") val message: String,
+
+ @Json(name = "consent_uri") val consentUri: String? = null,
+ // RESOURCE_LIMIT_EXCEEDED data
+ @Json(name = "limit_type") val limitType: String? = null,
+ @Json(name = "admin_contact") val adminUri: String? = null) {
+
companion object {
const val FORBIDDEN = "M_FORBIDDEN"
@@ -55,5 +60,8 @@ data class MatrixError(
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
+
+ // Possible value for "limit_type"
+ const val LIMIT_TYPE_MAU = "monthly_active_user"
}
}
\ No newline at end of file
diff --git a/vector/src/debug/res/layout/view_notification_area.xml b/vector/src/debug/res/layout/view_notification_area.xml
new file mode 100644
index 00000000..8af520c2
--- /dev/null
+++ b/vector/src/debug/res/layout/view_notification_area.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt
new file mode 100644
index 00000000..b57014f1
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/error/ResourceLimitErrorFormatter.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.error
+
+import android.content.Context
+import android.text.Html
+import androidx.annotation.StringRes
+import im.vector.matrix.android.api.failure.MatrixError
+import im.vector.riotx.R
+import me.gujun.android.span.span
+
+class ResourceLimitErrorFormatter(private val context: Context) {
+
+ // 'hard' if the logged in user has been locked out, 'soft' if they haven't
+ sealed class Mode(@StringRes val mauErrorRes: Int, @StringRes val defaultErrorRes: Int, @StringRes val contactRes: Int) {
+ // User can still send message (will be used in a near future)
+ object Soft : Mode(R.string.resource_limit_soft_mau, R.string.resource_limit_soft_default, R.string.resource_limit_soft_contact)
+
+ // User cannot send message anymore
+ object Hard : Mode(R.string.resource_limit_hard_mau, R.string.resource_limit_hard_default, R.string.resource_limit_hard_contact)
+ }
+
+ fun format(matrixError: MatrixError,
+ mode: Mode,
+ separator: CharSequence = " ",
+ clickable: Boolean = false): CharSequence {
+ val error = if (MatrixError.LIMIT_TYPE_MAU == matrixError.limitType) {
+ context.getString(mode.mauErrorRes)
+ } else {
+ context.getString(mode.defaultErrorRes)
+ }
+ val contact = if (clickable && matrixError.adminUri != null) {
+ val contactSubString = uriAsLink(matrixError.adminUri!!)
+ val contactFullString = context.getString(mode.contactRes, contactSubString)
+ Html.fromHtml(contactFullString)
+ } else {
+ val contactSubString = context.getString(R.string.resource_limit_contact_admin)
+ context.getString(mode.contactRes, contactSubString)
+ }
+ return span {
+ text = error
+ }
+ .append(separator)
+ .append(contact)
+ }
+
+ /**
+ * Create a HTML link with a uri
+ */
+ private fun uriAsLink(uri: String): String {
+ val contactStr = context.getString(R.string.resource_limit_contact_admin)
+ return "$contactStr"
+ }
+}
\ No newline at end of file
diff --git a/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt
new file mode 100644
index 00000000..6a9af149
--- /dev/null
+++ b/vector/src/main/java/im/vector/riotx/core/platform/NotificationAreaView.kt
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2019 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.riotx.core.platform
+
+import android.content.Context
+import android.graphics.Color
+import android.text.SpannableString
+import android.text.TextPaint
+import android.text.TextUtils
+import android.text.method.LinkMovementMethod
+import android.text.style.ClickableSpan
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.text.bold
+import butterknife.BindView
+import butterknife.ButterKnife
+import im.vector.matrix.android.api.failure.MatrixError
+import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
+import im.vector.matrix.android.api.permalinks.PermalinkFactory
+import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
+import im.vector.riotx.R
+import im.vector.riotx.core.error.ResourceLimitErrorFormatter
+import im.vector.riotx.features.themes.ThemeUtils
+import me.gujun.android.span.addSpan
+import me.gujun.android.span.span
+import me.saket.bettermovementmethod.BetterLinkMovementMethod
+import timber.log.Timber
+
+/**
+ * The view used to show some information about the room
+ * It does have a unique render method
+ */
+class NotificationAreaView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : RelativeLayout(context, attrs, defStyleAttr) {
+
+ @BindView(R.id.room_notification_icon)
+ lateinit var imageView: ImageView
+ @BindView(R.id.room_notification_message)
+ lateinit var messageView: TextView
+
+ var delegate: Delegate? = null
+ private var state: State = State.Initial
+
+ init {
+ setupView()
+ }
+
+ /**
+ * This methods is responsible for rendering the view according to the newState
+ *
+ * @param newState the newState representing the view
+ */
+ fun render(newState: State) {
+ if (newState == state) {
+ Timber.d("State unchanged")
+ return
+ }
+ Timber.d("Rendering $newState")
+ cleanUp()
+ state = newState
+ when (newState) {
+ is State.Default -> renderDefault()
+ is State.Hidden -> renderHidden()
+ is State.Tombstone -> renderTombstone(newState)
+ is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
+ is State.ConnectionError -> renderConnectionError()
+ is State.Typing -> renderTyping(newState)
+ is State.UnreadPreview -> renderUnreadPreview()
+ is State.ScrollToBottom -> renderScrollToBottom(newState)
+ is State.UnsentEvents -> renderUnsent(newState)
+ }
+ }
+
+ // PRIVATE METHODS *****************************************************************************************************************************************
+
+ private fun setupView() {
+ inflate(context, R.layout.view_notification_area, this)
+ ButterKnife.bind(this)
+ }
+
+ private fun cleanUp() {
+ messageView.setOnClickListener(null)
+ imageView.setOnClickListener(null)
+ setBackgroundColor(Color.TRANSPARENT)
+ messageView.text = null
+ imageView.setImageResource(0)
+ }
+
+ private fun renderTombstone(state: State.Tombstone) {
+ val roomTombstoneContent = state.tombstoneContent
+ val roomLink = PermalinkFactory.createPermalink(roomTombstoneContent.replacementRoom)
+ ?: return
+
+ visibility = View.VISIBLE
+ imageView.setImageResource(R.drawable.error)
+ val textColorInt = ThemeUtils.getColor(context, R.attr.vctr_message_text_color)
+ val message = span {
+ +resources.getString(R.string.room_tombstone_versioned_description)
+ +"\n"
+ span(resources.getString(R.string.room_tombstone_continuation_link)) {
+ textDecorationLine = "underline"
+ onClick = { delegate?.onUrlClicked(roomLink) }
+ }
+ }
+ messageView.movementMethod = BetterLinkMovementMethod.getInstance()
+ messageView.text = message
+ }
+
+ private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) {
+ visibility = View.VISIBLE
+ val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context)
+ val formatterMode: ResourceLimitErrorFormatter.Mode
+ val backgroundColor: Int
+ if (state.isSoft) {
+ backgroundColor = R.color.soft_resource_limit_exceeded
+ formatterMode = ResourceLimitErrorFormatter.Mode.Soft
+ } else {
+ backgroundColor = R.color.hard_resource_limit_exceeded
+ formatterMode = ResourceLimitErrorFormatter.Mode.Hard
+ }
+ val message = resourceLimitErrorFormatter.format(state.matrixError, formatterMode, clickable = true)
+ messageView.setTextColor(Color.WHITE)
+ messageView.text = message
+ messageView.movementMethod = LinkMovementMethod.getInstance()
+ messageView.setLinkTextColor(Color.WHITE)
+ setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
+ }
+
+ private fun renderConnectionError() {
+ visibility = View.VISIBLE
+ imageView.setImageResource(R.drawable.error)
+ messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
+ messageView.text = SpannableString(resources.getString(R.string.room_offline_notification))
+ }
+
+ private fun renderTyping(state: State.Typing) {
+ visibility = View.VISIBLE
+ imageView.setImageResource(R.drawable.vector_typing)
+ messageView.text = SpannableString(state.message)
+ messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
+ }
+
+ private fun renderUnreadPreview() {
+ visibility = View.VISIBLE
+ imageView.setImageResource(R.drawable.scrolldown)
+ messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
+ imageView.setOnClickListener { delegate?.closeScreen() }
+ }
+
+ private fun renderScrollToBottom(state: State.ScrollToBottom) {
+ visibility = View.VISIBLE
+ if (state.unreadCount > 0) {
+ imageView.setImageResource(R.drawable.newmessages)
+ messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
+ messageView.text = SpannableString(resources.getQuantityString(R.plurals.room_new_messages_notification, state.unreadCount, state.unreadCount))
+ } else {
+ imageView.setImageResource(R.drawable.scrolldown)
+ messageView.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_room_notification_text_color))
+ if (!TextUtils.isEmpty(state.message)) {
+ messageView.text = SpannableString(state.message)
+ }
+ }
+ messageView.setOnClickListener { delegate?.jumpToBottom() }
+ imageView.setOnClickListener { delegate?.jumpToBottom() }
+ }
+
+ private fun renderUnsent(state: State.UnsentEvents) {
+ visibility = View.VISIBLE
+ imageView.setImageResource(R.drawable.error)
+ val cancelAll = resources.getString(R.string.room_prompt_cancel)
+ val resendAll = resources.getString(R.string.room_prompt_resend)
+ val messageRes = if (state.hasUnknownDeviceEvents) R.string.room_unknown_devices_messages_notification else R.string.room_unsent_messages_notification
+ val message = context.getString(messageRes, resendAll, cancelAll)
+ val cancelAllPos = message.indexOf(cancelAll)
+ val resendAllPos = message.indexOf(resendAll)
+ val spannableString = SpannableString(message)
+ // cancelAllPos should always be > 0 but a GA crash reported here
+ if (cancelAllPos >= 0) {
+ spannableString.setSpan(CancelAllClickableSpan(), cancelAllPos, cancelAllPos + cancelAll.length, 0)
+ }
+
+ // resendAllPos should always be > 0 but a GA crash reported here
+ if (resendAllPos >= 0) {
+ spannableString.setSpan(ResendAllClickableSpan(), resendAllPos, resendAllPos + resendAll.length, 0)
+ }
+ messageView.movementMethod = LinkMovementMethod.getInstance()
+ messageView.setTextColor(ContextCompat.getColor(context, R.color.vector_fuchsia_color))
+ messageView.text = spannableString
+ }
+
+ private fun renderDefault() {
+ visibility = View.GONE
+ }
+
+ private fun renderHidden() {
+ visibility = View.GONE
+ }
+
+ /**
+ * Track the cancel all click.
+ */
+ private inner class CancelAllClickableSpan : ClickableSpan() {
+ override fun onClick(widget: View) {
+ delegate?.deleteUnsentEvents()
+ render(state)
+ }
+
+ override fun updateDrawState(ds: TextPaint) {
+ super.updateDrawState(ds)
+ ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
+ ds.bgColor = 0
+ ds.isUnderlineText = true
+ }
+ }
+
+ /**
+ * Track the resend all click.
+ */
+ private inner class ResendAllClickableSpan : ClickableSpan() {
+ override fun onClick(widget: View) {
+ delegate?.resendUnsentEvents()
+ render(state)
+ }
+
+ override fun updateDrawState(ds: TextPaint) {
+ super.updateDrawState(ds)
+ ds.color = ContextCompat.getColor(context, R.color.vector_fuchsia_color)
+ ds.bgColor = 0
+ ds.isUnderlineText = true
+ }
+ }
+
+ /**
+ * The state representing the view
+ * It can take one state at a time
+ * Priority of state is managed in {@link VectorRoomActivity.refreshNotificationsArea() }
+ */
+ sealed class State {
+
+ // Not yet rendered
+ object Initial : State()
+
+ // View will be Invisible
+ object Default : State()
+
+ // View will be Gone
+ object Hidden : State()
+
+ // Resource limit exceeded error will be displayed (only hard for the moment)
+ data class ResourceLimitExceededError(val isSoft: Boolean, val matrixError: MatrixError) : State()
+
+ // Server connection is lost
+ object ConnectionError : State()
+
+ // The room is dead
+ data class Tombstone(val tombstoneContent: RoomTombstoneContent) : State()
+
+ // Somebody is typing
+ data class Typing(val message: String) : State()
+
+ // Some new messages are unread in preview
+ object UnreadPreview : State()
+
+ // Some new messages are unread (grey or red)
+ data class ScrollToBottom(val unreadCount: Int, val message: String? = null) : State()
+
+ // Some event has been unsent
+ data class UnsentEvents(val hasUndeliverableEvents: Boolean, val hasUnknownDeviceEvents: Boolean) : State()
+ }
+
+ /**
+ * An interface to delegate some actions to another object
+ */
+ interface Delegate {
+ fun onUrlClicked(url: String)
+ fun resendUnsentEvents()
+ fun deleteUnsentEvents()
+ fun closeScreen()
+ fun jumpToBottom()
+ }
+
+ companion object {
+ /**
+ * Preference key.
+ */
+ private const val SHOW_INFO_AREA_KEY = "SETTINGS_SHOW_INFO_AREA_KEY"
+
+ /**
+ * Always show the info area.
+ */
+ private const val SHOW_INFO_AREA_VALUE_ALWAYS = "always"
+
+ /**
+ * Show the info area when it has messages or errors.
+ */
+ private const val SHOW_INFO_AREA_VALUE_MESSAGES_AND_ERRORS = "messages_and_errors"
+
+ /**
+ * Show the info area only when it has errors.
+ */
+ private const val SHOW_INFO_AREA_VALUE_ONLY_ERRORS = "only_errors"
+ }
+}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
index 204dfb5d..ab766e14 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt
@@ -76,6 +76,7 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp
+import im.vector.riotx.core.platform.NotificationAreaView
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
@@ -203,6 +204,7 @@ class RoomDetailFragment :
setupComposer()
setupAttachmentButton()
setupInviteView()
+ setupNotificationView()
roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(it) }
@@ -239,6 +241,36 @@ class RoomDetailFragment :
}
}
+ private fun setupNotificationView() {
+ notificationAreaView.delegate = object : NotificationAreaView.Delegate {
+
+ override fun onUrlClicked(url: String) {
+ permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor {
+ override fun navToRoom(roomId: String, eventId: String?): Boolean {
+ requireActivity().finish()
+ return false
+ }
+ })
+ }
+
+ override fun resendUnsentEvents() {
+ TODO("not implemented")
+ }
+
+ override fun deleteUnsentEvents() {
+ TODO("not implemented")
+ }
+
+ override fun closeScreen() {
+ TODO("not implemented")
+ }
+
+ override fun jumpToBottom() {
+ TODO("not implemented")
+ }
+ }
+ }
+
private fun exitSpecialMode() {
commandAutocompletePolicy.enabled = true
composerLayout.collapse()
@@ -259,17 +291,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody
- ?: messageContent.body)
+ ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document)
}
composerLayout.composerRelatedMessageContent.text = formattedBody
- ?: nonFormattedBody
+ ?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId
- ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
+ ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
@@ -298,9 +330,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
- ?: return
+ ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
- ?: return
+ ?: return
//TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
}
@@ -335,26 +367,26 @@ class RoomDetailFragment :
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
- R.drawable.ic_reply,
- object : RoomMessageTouchHelperCallback.QuickReplayHandler {
- override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
- (model as? AbsMessageItem)?.informationData?.let {
- val eventId = it.eventId
- roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
- }
- }
+ R.drawable.ic_reply,
+ object : RoomMessageTouchHelperCallback.QuickReplayHandler {
+ override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
+ (model as? AbsMessageItem)?.informationData?.let {
+ val eventId = it.eventId
+ roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
+ }
+ }
- override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
- return when (model) {
- is MessageFileItem,
- is MessageImageVideoItem,
- is MessageTextItem -> {
- return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
- }
- else -> false
- }
- }
- })
+ override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
+ return when (model) {
+ is MessageFileItem,
+ is MessageImageVideoItem,
+ is MessageTextItem -> {
+ return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
+ }
+ else -> false
+ }
+ }
+ })
val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView)
}
@@ -534,12 +566,14 @@ class RoomDetailFragment :
} else if (state.asyncInviter.complete) {
vectorBaseActivity.finish()
}
+
if (state.tombstoneContent == null) {
composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(state.isEncrypted)
+ notificationAreaView.render(NotificationAreaView.State.Hidden)
} else {
composerLayout.visibility = View.GONE
- showSnackWithMessage("TOMBSTONED", duration = Snackbar.LENGTH_INDEFINITE)
+ notificationAreaView.render(NotificationAreaView.State.Tombstone(state.tombstoneContent))
}
}
@@ -636,7 +670,7 @@ class RoomDetailFragment :
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), view, ViewCompat.getTransitionName(view)
- ?: "").toBundle()
+ ?: "").toBundle()
startActivity(intent, bundle)
}
@@ -716,7 +750,17 @@ class RoomDetailFragment :
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
}
-// AutocompleteUserPresenter.Callback
+
+ override fun onRoomCreateLinkClicked(url: String) {
+ permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
+ override fun navToRoom(roomId: String, eventId: String?): Boolean {
+ requireActivity().finish()
+ return false
+ }
+ })
+ }
+
+ // AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query))
@@ -730,7 +774,7 @@ class RoomDetailFragment :
}
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
val messageInformationData = actionData.data as? MessageInformationData
- ?: return
+ ?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
}
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
index 0b78f815..e8268bac 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt
@@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
fun onEventVisible(event: TimelineEvent)
+ fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View)
@@ -158,7 +159,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight
- || modelCache[i]?.eventId == this.eventIdToHighlight) {
+ || modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null
}
}
@@ -219,8 +220,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
// Should be build if not cached or if cached but contains mergedHeader or formattedDay
// We then are sure we always have items up to date.
if (modelCache[position] == null
- || modelCache[position]?.mergedHeaderModel != null
- || modelCache[position]?.formattedDayModel != null) {
+ || modelCache[position]?.mergedHeaderModel != null
+ || modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot)
}
}
@@ -294,7 +295,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
- ?: true
+ ?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt
index 21fe85d7..e32e2746 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt
@@ -18,7 +18,6 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
-import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
@@ -37,21 +36,16 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
val createRoomContent = event.root.getClearContent().toModel()
- ?: return null
+ ?: return null
val predecessor = createRoomContent.predecessor ?: return null
val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: return null
- val urlSpan = MatrixPermalinkSpan(roomLink, object : MatrixPermalinkSpan.Callback {
- override fun onUrlClicked(url: String) {
- callback?.onUrlClicked(roomLink)
- }
- })
- val textColorInt = colorProvider.getColor(R.color.riot_primary_text_color_light)
val text = span {
- text = stringProvider.getString(R.string.room_tombstone_continuation_description)
- append("\n")
- append(
- stringProvider.getString(R.string.room_tombstone_predecessor_link)
- )
+ +stringProvider.getString(R.string.room_tombstone_continuation_description)
+ +"\n"
+ span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
+ textDecorationLine = "underline"
+ onClick = { callback?.onRoomCreateLinkClicked(roomLink) }
+ }
}
return RoomCreateItem_()
.text(text)
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt
index 117f4bd2..3e5ef30d 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/RoomCreateItem.kt
@@ -21,10 +21,10 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
-import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
+import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_create)
abstract class RoomCreateItem : VectorEpoxyModel() {
@@ -32,6 +32,7 @@ abstract class RoomCreateItem : VectorEpoxyModel() {
@EpoxyAttribute lateinit var text: CharSequence
override fun bind(holder: Holder) {
+ holder.description.movementMethod = BetterLinkMovementMethod.getInstance()
holder.description.text = text
}
diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml
index f8cec433..ae38b510 100644
--- a/vector/src/main/res/layout/fragment_room_detail.xml
+++ b/vector/src/main/res/layout/fragment_room_detail.xml
@@ -74,12 +74,18 @@
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
- app:layout_constraintBottom_toTopOf="@+id/composerLayout"
+ app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:listitem="@layout/item_timeline_event_base" />
+
+
+