Tombstone : add notification area and handle links

This commit is contained in:
ganfra 2019-07-26 14:51:14 +02:00
parent 9e5c70dda3
commit 9a1e16a170
10 changed files with 539 additions and 51 deletions

View File

@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
@Json(name = "errcode") val code: String, @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 { companion object {
const val FORBIDDEN = "M_FORBIDDEN" const val FORBIDDEN = "M_FORBIDDEN"
@ -55,5 +60,8 @@ data class MatrixError(
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user"
} }
} }

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
tools:background="@color/vector_fuchsia_color"
tools:parentTag="android.widget.RelativeLayout">
<ImageView
android:id="@+id/room_notification_icon"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="24dp"
android:padding="5dp"
tools:src="@drawable/vector_typing" />
<TextView
android:id="@+id/room_notification_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="64dp"
android:layout_marginEnd="16dp"
android:accessibilityLiveRegion="polite"
android:textColor="?attr/vctr_room_notification_text_color"
tools:text="a text here" />
</merge>

View File

@ -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 "<a href=\"$uri\">$contactStr</a>"
}
}

View File

@ -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"
}
}

View File

@ -76,6 +76,7 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp 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.platform.VectorBaseFragment
import im.vector.riotx.core.utils.* import im.vector.riotx.core.utils.*
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
@ -203,6 +204,7 @@ class RoomDetailFragment :
setupComposer() setupComposer()
setupAttachmentButton() setupAttachmentButton()
setupInviteView() setupInviteView()
setupNotificationView()
roomDetailViewModel.subscribe { renderState(it) } roomDetailViewModel.subscribe { renderState(it) }
textComposerViewModel.subscribe { renderTextComposerState(it) } textComposerViewModel.subscribe { renderTextComposerState(it) }
roomDetailViewModel.sendMessageResultLiveData.observeEvent(this) { renderSendMessageResult(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() { private fun exitSpecialMode() {
commandAutocompletePolicy.enabled = true commandAutocompletePolicy.enabled = true
composerLayout.collapse() composerLayout.collapse()
@ -259,17 +291,17 @@ class RoomDetailFragment :
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
val parser = Parser.builder().build() val parser = Parser.builder().build()
val document = parser.parse(messageContent.formattedBody val document = parser.parse(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
formattedBody = eventHtmlRenderer.render(document) formattedBody = eventHtmlRenderer.render(document)
} }
composerLayout.composerRelatedMessageContent.text = formattedBody composerLayout.composerRelatedMessageContent.text = formattedBody
?: nonFormattedBody ?: nonFormattedBody
composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "") composerLayout.composerEditText.setText(if (useText) event.getTextEditableContent() else "")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
avatarRenderer.render(event.senderAvatar, event.root.senderId avatarRenderer.render(event.senderAvatar, event.root.senderId
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar) ?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length) composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand { composerLayout.expand {
@ -298,9 +330,9 @@ class RoomDetailFragment :
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
REACTION_SELECT_REQUEST_CODE -> { REACTION_SELECT_REQUEST_CODE -> {
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
?: return ?: return
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
?: return ?: return
//TODO check if already reacted with that? //TODO check if already reacted with that?
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
} }
@ -335,26 +367,26 @@ class RoomDetailFragment :
if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) { if (VectorPreferences.swipeToReplyIsEnabled(requireContext())) {
val swipeCallback = RoomMessageTouchHelperCallback(requireContext(), val swipeCallback = RoomMessageTouchHelperCallback(requireContext(),
R.drawable.ic_reply, R.drawable.ic_reply,
object : RoomMessageTouchHelperCallback.QuickReplayHandler { object : RoomMessageTouchHelperCallback.QuickReplayHandler {
override fun performQuickReplyOnHolder(model: EpoxyModel<*>) { override fun performQuickReplyOnHolder(model: EpoxyModel<*>) {
(model as? AbsMessageItem)?.informationData?.let { (model as? AbsMessageItem)?.informationData?.let {
val eventId = it.eventId val eventId = it.eventId
roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId)) roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(eventId))
} }
} }
override fun canSwipeModel(model: EpoxyModel<*>): Boolean { override fun canSwipeModel(model: EpoxyModel<*>): Boolean {
return when (model) { return when (model) {
is MessageFileItem, is MessageFileItem,
is MessageImageVideoItem, is MessageImageVideoItem,
is MessageTextItem -> { is MessageTextItem -> {
return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED return (model as AbsMessageItem).informationData.sendState == SendState.SYNCED
} }
else -> false else -> false
} }
} }
}) })
val touchHelper = ItemTouchHelper(swipeCallback) val touchHelper = ItemTouchHelper(swipeCallback)
touchHelper.attachToRecyclerView(recyclerView) touchHelper.attachToRecyclerView(recyclerView)
} }
@ -534,12 +566,14 @@ class RoomDetailFragment :
} else if (state.asyncInviter.complete) { } else if (state.asyncInviter.complete) {
vectorBaseActivity.finish() vectorBaseActivity.finish()
} }
if (state.tombstoneContent == null) { if (state.tombstoneContent == null) {
composerLayout.visibility = View.VISIBLE composerLayout.visibility = View.VISIBLE
composerLayout.setRoomEncrypted(state.isEncrypted) composerLayout.setRoomEncrypted(state.isEncrypted)
notificationAreaView.render(NotificationAreaView.State.Hidden)
} else { } else {
composerLayout.visibility = View.GONE 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 intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation( val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), view, ViewCompat.getTransitionName(view) requireActivity(), view, ViewCompat.getTransitionName(view)
?: "").toBundle() ?: "").toBundle()
startActivity(intent, bundle) startActivity(intent, bundle)
} }
@ -716,7 +750,17 @@ class RoomDetailFragment :
ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData) ViewEditHistoryBottomSheet.newInstance(roomDetailArgs.roomId, informationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") .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?) { override fun onQueryUsers(query: CharSequence?) {
textComposerViewModel.process(TextComposerActions.QueryUsers(query)) textComposerViewModel.process(TextComposerActions.QueryUsers(query))
@ -730,7 +774,7 @@ class RoomDetailFragment :
} }
MessageMenuViewModel.ACTION_VIEW_REACTIONS -> { MessageMenuViewModel.ACTION_VIEW_REACTIONS -> {
val messageInformationData = actionData.data as? MessageInformationData val messageInformationData = actionData.data as? MessageInformationData
?: return ?: return
ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData) ViewReactionBottomSheet.newInstance(roomDetailArgs.roomId, messageInformationData)
.show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS")
} }

View File

@ -54,6 +54,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Tim
interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback { interface Callback : ReactionPillCallback, AvatarCallback, BaseCallback, UrlClickCallback {
fun onEventVisible(event: TimelineEvent) fun onEventVisible(event: TimelineEvent)
fun onRoomCreateLinkClicked(url: String)
fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View) fun onEncryptedMessageClicked(informationData: MessageInformationData, view: View)
fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View) fun onImageMessageClicked(messageImageContent: MessageImageContent, mediaData: ImageContentRenderer.Data, view: View)
fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.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) { synchronized(modelCache) {
for (i in 0 until modelCache.size) { for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == eventIdToHighlight if (modelCache[i]?.eventId == eventIdToHighlight
|| modelCache[i]?.eventId == this.eventIdToHighlight) { || modelCache[i]?.eventId == this.eventIdToHighlight) {
modelCache[i] = null 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 // 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. // We then are sure we always have items up to date.
if (modelCache[position] == null if (modelCache[position] == null
|| modelCache[position]?.mergedHeaderModel != null || modelCache[position]?.mergedHeaderModel != null
|| modelCache[position]?.formattedDayModel != null) { || modelCache[position]?.formattedDayModel != null) {
modelCache[position] = buildItemModels(position, currentSnapshot) 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 // => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull() val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey) val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true ?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState } val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) { if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds) collapsedEventIds.addAll(mergedEventIds)

View File

@ -18,7 +18,6 @@
package im.vector.riotx.features.home.room.detail.timeline.factory 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.permalinks.PermalinkFactory
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent 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? { fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>() val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
?: return null ?: return null
val predecessor = createRoomContent.predecessor ?: return null val predecessor = createRoomContent.predecessor ?: return null
val roomLink = PermalinkFactory.createPermalink(predecessor.roomId) ?: 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 { val text = span {
text = stringProvider.getString(R.string.room_tombstone_continuation_description) +stringProvider.getString(R.string.room_tombstone_continuation_description)
append("\n") +"\n"
append( span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) {
stringProvider.getString(R.string.room_tombstone_predecessor_link) textDecorationLine = "underline"
) onClick = { callback?.onRoomCreateLinkClicked(roomLink) }
}
} }
return RoomCreateItem_() return RoomCreateItem_()
.text(text) .text(text)

View File

@ -21,10 +21,10 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_create) @EpoxyModelClass(layout = R.layout.item_timeline_event_create)
abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() { abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
@ -32,6 +32,7 @@ abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
@EpoxyAttribute lateinit var text: CharSequence @EpoxyAttribute lateinit var text: CharSequence
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.description.movementMethod = BetterLinkMovementMethod.getInstance()
holder.description.text = text holder.description.text = text
} }

View File

@ -74,12 +74,18 @@
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/composerLayout" app:layout_constraintBottom_toTopOf="@+id/recyclerViewBarrier"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/roomToolbar" app:layout_constraintTop_toBottomOf="@id/roomToolbar"
tools:listitem="@layout/item_timeline_event_base" /> tools:listitem="@layout/item_timeline_event_base" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/recyclerViewBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="composerLayout,notificationAreaView" />
<im.vector.riotx.features.home.room.detail.composer.TextComposerView <im.vector.riotx.features.home.room.detail.composer.TextComposerView
android:id="@+id/composerLayout" android:id="@+id/composerLayout"
@ -89,6 +95,16 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<im.vector.riotx.core.platform.NotificationAreaView
android:id="@+id/notificationAreaView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<im.vector.riotx.features.invite.VectorInviteView <im.vector.riotx.features.invite.VectorInviteView
android:id="@+id/inviteView" android:id="@+id/inviteView"
android:layout_width="0dp" android:layout_width="0dp"

View File

@ -12,9 +12,8 @@
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:background="@drawable/bg_tombstone_predecessor" android:background="?attr/riotx_keys_backup_banner_accent_color"
android:drawableStart="@drawable/error" android:drawableStart="@drawable/error"
android:drawableLeft="@drawable/error"
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:gravity="center|start" android:gravity="center|start"
android:minHeight="80dp" android:minHeight="80dp"