diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt index fce166c37a..ca9d81cba1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/ReferencesAggregatedSummary.kt @@ -27,4 +27,3 @@ class ReferencesAggregatedSummary( val sourceEvents: List, val localEchos: List ) - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt index 9c7547b5e1..88f127066d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/ReferencesAggregatedSummaryEntityQueries.kt @@ -17,6 +17,3 @@ internal fun ReferencesAggregatedSummaryEntity.Companion.create(realm: Realm, tx this.eventId = txID } } - - - diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index c1743ae3fc..5d00b09204 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 80f54a9c1f..9cc8eabe58 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index c1d3f4ce4a..3ce27be63a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index 576b9fa0ba..fe1a681480 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 102412948b..e8047c2b06 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -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 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 3f234fcd3e..29b01120d1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 9c96f17022..93d5ab3789 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -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?, @@ -97,14 +99,15 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel() return when (messageContent) { - is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) - is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) + is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) + is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) + 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, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index a705576234..3b0c2c2bb7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt new file mode 100644 index 0000000000..56284b6777 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -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() == 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 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt index 75100e6c03..f5253a9a28 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/format/NoticeEventFormatter.kt @@ -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") diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 784a180d00..5e29c8db67 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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()?.verificationSummary + ReferencesInfoData( + VerificationState.values().firstOrNull { stateStr == it.name } + ?: VerificationState.REQUEST + ) + }, + sentByMe = event.root.senderId == session.myUserId ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt new file mode 100644 index 0000000000..6d99bb2650 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -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 : BaseEventItem() { + + 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 { + 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(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 +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt index 713b60d4d8..7d8fe3a10e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -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 : BaseEventItem() { +/** + * 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 : AbsBaseMessageItem() { + + override val baseAttributes: AbsBaseMessageItem.Attributes + get() = attributes @EpoxyAttribute lateinit var attributes: Attributes @@ -45,24 +47,6 @@ abstract class AbsMessageItem : BaseEventItem() { 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 : BaseEventItem() { 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 { - 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(R.id.messageAvatarImageView) val memberNameView by bind(R.id.messageMemberNameView) val timeView by bind(R.id.messageTimeView) - val reactionsContainer by bind(R.id.reactionsContainer) } /** @@ -155,15 +91,15 @@ abstract class AbsMessageItem : BaseEventItem() { */ 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 } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 2dd581ce6f..5c0b521106 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -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? = null, val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList() + val readReceipts: List = emptyList(), + val referencesInfoData: ReferencesInfoData? = null, + val sentByMe : Boolean +) : Parcelable + +@Parcelize +data class ReferencesInfoData( + val verificationStatus: VerificationState ) : Parcelable @Parcelize diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt new file mode 100644 index 0000000000..bddef2c130 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt @@ -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() { + + 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 { + 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(R.id.itemVerificationDoneTitleTextView) + val descriptionView by bind(R.id.itemVerificationDoneDetailTextView) + val endGuideline by bind(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 +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt new file mode 100644 index 0000000000..1a0e89fc32 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -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() { + + 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 { + 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(R.id.itemVerificationTitleTextView) + val descriptionView by bind(R.id.itemVerificationDetailTextView) + val buttonBar by bind(R.id.itemVerificationButtonBar) + val statusTextView by bind(R.id.itemVerificationStatusText) + val endGuideline by bind(R.id.messageEndGuideline) + private val declineButton by bind