Merge pull request #1280 from vector-im/feature/e2e_timeline_decoration
Feature/e2e timeline decoration
This commit is contained in:
commit
8e357c6b7f
@ -28,6 +28,7 @@ Improvements 🙌:
|
||||
- Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
|
||||
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
|
||||
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
|
||||
- E2E timeline decoration (#1279)
|
||||
- Manage Session Settings / Cross Signing update (#1295)
|
||||
- Cross-Signing | Review sessions toast update old vs new (#1293, #1306)
|
||||
|
||||
|
@ -46,6 +46,7 @@ data class RoomSummary constructor(
|
||||
val readMarkerId: String? = null,
|
||||
val userDrafts: List<UserDraft> = emptyList(),
|
||||
val isEncrypted: Boolean,
|
||||
val encryptionEventTs: Long?,
|
||||
val inviterId: String? = null,
|
||||
val typingRoomMemberIds: List<String> = emptyList(),
|
||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.store.db.model
|
||||
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm
|
||||
import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm
|
||||
@ -36,7 +37,7 @@ internal open class OlmInboundGroupSessionEntity(
|
||||
: RealmObject() {
|
||||
|
||||
fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? {
|
||||
return deserializeFromRealm(olmInboundGroupSessionData)
|
||||
return tryThis { deserializeFromRealm<OlmInboundGroupSessionWrapper?>(olmInboundGroupSessionData) }
|
||||
}
|
||||
|
||||
fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) {
|
||||
|
@ -53,6 +53,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
||||
canonicalAlias = roomSummaryEntity.canonicalAlias,
|
||||
aliases = roomSummaryEntity.aliases.toList(),
|
||||
isEncrypted = roomSummaryEntity.isEncrypted,
|
||||
encryptionEventTs = roomSummaryEntity.encryptionEventTs,
|
||||
typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(),
|
||||
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
|
||||
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
|
||||
|
@ -48,6 +48,7 @@ internal open class RoomSummaryEntity(
|
||||
// this is required for querying
|
||||
var flatAliases: String = "",
|
||||
var isEncrypted: Boolean = false,
|
||||
var encryptionEventTs: Long? = 0,
|
||||
var typingUserIds: RealmList<String> = RealmList(),
|
||||
var roomEncryptionTrustLevelStr: String? = null,
|
||||
var inviterId: String? = null
|
||||
|
@ -136,6 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor(
|
||||
roomSummaryEntity.aliases.addAll(roomAliases)
|
||||
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
|
||||
roomSummaryEntity.isEncrypted = encryptionEvent != null
|
||||
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs
|
||||
roomSummaryEntity.typingUserIds.clear()
|
||||
roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty())
|
||||
|
||||
|
@ -26,7 +26,6 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.transition.AutoTransition
|
||||
@ -172,7 +171,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
||||
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
|
||||
else -> R.drawable.ic_shield_black
|
||||
}
|
||||
composerShieldImageView.setImageDrawable(ContextCompat.getDrawable(context, shieldRes))
|
||||
composerShieldImageView.setImageResource(shieldRes)
|
||||
} else {
|
||||
composerEditText.setHint(R.string.room_message_placeholder)
|
||||
composerShieldImageView.isVisible = false
|
||||
|
@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.dividerItem
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import im.vector.riotx.features.home.room.detail.timeline.tools.linkify
|
||||
import javax.inject.Inject
|
||||
@ -72,6 +73,29 @@ class MessageActionsEpoxyController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
when (state.informationData.e2eDecoration) {
|
||||
E2EDecoration.WARN_IN_CLEAR -> {
|
||||
bottomSheetSendStateItem {
|
||||
id("e2e_clear")
|
||||
showProgress(false)
|
||||
text(stringProvider.getString(R.string.unencrypted))
|
||||
drawableStart(R.drawable.ic_shield_warning_small)
|
||||
}
|
||||
}
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
bottomSheetSendStateItem {
|
||||
id("e2e_unverified")
|
||||
showProgress(false)
|
||||
text(stringProvider.getString(R.string.encrypted_unverified))
|
||||
drawableStart(R.drawable.ic_shield_warning_small)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
// Quick reactions
|
||||
if (state.canReact() && state.quickStates is Success) {
|
||||
// Separator
|
||||
|
@ -18,19 +18,23 @@
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
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.Room
|
||||
import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent
|
||||
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.api.session.room.timeline.getLastMessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
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.core.utils.getColorFromUserId
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData
|
||||
@ -72,6 +76,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId))
|
||||
}
|
||||
|
||||
val room = event.root.roomId?.let { session.getRoom(it) }
|
||||
val e2eDecoration = getE2EDecoration(room, event)
|
||||
return MessageInformationData(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
@ -111,10 +117,59 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
?: VerificationState.REQUEST
|
||||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = event.root.senderId == session.myUserId
|
||||
sentByMe = event.root.senderId == session.myUserId,
|
||||
e2eDecoration = e2eDecoration
|
||||
)
|
||||
}
|
||||
|
||||
private fun getE2EDecoration(room: Room?, event: TimelineEvent): E2EDecoration {
|
||||
return if (room?.isEncrypted() == true
|
||||
// is user verified
|
||||
&& session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) {
|
||||
val ts = room.roomSummary()?.encryptionEventTs ?: 0
|
||||
val eventTs = event.root.originServerTs ?: 0
|
||||
if (event.isEncrypted()) {
|
||||
// Do not decorate failed to decrypt, or redaction (we lost sender device info)
|
||||
if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) {
|
||||
E2EDecoration.NONE
|
||||
} else {
|
||||
val sendingDevice = event.root.content
|
||||
.toModel<EncryptedEventContent>()
|
||||
?.deviceId
|
||||
?.let { deviceId ->
|
||||
session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId)
|
||||
}
|
||||
when {
|
||||
sendingDevice == null -> {
|
||||
// For now do not decorate this with warning
|
||||
// maybe it's a deleted session
|
||||
E2EDecoration.NONE
|
||||
}
|
||||
sendingDevice.trustLevel == null -> {
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN
|
||||
}
|
||||
sendingDevice.trustLevel?.isVerified().orFalse() -> {
|
||||
E2EDecoration.NONE
|
||||
}
|
||||
else -> {
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (EventType.isStateEvent(event.root.type)) {
|
||||
// Do not warn for state event, they are always in clear
|
||||
E2EDecoration.NONE
|
||||
} else {
|
||||
// If event is in clear after the room enabled encryption we should warn
|
||||
if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE
|
||||
}
|
||||
}
|
||||
} else {
|
||||
E2EDecoration.NONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiles type message never show the sender information (like verification request), so we should repeat it for next message
|
||||
* even if same sender
|
||||
|
@ -92,6 +92,18 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
||||
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
}
|
||||
|
||||
when (baseAttributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.isVisible = false
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning)
|
||||
holder.e2EDecorationView.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
holder.view.setOnClickListener(baseAttributes.itemClickListener)
|
||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
}
|
||||
@ -110,6 +122,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
||||
|
||||
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
|
||||
val e2EDecorationView by bind<ImageView>(R.id.messageE2EDecoration)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -40,7 +40,8 @@ data class MessageInformationData(
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val referencesInfoData: ReferencesInfoData? = null,
|
||||
val sentByMe : Boolean
|
||||
val sentByMe : Boolean,
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
|
||||
) : Parcelable {
|
||||
|
||||
val matrixItem: MatrixItem
|
||||
@ -75,4 +76,11 @@ data class PollResponseData(
|
||||
val isClosed: Boolean = false
|
||||
) : Parcelable
|
||||
|
||||
enum class E2EDecoration {
|
||||
NONE,
|
||||
WARN_IN_CLEAR,
|
||||
WARN_SENT_BY_UNVERIFIED,
|
||||
WARN_SENT_BY_UNKNOWN
|
||||
}
|
||||
|
||||
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
|
@ -19,6 +19,7 @@ package im.vector.riotx.features.home.room.detail.timeline.item
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
@ -45,6 +46,18 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||
holder.avatarImageView.onClick(attributes.avatarClickListener)
|
||||
|
||||
when (attributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.isVisible = false
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.setImageResource(R.drawable.ic_shield_warning)
|
||||
holder.e2EDecorationView.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
@ -56,6 +69,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||
class Holder : BaseHolder(STUB_ID) {
|
||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
val e2EDecorationView by bind<ImageView>(R.id.messageE2EDecoration)
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
|
20
vector/src/main/res/drawable/ic_shield_warning_small.xml
Normal file
20
vector/src/main/res/drawable/ic_shield_warning_small.xml
Normal file
@ -0,0 +1,20 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="m12,21s9,-3.8 9,-9.5v-6.65l-9,-2.85 -9,2.85v6.65c0,5.7 9,9.5 9,9.5z"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#ff4b55"
|
||||
android:strokeColor="#fff"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12.05,5.5L12.05,5.5A1.25,1.25 0,0 1,13.3 6.75L13.3,12.25A1.25,1.25 0,0 1,12.05 13.5L12.05,13.5A1.25,1.25 0,0 1,10.8 12.25L10.8,6.75A1.25,1.25 0,0 1,12.05 5.5z"
|
||||
android:fillColor="#fff"/>
|
||||
<path
|
||||
android:pathData="M12.05,15L12.05,15A1.25,1.25 0,0 1,13.3 16.25L13.3,16.25A1.25,1.25 0,0 1,12.05 17.5L12.05,17.5A1.25,1.25 0,0 1,10.8 16.25L10.8,16.25A1.25,1.25 0,0 1,12.05 15z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
@ -62,6 +62,24 @@
|
||||
android:layout_height="0dp"
|
||||
tools:layout_marginStart="52dp" />
|
||||
|
||||
<Space
|
||||
android:id="@+id/decorationSpace"
|
||||
android:layout_width="4dp"
|
||||
android:layout_height="8dp"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageE2EDecoration"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignTop="@id/viewStubContainer"
|
||||
android:layout_marginTop="7dp"
|
||||
android:layout_alignEnd="@id/decorationSpace"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_shield_warning"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/viewStubContainer"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -63,6 +63,18 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageE2EDecoration"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_toStartOf="@id/viewStubContainer"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_shield_warning"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||
android:id="@+id/readReceiptsView"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -54,6 +54,18 @@
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageE2EDecoration"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_alignTop="@id/viewStubContainer"
|
||||
android:layout_toStartOf="@id/viewStubContainer"
|
||||
android:visibility="gone"
|
||||
tools:src="@drawable/ic_shield_warning"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/messageFailToSendIndicator"
|
||||
android:layout_width="14dp"
|
||||
|
@ -6,6 +6,22 @@
|
||||
<!-- Sections has been created to limit merge conflicts. -->
|
||||
|
||||
<!-- BEGIN Strings added by Valere -->
|
||||
<string name="use_other_session_content_description">Use the latest Riot on your other devices, Riot Web, Riot Desktop, Riot iOS, RiotX for Android, or another cross-signing capable Matrix client</string>
|
||||
<string name="riot_desktop_web">Riot Web\nRiot Desktop</string>
|
||||
<string name="riot_ios_android">Riot iOS\nRiot X for Android</string>
|
||||
<string name="or_other_mx_capabale_client">or another cross-signing capable Matrix client</string>
|
||||
<string name="use_latest_riot">Use the latest Riot on your other devices:</string>
|
||||
<string name="command_description_discard_session">Forces the current outbound group session in an encrypted room to be discarded</string>
|
||||
<string name="command_description_discard_session_not_handled">Only supported in encrypted rooms</string>
|
||||
<!-- first will be replaced by recovery_passphrase, second will be replaced by recovery_key-->
|
||||
<string name="enter_secret_storage_passphrase_or_key">Use your %1$s or use your %2$s to continue.</string>
|
||||
<string name="use_recovery_key">Use Recovery Key</string>
|
||||
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
|
||||
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
|
||||
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
|
||||
|
||||
<string name="unencrypted">Unencrypted</string>
|
||||
<string name="encrypted_unverified">Encrypted by an unverified device</string>
|
||||
<string name="review_logins">Review where you’re logged in</string>
|
||||
<string name="verify_other_sessions">Verify all your sessions to ensure your account & messages are safe</string>
|
||||
<!-- END Strings added by Valere -->
|
||||
|
Loading…
x
Reference in New Issue
Block a user