E2e lab error display

This commit is contained in:
Valere 2020-05-19 09:44:34 +02:00 committed by Benoit Marty
parent f9d931960b
commit 36de891451
16 changed files with 393 additions and 45 deletions

View file

@ -75,6 +75,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
data class RequestVerification(val userId: String) : RoomDetailAction() data class RequestVerification(val userId: String) : RoomDetailAction()
data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction() data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction()
data class TapOnFailedToDecrypt(val eventId: String) : RoomDetailAction()
data class ReRequestKeys(val eventId: String) : RoomDetailAction() data class ReRequestKeys(val eventId: String) : RoomDetailAction()
object SelectStickerAttachment : RoomDetailAction() object SelectStickerAttachment : RoomDetailAction()

View file

@ -86,6 +86,8 @@ import im.vector.matrix.android.api.session.widgets.model.WidgetType
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder import im.vector.riotx.core.dialogs.ConfirmationDialogBuilder
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
@ -340,6 +342,7 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it)
is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it)
is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it)
is RoomDetailViewEvents.ShowE2EErrorMessage -> displayE2eError(it.withHeldCode)
RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager() RoomDetailViewEvents.DisplayPromptForIntegrationManager -> displayPromptForIntegrationManager()
is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it) is RoomDetailViewEvents.OpenStickerPicker -> openStickerPicker(it)
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
@ -941,6 +944,20 @@ class RoomDetailFragment @Inject constructor(
.show() .show()
} }
private fun displayE2eError(withHeldCode: WithHeldCode?) {
val msgId = when (withHeldCode) {
WithHeldCode.BLACKLISTED -> R.string.crypto_error_withheld_blacklisted
WithHeldCode.UNVERIFIED -> R.string.crypto_error_withheld_unverified
WithHeldCode.UNAUTHORISED,
WithHeldCode.UNAVAILABLE -> R.string.crypto_error_withheld_generic
else -> R.string.notice_crypto_unable_to_decrypt_friendly_desc
}
AlertDialog.Builder(requireActivity())
.setMessage(msgId)
.setPositiveButton(R.string.ok, null)
.show()
}
private fun promptReasonToReportContent(action: EventSharedAction.ReportContentCustom) { private fun promptReasonToReportContent(action: EventSharedAction.ReportContentCustom) {
val inflater = requireActivity().layoutInflater val inflater = requireActivity().layoutInflater
val layout = inflater.inflate(R.layout.dialog_report_content, null) val layout = inflater.inflate(R.layout.dialog_report_content, null)
@ -1205,13 +1222,18 @@ class RoomDetailFragment @Inject constructor(
roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction)) roomDetailViewModel.handle(RoomDetailAction.LoadMoreTimelineEvents(direction))
} }
override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { override fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View) {
if (messageContent is MessageVerificationRequestContent) { when (messageContent) {
roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) is MessageVerificationRequestContent -> {
roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null))
}
is EncryptedEventContent -> {
roomDetailViewModel.handle(RoomDetailAction.TapOnFailedToDecrypt(informationData.eventId))
}
} }
} }
override fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean { override fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean {
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val roomId = roomDetailArgs.roomId val roomId = roomDetailArgs.roomId

View file

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.riotx.core.platform.VectorViewEvents import im.vector.riotx.core.platform.VectorViewEvents
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import java.io.File import java.io.File
@ -33,6 +34,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents() data class ActionFailure(val action: RoomDetailAction, val throwable: Throwable) : RoomDetailViewEvents()
data class ShowMessage(val message: String) : RoomDetailViewEvents() data class ShowMessage(val message: String) : RoomDetailViewEvents()
data class ShowE2EErrorMessage(val withHeldCode: WithHeldCode?) : RoomDetailViewEvents()
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents() data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.NoOpMatrixCallback import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.query.QueryStringValue import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.isTextMessage
@ -59,6 +60,8 @@ import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.asObservable
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
@ -259,6 +262,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.RequestVerification -> handleRequestVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
@ -1034,6 +1038,19 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleTapOnFailedToDecrypt(action: RoomDetailAction.TapOnFailedToDecrypt) {
room.getTimeLineEvent(action.eventId)?.let {
val code = when (it.root.mCryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> {
WithHeldCode.fromCode(it.root.mCryptoErrorReason)
}
else -> null
}
_viewEvents.post(RoomDetailViewEvents.ShowE2EErrorMessage(code))
}
}
private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) { private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) {
room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue) room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue)
} }

View file

@ -26,7 +26,6 @@ import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
@ -89,8 +88,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
interface BaseCallback { interface BaseCallback {
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) fun onEventCellClicked(informationData: MessageInformationData, messageContent: Any?, view: View)
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View): Boolean fun onEventLongClicked(informationData: MessageInformationData, messageContent: Any?, view: View): Boolean
} }
interface AvatarCallback { interface AvatarCallback {
@ -283,13 +282,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun getModels(): List<EpoxyModel<*>> { private fun getModels(): List<EpoxyModel<*>> {
buildCacheItemsIfNeeded() buildCacheItemsIfNeeded()
return modelCache return modelCache
.map { .map { cacheItemData ->
val eventModel = if (it == null || mergedHeaderItemFactory.isCollapsed(it.localId)) { val eventModel = if (cacheItemData == null || mergedHeaderItemFactory.isCollapsed(cacheItemData.localId)) {
null null
} else { } else {
it.eventModel cacheItemData.eventModel
} }
listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel) listOf(eventModel,
cacheItemData?.mergedHeaderModel,
cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null }
)
} }
.flatten() .flatten()
.filterNotNull() .filterNotNull()

View file

@ -18,6 +18,9 @@ package im.vector.riotx.features.home.room.detail.timeline.action
import android.view.View import android.view.View
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem import im.vector.riotx.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem

View file

@ -15,6 +15,8 @@
*/ */
package im.vector.riotx.features.home.room.detail.timeline.action package im.vector.riotx.features.home.room.detail.timeline.action
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext

View file

@ -18,8 +18,9 @@ package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.resources.ColorProvider
@ -31,6 +32,7 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.image import me.gujun.android.span.image
import me.gujun.android.span.span import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
@ -41,7 +43,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val drawableProvider: DrawableProvider, private val drawableProvider: DrawableProvider,
private val attributesFactory: MessageItemAttributesFactory) { private val attributesFactory: MessageItemAttributesFactory,
private val vectorPreferences: VectorPreferences) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -52,29 +55,18 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
return when { return when {
EventType.ENCRYPTED == event.root.getClearType() -> { EventType.ENCRYPTED == event.root.getClearType() -> {
val cryptoError = event.root.mCryptoError val cryptoError = event.root.mCryptoError
// val errorDescription =
// if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
// stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
// } else {
// // TODO i18n
// cryptoError?.name
// }
val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary) val spannableStr = if (vectorPreferences.hideE2ETechnicalErrors()) {
val spannableStr = if(cryptoError == null) {
val colorFromAttribute = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
if (cryptoError == null) {
span(stringProvider.getString(R.string.encrypted_message)) { span(stringProvider.getString(R.string.encrypted_message)) {
textStyle = "italic" textStyle = "italic"
textColor = colorFromAttribute textColor = colorFromAttribute
} }
} else { } else {
when(cryptoError) { when (cryptoError) {
MXCryptoError.ErrorType.KEYS_WITHHELD -> { MXCryptoError.ErrorType.KEYS_WITHHELD -> {
// val why = when (event.root.mCryptoErrorReason) {
// WithHeldCode.BLACKLISTED.value -> stringProvider.getString(R.string.crypto_error_withheld_blacklisted)
// WithHeldCode.UNVERIFIED.value -> stringProvider.getString(R.string.crypto_error_withheld_unverified)
// else -> stringProvider.getString(R.string.crypto_error_withheld_generic)
// }
//stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, why)
span { span {
apply { apply {
drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let { drawableProvider.getDrawable(R.drawable.ic_forbidden, colorFromAttribute)?.let {
@ -87,7 +79,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
} }
} }
} }
else -> { else -> {
span { span {
apply { apply {
drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let { drawableProvider.getDrawable(R.drawable.ic_clock, colorFromAttribute)?.let {
@ -99,20 +91,29 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
textColor = colorFromAttribute textColor = colorFromAttribute
} }
} }
} }
} }
}
} else {
val errorDescription =
if (cryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id)
} else {
// TODO i18n
cryptoError?.name
}
val message = stringProvider.getString(R.string.encrypted_message).takeIf { cryptoError == null }
?: stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription)
span(message) {
textStyle = "italic"
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
}
} }
// val spannableStr = span(message) {
// textStyle = "italic"
// textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
// }
// TODO This is not correct format for error, change it
val informationData = messageInformationDataFactory.create(event, nextEvent) val informationData = messageInformationDataFactory.create(event, nextEvent)
val attributes = attributesFactory.create(null, informationData, callback) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, callback)
return MessageTextItem_() return MessageTextItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(highlight) .highlighted(highlight)

View file

@ -16,6 +16,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
@ -36,11 +37,15 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipE
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedUTDItem_
import im.vector.riotx.features.settings.VectorPreferences
import javax.inject.Inject import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer, class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val activeSessionHolder: ActiveSessionHolder) { private val vectorPreferences: VectorPreferences) {
private val collapsedEventIds = linkedSetOf<Long>() private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>() private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -58,7 +63,9 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit) requestModelBuild: () -> Unit)
: BasedMergedItem<*>? { : BasedMergedItem<*>? {
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE return if (shouldMergedAsCannotDecryptGroup(event, nextEvent)) {
buildUTDMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
} else if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) { && event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
// It's the first item before room.create // It's the first item before room.create
// Collapse all room configuration events // Collapse all room configuration events
@ -130,6 +137,89 @@ class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: Av
} }
} }
// Event should be UTD
// Next event should not
private fun shouldMergedAsCannotDecryptGroup(event: TimelineEvent, nextEvent: TimelineEvent?) : Boolean {
if (!vectorPreferences.hideE2ETechnicalErrors()) return false
// if event is not UTD return false
if (event.root.getClearType() != EventType.ENCRYPTED || event.root.mCryptoError != MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) return false
// At this point event cannot be decrypted
// Let's check if older event is not UTD
return nextEvent == null || nextEvent.root.getClearType() != EventType.ENCRYPTED
}
private fun buildUTDMergedSummary(currentPosition: Int,
items: List<TimelineEvent>,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedUTDItem_? {
var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null
var tmpPos = currentPosition - 1
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
while (prevEvent != null
&& prevEvent.root.getClearType() == EventType.ENCRYPTED
&& prevEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
mergedEvents.add(prevEvent)
tmpPos--
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
}
if (mergedEvents.size < 3) return null
var highlighted = false
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.reversed()
.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
)
mergedData.add(data)
}
val mergedEventIds = mergedEvents.map { it.localId }
// We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
} else {
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedUTDItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}
)
return MergedUTDItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
}
private fun buildRoomCreationMergedSummary(currentPosition: Int, private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List<TimelineEvent>, items: List<TimelineEvent>,
event: TimelineEvent, event: TimelineEvent,

View file

@ -18,7 +18,6 @@
package im.vector.riotx.features.home.room.detail.timeline.helper package im.vector.riotx.features.home.room.detail.timeline.helper
import android.view.View import android.view.View
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -34,7 +33,7 @@ class MessageItemAttributesFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val emojiCompatFontProvider: EmojiCompatFontProvider) { private val emojiCompatFontProvider: EmojiCompatFontProvider) {
fun create(messageContent: MessageContent?, fun create(messageContent: Any?,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): AbsMessageItem.Attributes { callback: TimelineEventController.Callback?): AbsMessageItem.Attributes {
return AbsMessageItem.Attributes( return AbsMessageItem.Attributes(

View file

@ -0,0 +1,112 @@
/*
* Copyright (c) 2020 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.ViewGroup
import android.widget.RelativeLayout
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
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_noinfo)
abstract class MergedUTDItem : BasedMergedItem<MergedUTDItem.Holder>() {
@EpoxyAttribute
override lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
holder.mergedTile.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
// if (attributes.isCollapsed) {
// // Take the oldest data
// val data = distinctMergeData.lastOrNull()
//
// val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
// data?.memberName ?: data?.userId ?: "")
// holder.summaryView.text = summary
// holder.summaryView.visibility = View.VISIBLE
// holder.avatarView.visibility = View.VISIBLE
// if (data != null) {
// holder.avatarView.visibility = View.VISIBLE
// attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
// } else {
// holder.avatarView.visibility = View.GONE
// }
//
// if (attributes.hasEncryptionEvent) {
// holder.encryptionTile.isVisible = true
// holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
// this.marginEnd = leftGuideline
// }
// if (attributes.isEncryptionAlgorithmSecure) {
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
// holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
// null, null, null
// )
// } else {
// holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
// holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
// holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
// ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
// null, null, null
// )
// }
// } else {
// holder.encryptionTile.isVisible = false
// }
// } else {
// holder.avatarView.visibility = View.INVISIBLE
// holder.summaryView.visibility = View.GONE
// holder.encryptionTile.isGone = true
// }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
// val summaryView by bind<TextView>(R.id.itemNoticeTextView)
// val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
val mergedTile by bind<ViewGroup>(R.id.mergedUTDTile)
//
// val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
// val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
}
companion object {
private const val STUB_ID = R.id.messageContentMergedUTDStub
}
data class Attributes(
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes
}

View file

@ -144,6 +144,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY" private const val SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY = "SETTINGS_LABS_SHOW_HIDDEN_EVENTS_PREFERENCE_KEY"
private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY" private const val SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY = "SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY"
private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY" private const val SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY = "SETTINGS_DEVELOPER_MODE_FAIL_FAST_PREFERENCE_KEY"
private const val SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS = "SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS"
// analytics // analytics
const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY" const val SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY"
@ -265,6 +266,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true) return defaultPrefs.getBoolean(SETTINGS_LABS_ENABLE_SWIPE_TO_REPLY, true)
} }
fun hideE2ETechnicalErrors(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS, true)
}
fun labAllowedExtendedLogging(): Boolean { fun labAllowedExtendedLogging(): Boolean {
return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false) return developerMode() && defaultPrefs.getBoolean(SETTINGS_LABS_ALLOW_EXTENDED_LOGS, false)
} }

View file

@ -61,6 +61,13 @@
tools:layout_marginTop="240dp" tools:layout_marginTop="240dp"
tools:visibility="visible" /> tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMergedUTDStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_merged_utd_stub"
tools:layout_marginTop="240dp"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
<ImageView <ImageView

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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">
<LinearLayout
android:id="@+id/mergedUTDTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:orientation="vertical"
android:layout_gravity="center"
android:layout_marginTop="2dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="2dp"
android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp">
<TextView
android:id="@+id/itemVerificationDoneTitleTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="8dp"
android:drawableStart="@drawable/ic_clock"
android:drawableTint="?riotx_text_secondary"
android:drawablePadding="2dp"
android:gravity="start"
android:textColor="?riotx_text_secondary"
android:textSize="15sp"
android:text="@string/notice_crypto_unable_to_decrypt_merged" />
<TextView
android:id="@+id/itemVerificationDoneDetailTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
android:textStyle="italic"
android:text="@string/notice_crypto_unable_to_decrypt_friendly_desc" />
<TextView
android:id="@+id/itemMergedExpandTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="2dp"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:visibility="gone"
android:text="@string/merged_events_expand"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:textStyle="italic" />
</LinearLayout>
<View
android:id="@+id/itemMergedSeparatorView"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/mergedUTDTile"
android:layout_marginTop="4dp"
android:background="?attr/riotx_header_panel_background" />
</RelativeLayout>

View file

@ -1734,6 +1734,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string> <string name="room_directory_search_hint">Name or ID (#example:matrix.org)</string>
<string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string> <string name="labs_swipe_to_reply_in_timeline">Enable swipe to reply in timeline</string>
<string name="labs_hide_technical_e2e_in_timeline">Hide failed to decrypt technical details in timeline</string>
<string name="link_copied_to_clipboard">Link copied to clipboard</string> <string name="link_copied_to_clipboard">Link copied to clipboard</string>
@ -2503,7 +2504,10 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string> <string name="notice_crypto_unable_to_decrypt_final">You cannot access this message</string>
<string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string> <string name="notice_crypto_unable_to_decrypt_friendly">Waiting for this message, this may take a while</string>
<string name="crypto_error_withheld_blacklisted">You have been blocked</string> <string name="crypto_utd">Cannot Decrypt</string>
<string name="crypto_error_withheld_unverified">Session not trusted by sender</string> <string name="notice_crypto_unable_to_decrypt_friendly_desc">Due to end-to-end encryption, you might need to wait for someone\'s message to arrive because the encryption keys were not properly sent to you.</string>
<string name="crypto_error_withheld_generic">Sender purposely did not send the keys</string> <string name="crypto_error_withheld_blacklisted">You cannot access this message because you have been blocked by the sender</string>
<string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string>
<string name="crypto_error_withheld_generic">You cannot access this message because the sender purposely did not send the keys</string>
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
</resources> </resources>

View file

@ -40,6 +40,10 @@
android:title="@string/labs_swipe_to_reply_in_timeline" /> android:title="@string/labs_swipe_to_reply_in_timeline" />
<im.vector.riotx.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_LABS_HIDE_TECHNICAL_E2E_ERRORS"
android:title="@string/labs_hide_technical_e2e_in_timeline" />
<!--</im.vector.riotx.core.preference.VectorPreferenceCategory>--> <!--</im.vector.riotx.core.preference.VectorPreferenceCategory>-->
</androidx.preference.PreferenceScreen> </androidx.preference.PreferenceScreen>