mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Tombstone : add notification area and handle links
This commit is contained in:
parent
9e5c70dda3
commit
9a1e16a170
10 changed files with 539 additions and 51 deletions
|
@ -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"
|
||||
}
|
||||
}
|
33
vector/src/debug/res/layout/view_notification_area.xml
Normal file
33
vector/src/debug/res/layout/view_notification_area.xml
Normal 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>
|
|
@ -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>"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<RoomCreateContent>()
|
||||
?: 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)
|
||||
|
|
|
@ -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<RoomCreateItem.Holder>() {
|
||||
|
@ -32,6 +32,7 @@ abstract class RoomCreateItem : VectorEpoxyModel<RoomCreateItem.Holder>() {
|
|||
@EpoxyAttribute lateinit var text: CharSequence
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
holder.description.movementMethod = BetterLinkMovementMethod.getInstance()
|
||||
holder.description.text = text
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<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
|
||||
android:id="@+id/composerLayout"
|
||||
|
@ -89,6 +95,16 @@
|
|||
app:layout_constraintEnd_toEndOf="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
|
||||
android:id="@+id/inviteView"
|
||||
android:layout_width="0dp"
|
||||
|
|
|
@ -12,9 +12,8 @@
|
|||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginTop="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:drawableLeft="@drawable/error"
|
||||
android:drawablePadding="16dp"
|
||||
android:gravity="center|start"
|
||||
android:minHeight="80dp"
|
||||
|
|
Loading…
Reference in a new issue