Incoming DM verification handling in timeline

This commit is contained in:
Valere 2019-12-11 16:48:33 +01:00
parent 02f03e6b23
commit 0776a301ea
32 changed files with 963 additions and 107 deletions

View File

@ -27,4 +27,3 @@ class ReferencesAggregatedSummary(
val sourceEvents: List<String>, val sourceEvents: List<String>,
val localEchos: List<String> val localEchos: List<String>
) )

View File

@ -17,6 +17,3 @@ internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, tx
this.eventId = txID this.eventId = txID
} }
} }

View File

@ -63,4 +63,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ClearSendQueue : RoomDetailAction() object ClearSendQueue : RoomDetailAction()
object ResendAll : RoomDetailAction() object ResendAll : RoomDetailAction()
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction()
} }

View File

@ -1024,6 +1024,10 @@ class RoomDetailFragment @Inject constructor(
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
} }
override fun onTimelineItemAction(itemAction: RoomDetailAction) {
roomDetailViewModel.handle(itemAction)
}
override fun onRoomCreateLinkClicked(url: String) { override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean { override fun navToRoom(roomId: String, eventId: String?): Boolean {

View File

@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
@ -177,6 +178,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
} }
} }
@ -786,6 +789,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}) })
} }
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
session.getSasVerificationService().beginKeyVerificationInDMs(
KeyVerificationStart.VERIF_METHOD_SAS,
action.transactionId,
room.roomId,
action.otherUserId,
action.otherdDeviceId,
null
)
}
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
Timber.e("TODO implement $action")
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.features.home.room.detail.RoomDetailAction
import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.UnreadState
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
@ -62,6 +63,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData) fun onEditedDecorationClicked(informationData: MessageInformationData)
// TODO move all callbacks to this?
fun onTimelineItemAction(itemAction: RoomDetailAction)
} }
interface ReactionPillCallback { interface ReactionPillCallback {

View File

@ -23,10 +23,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.isTextMessage
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.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -172,6 +169,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer.get().render(messageContent.formattedBody eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
} else if (messageContent is MessageVerificationRequestContent) {
stringProvider.getString(R.string.verification_request)
} else { } else {
messageContent?.body messageContent?.body
} }

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory package im.vector.riotx.features.home.room.detail.timeline.factory
import android.view.View import android.view.View
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
@ -34,7 +35,8 @@ import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider) { private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
highlight: Boolean, highlight: Boolean,
@ -46,7 +48,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
sendState = event.root.sendState, sendState = event.root.sendState,
avatarUrl = event.senderAvatar, avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(), memberName = event.getDisambiguatedDisplayName(),
showInformation = false showInformation = false,
sentByMe = event.root.senderId == session.myUserId
) )
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,

View File

@ -24,6 +24,7 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.RelationType
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.message.* import im.vector.matrix.android.api.session.room.model.message.*
@ -64,7 +65,8 @@ class MessageItemFactory @Inject constructor(
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider) { private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -104,6 +106,7 @@ class MessageItemFactory @Inject constructor(
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
} }
} }
@ -128,6 +131,51 @@ class MessageItemFactory @Inject constructor(
})) }))
} }
private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
return null
}
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
else informationData.memberName
return VerificationRequestItem_()
.attributes(
VerificationRequestItem.Attributes(
otherUserId,
otherUserName.toString(),
messageContent.fromDevice,
informationData.eventId,
informationData,
attributes.avatarRenderer,
attributes.colorProvider,
attributes.itemLongClickListener,
attributes.itemClickListener,
attributes.reactionPillCallback,
attributes.readReceiptsCallback,
attributes.emojiTypeFace
)
)
.callback(callback)
// .izLocalFile(messageContent.getFileUrl().isLocalFile())
// .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
// .filename(messageContent.body)
// .iconRes(R.drawable.filetype_audio)
// .clickListener(
// DebouncedClickListener(View.OnClickListener {
// callback?.onAudioMessageClicked(messageContent)
// }))
}
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
@ -193,7 +241,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,

View File

@ -28,7 +28,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val encryptedItemFactory: EncryptedItemFactory, private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory) { private val roomCreateItemFactory: RoomCreateItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -66,13 +67,15 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
} }
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> { EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case // These events are filtered from timeline in normal case
// Only visible in developer mode // Only visible in developer mode
defaultItemFactory.create(event, highlight, callback) noticeItemFactory.create(event, highlight, callback)
}
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback)
} }
// Unhandled event types (yet) // Unhandled event types (yet)

View File

@ -0,0 +1,116 @@
/*
* 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.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
import javax.inject.Inject
/**
* Can creates verification conclusion items
* Notice that not all KEY_VERIFICATION_DONE will be displayed in timeline,
* several checks are made to see if this conclusion is attached to a known request
*/
class VerificationItemFactory @Inject constructor(
private val colorProvider: ColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory,
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session
) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null
val relContent: MessageRelationContent = event.root.content.toModel()
?: event.root.getClearContent().toModel()
?: return ignoredConclusion(event, highlight, callback)
if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback)
val refEventId = relContent.relatesTo?.eventId
?: return ignoredConclusion(event, highlight, callback)
// If we cannot find the referenced request we do not display the done event
val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
?: return ignoredConclusion(event, highlight, callback)
// If it's not a request ignore this event
if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
// Is the request referenced is actually really completed?
val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) return ignoredConclusion(event, highlight, callback)
val informationData = messageInformationDataFactory.create(event, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
when (event.root.getClearType()) {
EventType.KEY_VERIFICATION_DONE -> {
// We only tale the one sent by me
if (informationData.sentByMe) {
// We only display the done sent by the other user, the done send by me is ignored
return ignoredConclusion(event, highlight, callback)
}
return VerificationRequestConclusionItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
colorProvider = colorProvider,
emojiTypeFace = attributes.emojiTypeFace,
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}
return null
}
private fun ignoredConclusion(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
return null
}
}

View File

@ -44,6 +44,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.MESSAGE, EventType.MESSAGE,
EventType.REACTION, EventType.REACTION,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_KEY,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")

View File

@ -20,15 +20,19 @@ package im.vector.riotx.features.home.room.detail.timeline.helper
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.getColorFromUserId
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData
import me.gujun.android.span.span import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
@ -86,7 +90,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map { .map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
} }
.toList() .toList(),
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
val stateStr = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationSummary
ReferencesInfoData(
VerificationState.values().firstOrNull { stateStr == it.name }
?: VerificationState.REQUEST
)
},
sentByMe = event.root.senderId == session.myUserId
) )
} }
} }

View File

@ -0,0 +1,142 @@
/*
* 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.features.home.room.detail.timeline.item
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.ui.getMessageTextColor
/**
* Base timeline item with reactions and read receipts.
* Manages associated click listeners and send status.
* Should not be used as this, use a subclass.
*/
abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem<H>() {
abstract val baseAttributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
})
private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
}
override fun onUnReacted(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, false)
}
override fun onLongClick(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onLongClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString)
}
}
open fun shouldShowReactionAtBottom(): Boolean {
return true
}
override fun getEventIds(): List<String> {
return listOf(baseAttributes.informationData.eventId)
}
override fun bind(holder: H) {
super.bind(holder)
holder.readReceiptsView.render(
baseAttributes.informationData.readReceipts,
baseAttributes.avatarRenderer,
_readReceiptsClickListener
)
val reactions = baseAttributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
} else {
holder.reactionsContainer.isVisible = true
holder.reactionsContainer.removeAllViews()
reactions.take(8).forEach { reaction ->
val reactionButton = ReactionButton(holder.view.context)
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
holder.reactionsContainer.addView(reactionButton)
}
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
holder.view.setOnClickListener(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
override fun unbind(holder: H) {
holder.readReceiptsView.unbind()
super.unbind(holder)
}
protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
root.isClickable = baseAttributes.informationData.sendState.isSent()
val state = if (baseAttributes.informationData.hasPendingEdits) SendState.UNSENT else baseAttributes.informationData.sendState
textView?.setTextColor(baseAttributes.colorProvider.getMessageTextColor(state))
failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
}
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
}
/**
* This class holds all the common attributes for timeline items.
*/
interface Attributes {
// val avatarSize: Int,
val informationData: MessageInformationData
val avatarRenderer: AvatarRenderer
val colorProvider: ColorProvider
val itemLongClickListener: View.OnLongClickListener?
val itemClickListener: View.OnClickListener?
// val memberClickListener: View.OnClickListener?
val reactionPillCallback: TimelineEventController.ReactionPillCallback?
// val avatarCallback: TimelineEventController.AvatarCallback?
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
// val emojiTypeFace: Typeface?
}
// data class AbsAttributes(
// override val informationData: MessageInformationData,
// override val avatarRenderer: AvatarRenderer,
// override val colorProvider: ColorProvider,
// override val itemLongClickListener: View.OnLongClickListener? = null,
// override val itemClickListener: View.OnClickListener? = null,
// override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
// override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
// ) : Attributes
}

View File

@ -18,22 +18,24 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.ui.getMessageTextColor
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() { /**
* Base timeline item that adds an optional information bar with the sender avatar, name and time
* Adds associated click listeners (on avatar, displayname)
*/
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>() {
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
@ -45,24 +47,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
attributes.avatarCallback?.onMemberNameClicked(attributes.informationData) attributes.avatarCallback?.onMemberNameClicked(attributes.informationData)
}) })
private val _readReceiptsClickListener = DebouncedClickListener(View.OnClickListener {
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, true)
}
override fun onUnReacted(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onClickOnReactionPill(attributes.informationData, reactionButton.reactionString, false)
}
override fun onLongClick(reactionButton: ReactionButton) {
attributes.reactionPillCallback?.onLongClickOnReactionPill(attributes.informationData, reactionButton.reactionString)
}
}
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
if (attributes.informationData.showInformation) { if (attributes.informationData.showInformation) {
@ -94,60 +78,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
} }
holder.view.setOnClickListener(attributes.itemClickListener)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(
attributes.informationData.readReceipts,
attributes.avatarRenderer,
_readReceiptsClickListener
)
val reactions = attributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false
} else {
holder.reactionsContainer.isVisible = true
holder.reactionsContainer.removeAllViews()
reactions.take(8).forEach { reaction ->
val reactionButton = ReactionButton(holder.view.context)
reactionButton.reactedListener = reactionClickListener
reactionButton.setTag(R.id.reactionsContainer, reaction.key)
reactionButton.reactionString = reaction.key
reactionButton.reactionCount = reaction.count
reactionButton.setChecked(reaction.addedByMe)
reactionButton.isEnabled = reaction.synced
holder.reactionsContainer.addView(reactionButton)
}
holder.reactionsContainer.setOnLongClickListener(attributes.itemLongClickListener)
}
} }
override fun unbind(holder: H) { abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
holder.readReceiptsView.unbind()
super.unbind(holder)
}
open fun shouldShowReactionAtBottom(): Boolean {
return true
}
override fun getEventIds(): List<String> {
return listOf(attributes.informationData.eventId)
}
protected open fun renderSendState(root: View, textView: TextView?, failureIndicator: ImageView? = null) {
root.isClickable = attributes.informationData.sendState.isSent()
val state = if (attributes.informationData.hasPendingEdits) SendState.UNSENT else attributes.informationData.sendState
textView?.setTextColor(attributes.colorProvider.getMessageTextColor(state))
failureIndicator?.isVisible = attributes.informationData.sendState.hasFailed()
}
abstract class Holder(@IdRes stubId: Int) : BaseHolder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView) val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView) val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView) val timeView by bind<TextView>(R.id.messageTimeView)
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
} }
/** /**
@ -155,15 +91,15 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
*/ */
data class Attributes( data class Attributes(
val avatarSize: Int, val avatarSize: Int,
val informationData: MessageInformationData, override val informationData: MessageInformationData,
val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
val colorProvider: ColorProvider, override val colorProvider: ColorProvider,
val itemLongClickListener: View.OnLongClickListener? = null, override val itemLongClickListener: View.OnLongClickListener? = null,
val itemClickListener: View.OnClickListener? = null, override val itemClickListener: View.OnClickListener? = null,
val memberClickListener: View.OnClickListener? = null, val memberClickListener: View.OnClickListener? = null,
val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null, override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null, val avatarCallback: TimelineEventController.AvatarCallback? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null val emojiTypeFace: Typeface? = null
) ) : AbsBaseMessageItem.Attributes
} }

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
import android.os.Parcelable import android.os.Parcelable
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.session.room.VerificationState
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@Parcelize @Parcelize
@ -33,7 +34,14 @@ data class MessageInformationData(
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList() val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null,
val sentByMe : Boolean
) : Parcelable
@Parcelize
data class ReferencesInfoData(
val verificationStatus: VerificationState
) : Parcelable ) : Parcelable
@Parcelize @Parcelize

View File

@ -0,0 +1,77 @@
/*
* 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.features.home.room.detail.timeline.item
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.view.View
import android.widget.RelativeLayout
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<VerificationRequestConclusionItem.Holder>() {
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
@EpoxyAttribute
lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {
super.bind(holder)
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
holder.titleView.text = holder.view.context.getString(R.string.sas_verified)
holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
}
class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
val titleView by bind<AppCompatTextView>(R.id.itemVerificationDoneTitleTextView)
val descriptionView by bind<AppCompatTextView>(R.id.itemVerificationDoneDetailTextView)
val endGuideline by bind<View>(R.id.messageEndGuideline)
}
companion object {
private const val STUB_ID = R.id.messageVerificationDoneStub
}
/**
* This class holds all the common attributes for timeline items.
*/
data class Attributes(
val toUserId: String,
val toUserName: String,
override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer,
override val colorProvider: ColorProvider,
override val itemLongClickListener: View.OnLongClickListener? = null,
override val itemClickListener: View.OnClickListener? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
) : AbsBaseMessageItem.Attributes
}

View File

@ -0,0 +1,177 @@
/*
* 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.features.home.room.detail.timeline.item
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.RoomDetailAction
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestItem.Holder>() {
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
@EpoxyAttribute
lateinit var attributes: Attributes
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
override fun getViewType() = STUB_ID
@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {
super.bind(holder)
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
holder.titleView.text = if (attributes.informationData.sentByMe)
holder.view.context.getString(R.string.verification_sent)
// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name
// ?: "??"}"
else
holder.view.context.getString(R.string.verification_request)
// + "\n ${attributes.informationData.referencesInfoData?.verificationStatus?.name
// ?: "??"}"
holder.descriptionView.text = if (!attributes.informationData.sentByMe)
"${attributes.informationData.memberName} (${attributes.informationData.senderId})"
else
"${attributes.otherUserName} (${attributes.otherUserId})"
when (attributes.informationData.referencesInfoData?.verificationStatus) {
VerificationState.REQUEST,
null -> {
holder.buttonBar.isVisible = !attributes.informationData.sentByMe
holder.statusTextView.text = null
holder.statusTextView.isVisible = false
}
VerificationState.CANCELED_BY_OTHER -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
holder.statusTextView.isVisible = true
}
VerificationState.CANCELED_BY_ME -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_you_cancelled)
holder.statusTextView.isVisible = true
}
VerificationState.WAITING -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_waiting)
holder.statusTextView.isVisible = true
}
VerificationState.DONE -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = if (attributes.informationData.sentByMe)
holder.view.context.getString(R.string.verification_request_other_accepted, attributes.otherUserName)
else
holder.view.context.getString(R.string.verification_request_you_accepted)
holder.statusTextView.isVisible = true
}
else -> {
holder.buttonBar.isVisible = false
holder.statusTextView.text = null
holder.statusTextView.isVisible = false
}
}
holder.callback = callback
holder.attributes = attributes
}
override fun unbind(holder: Holder) {
super.unbind(holder)
holder.callback = null
holder.attributes = null
}
class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
var callback: TimelineEventController.Callback? = null
var attributes: Attributes? = null
private val _clickListener = DebouncedClickListener(View.OnClickListener {
val att = attributes ?: return@OnClickListener
if (it == acceptButton) {
callback?.onTimelineItemAction(RoomDetailAction.AcceptVerificationRequest(
att.referenceId,
att.otherUserId,
att.fromDevide))
} else if (it == declineButton) {
callback?.onTimelineItemAction(RoomDetailAction.DeclineVerificationRequest(att.referenceId))
}
})
val titleView by bind<AppCompatTextView>(R.id.itemVerificationTitleTextView)
val descriptionView by bind<AppCompatTextView>(R.id.itemVerificationDetailTextView)
val buttonBar by bind<ViewGroup>(R.id.itemVerificationButtonBar)
val statusTextView by bind<TextView>(R.id.itemVerificationStatusText)
val endGuideline by bind<View>(R.id.messageEndGuideline)
private val declineButton by bind<Button>(R.id.sas_verification_verified_decline_button)
private val acceptButton by bind<Button>(R.id.sas_verification_verified_accept_button)
override fun bindView(itemView: View) {
super.bindView(itemView)
acceptButton.setOnClickListener(_clickListener)
declineButton.setOnClickListener(_clickListener)
}
}
companion object {
private const val STUB_ID = R.id.messageVerificationRequestStub
}
/**
* This class holds all the common attributes for timeline items.
*/
data class Attributes(
val otherUserId: String,
val otherUserName: String,
val fromDevide: String,
val referenceId: String,
// val avatarSize: Int,
override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer,
override val colorProvider: ColorProvider,
override val itemLongClickListener: View.OnLongClickListener? = null,
override val itemClickListener: View.OnClickListener? = null,
// val memberClickListener: View.OnClickListener? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
// val avatarCallback: TimelineEventController.AvatarCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
) : AbsBaseMessageItem.Attributes
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/riotx_button_disabled_alpha12" android:state_enabled="false" />
<item android:color="@color/riotx_destructive_accent_alpha12" android:state_enabled="true" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/riotx_disabled_accent" android:state_enabled="false" />
<item android:color="@color/button_destructive_enabled_text_color" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/riotx_button_disabled_alpha12" android:state_enabled="false" />
<item android:color="@color/riotx_positive_accent_alpha12" android:state_enabled="true" />
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/riotx_disabled_accent" android:state_enabled="false" />
<item android:color="@color/riotx_positive_accent" />
</selector>

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,21C12,21 21,17.2 21,11.5V4.85L12,2L3,4.85V11.5C3,17.2 12,21 12,21Z"
android:strokeLineJoin="round"
android:fillColor="#2E2F32"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M12,21C12,21 21,17.2 21,11.5V4.85L12,2L3,4.85V11.5C3,17.2 12,21 12,21Z"
android:strokeLineJoin="round"
android:fillColor="#03B381"
android:fillType="evenOdd"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M17.2268,7.8065C17.6053,8.1718 17.6053,8.7639 17.2268,9.1291L11.4013,14.7502C11.0228,15.1154 10.4091,15.1154 10.0306,14.7502L10.0145,14.7342C10.0084,14.7286 10.0023,14.7229 9.9964,14.7171L7.3235,12.1381C6.926,11.7546 6.926,11.1328 7.3235,10.7493C7.7209,10.3658 8.3653,10.3658 8.7627,10.7493L10.7838,12.6995L15.8561,7.8065C16.2346,7.4413 16.8483,7.4413 17.2268,7.8065Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="40dp" android:height="40dp"/>
<solid android:color="?vctr_list_header_background_color" />
<corners android:radius="8dp" />
</shape>

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground">
<im.vector.riotx.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/informationBottom"
android:layout_alignParentTop="true"
android:background="?riotx_highlighted_message_background" />
<View
android:id="@+id/messageStartGuideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="52dp" />
<View
android:id="@+id/messageEndGuideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="52dp" />
<FrameLayout
android:id="@+id/viewStubContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_toStartOf="@id/messageEndGuideline"
android:layout_toEndOf="@id/messageStartGuideline"
android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp">
<ViewStub
android:id="@+id/messageVerificationRequestStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_verification_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageVerificationDoneStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_verification_done_stub"
tools:visibility="visible" />
</FrameLayout>
<LinearLayout
android:id="@+id/informationBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/viewStubContainer"
android:layout_toEndOf="@id/messageStartGuideline"
android:orientation="vertical">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/reactionsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginBottom="4dp"
app:dividerDrawable="@drawable/reaction_divider"
app:flexWrap="wrap"
app:showDivider="middle"
tools:background="#F0E0F0"
tools:layout_height="40dp">
<!-- ReactionButtons will be added here in the code -->
<!--im.vector.riotx.features.reactions.widget.ReactionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content" /-->
</com.google.android.flexbox.FlexboxLayout>
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<TextView
android:id="@+id/itemVerificationDoneTitleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:drawableStart="@drawable/ic_shield_trusted"
android:drawablePadding="6dp"
android:gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="@string/sas_verified" />
<TextView
android:id="@+id/itemVerificationDoneDetailTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
tools:text="Alice (@alice:matrix.org)" />
</LinearLayout>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical">
<TextView
android:id="@+id/itemVerificationTitleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:drawableStart="@drawable/ic_shield_black"
android:drawablePadding="6dp"
android:gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="@string/verification_request" />
<TextView
android:id="@+id/itemVerificationDetailTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:textColor="?riotx_text_primary"
android:textSize="12sp"
tools:text="Alice (@alice:matrix.org)" />
<LinearLayout
android:id="@+id/itemVerificationButtonBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_verification_verified_decline_button"
style="@style/VectorButtonStyleDestructive"
android:layout_marginEnd="16dp"
android:text="@string/decline" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sas_verification_verified_accept_button"
style="@style/VectorButtonStylePositive"
android:text="@string/accept" />
</LinearLayout>
<TextView
android:id="@+id/itemVerificationStatusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/verification_request_you_accepted"
android:textColor="?attr/vctr_notice_secondary"
android:visibility="gone"
tools:visibility="visible"
android:textSize="13sp" />
</LinearLayout>

View File

@ -123,6 +123,9 @@
<!-- Button color --> <!-- Button color -->
<color name="button_enabled_text_color">#FFFFFFFF</color> <color name="button_enabled_text_color">#FFFFFFFF</color>
<color name="button_disabled_text_color">#FFFFFFFF</color> <color name="button_disabled_text_color">#FFFFFFFF</color>
<color name="button_destructive_enabled_text_color">#FF4B55</color>
<color name="button_destructive_disabled_text_color">#FF4B55</color>
<!-- Link color --> <!-- Link color -->
<color name="link_color_light">#368BD6</color> <color name="link_color_light">#368BD6</color>

View File

@ -7,6 +7,17 @@
<color name="riotx_accent">#FF03B381</color> <color name="riotx_accent">#FF03B381</color>
<color name="riotx_accent_alpha25">#3F03B381</color> <color name="riotx_accent_alpha25">#3F03B381</color>
<color name="riotx_destructive_accent">#FFFF4B55</color>
<color name="riotx_destructive_accent_alpha12">#1EFF4B55</color>
<color name="riotx_positive_accent">#03B381</color>
<color name="riotx_disabled_accent">#61708B</color>
<color name="riotx_positive_accent_alpha12">#1E03B381</color>
<color name="riotx_button_disabled_alpha12">#1E61708B</color>
<color name="riotx_notice">#FFFF4B55</color> <color name="riotx_notice">#FFFF4B55</color>
<color name="riotx_notice_secondary">#FF61708B</color> <color name="riotx_notice_secondary">#FF61708B</color>
<color name="riotx_links">#FF368BD6</color> <color name="riotx_links">#FF368BD6</color>

View File

@ -142,4 +142,14 @@
<string name="seen_by">Seen by</string> <string name="seen_by">Seen by</string>
<string name="verification_request">Verification Request</string>
<string name="verification_sent">Verification Sent</string>
<string name="verification_request_you_accepted">You accepted</string>
<string name="verification_request_other_accepted">%s accepted</string>
<string name="verification_request_you_cancelled">You cancelled</string>
<string name="verification_request_other_cancelled">%s cancelled</string>
<string name="verification_request_waiting">Waiting…</string>
</resources> </resources>

View File

@ -134,6 +134,23 @@
<item name="android:textColor">@color/button_text_color_selector</item> <item name="android:textColor">@color/button_text_color_selector</item>
</style> </style>
<style name="VectorButtonStyleDestructive" parent="Widget.MaterialComponents.Button.UnelevatedButton">
<item name="backgroundTint">@color/button_destructive_background_selector</item>
<item name="android:paddingLeft">16dp</item>
<item name="android:paddingRight">16dp</item>
<item name="android:minWidth">94dp</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">14sp</item>
<item name="android:textAllCaps">false</item>
<item name="android:textColor">@color/button_destructive_text_color_selector</item>
</style>
<style name="VectorButtonStylePositive" parent="VectorButtonStyleDestructive">
<item name="backgroundTint">@color/button_positive_background_selector</item>
<item name="android:textColor">@color/button_positive_text_color_selector</item>
</style>
<!--Widget.AppCompat.Button.Borderless.Colored, which sets the text color to colorAccent, <!--Widget.AppCompat.Button.Borderless.Colored, which sets the text color to colorAccent,
using colorControlHighlight as an overlay for focused and pressed states.--> using colorControlHighlight as an overlay for focused and pressed states.-->
<style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton"> <style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton">