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 localEchos: List<String>
)

View File

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

View File

@ -63,4 +63,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ClearSendQueue : 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")
}
override fun onTimelineItemAction(itemAction: RoomDetailAction) {
roomDetailViewModel.handle(itemAction)
}
override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
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.internal.crypto.attachments.toElementToDecrypt
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.unwrap
import im.vector.riotx.R
@ -177,6 +178,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
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() {
session.rx()
.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.epoxy.LoadingItem_
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.UnreadState
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 onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData)
// TODO move all callbacks to this?
fun onTimelineItemAction(itemAction: RoomDetailAction)
}
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.isTextMessage
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.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.model.message.*
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.getLastMessageContent
@ -172,6 +169,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body)
} else if (messageContent is MessageVerificationRequestContent) {
stringProvider.getString(R.string.verification_request)
} else {
messageContent?.body
}

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
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.EventType
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,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider) {
private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent,
highlight: Boolean,
@ -46,7 +48,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
sendState = event.root.sendState,
avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(),
showInformation = false
showInformation = false,
sentByMe = event.root.senderId == session.myUserId
)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,

View File

@ -24,6 +24,7 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.view.View
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.toModel
import im.vector.matrix.android.api.session.room.model.message.*
@ -64,7 +65,8 @@ class MessageItemFactory @Inject constructor(
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider) {
private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
@ -104,6 +106,7 @@ class MessageItemFactory @Inject constructor(
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(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)
}
}
@ -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,
informationData: MessageInformationData,
highlight: Boolean,
@ -193,7 +241,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl,
url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height,
maxHeight = maxHeight,

View File

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

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.MESSAGE,
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)
else -> {
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.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.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.resources.ColorProvider
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.ReactionInfoData
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 javax.inject.Inject
@ -86,7 +90,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
.map {
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.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
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
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
lateinit var attributes: Attributes
@ -45,24 +47,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
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) {
super.bind(holder)
if (attributes.informationData.showInformation) {
@ -94,60 +78,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.avatarImageView.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) {
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) {
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
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(
val avatarSize: Int,
val informationData: MessageInformationData,
val avatarRenderer: AvatarRenderer,
val colorProvider: ColorProvider,
val itemLongClickListener: View.OnLongClickListener? = null,
val itemClickListener: View.OnClickListener? = null,
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,
val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
override val reactionPillCallback: TimelineEventController.ReactionPillCallback? = null,
val avatarCallback: TimelineEventController.AvatarCallback? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = 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 im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.internal.session.room.VerificationState
import kotlinx.android.parcel.Parcelize
@Parcelize
@ -33,7 +34,14 @@ data class MessageInformationData(
val orderedReactionList: List<ReactionInfoData>? = null,
val hasBeenEdited: 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
@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 -->
<color name="button_enabled_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 -->
<color name="link_color_light">#368BD6</color>

View File

@ -7,6 +7,17 @@
<color name="riotx_accent">#FF03B381</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_secondary">#FF61708B</color>
<color name="riotx_links">#FF368BD6</color>

View File

@ -142,4 +142,14 @@
<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>

View File

@ -134,6 +134,23 @@
<item name="android:textColor">@color/button_text_color_selector</item>
</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,
using colorControlHighlight as an overlay for focused and pressed states.-->
<style name="VectorButtonStyleText" parent="Widget.MaterialComponents.Button.TextButton">