ui: new type for EventTimelineItem::get_shield

Separate the shield types between common and UI, so that we can change common
without breaking UI.

The new type does not include a `message` field: since it cannot be localised,
clients should not be using it.
This commit is contained in:
Richard van der Hoff
2025-12-18 13:14:38 +00:00
parent dbefaef777
commit d5ce01acab
7 changed files with 100 additions and 30 deletions

View File

@@ -21,7 +21,6 @@ use matrix_sdk::{
attachment::{
AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo, Thumbnail,
},
deserialized_responses::{ShieldState as SdkShieldState, ShieldStateCode},
event_cache::RoomPaginationStatus,
room::edit::EditedContent as SdkEditedContent,
};
@@ -33,6 +32,7 @@ use matrix_sdk_ui::timeline::{
self, AttachmentConfig, AttachmentSource, EventItemOrigin,
LatestEventValue as UiLatestEventValue, LatestEventValueLocalState,
MediaUploadProgress as SdkMediaUploadProgress, Profile, TimelineDetails,
TimelineEventShieldState as SdkShieldState, TimelineEventShieldStateCode,
TimelineUniqueId as SdkTimelineUniqueId,
};
use mime::Mime;
@@ -993,8 +993,8 @@ pub enum ShieldState {
impl From<SdkShieldState> for ShieldState {
fn from(value: SdkShieldState) -> Self {
match value {
SdkShieldState::Red { code, message: _ } => Self::Red { code },
SdkShieldState::Grey { code, message: _ } => Self::Grey { code },
SdkShieldState::Red { code } => Self::Red { code },
SdkShieldState::Grey { code } => Self::Grey { code },
SdkShieldState::None => Self::None,
}
}

View File

@@ -46,7 +46,6 @@ const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device.";
const MISMATCHED_SENDER: &str = "\
The sender of the event does not match the owner of the device \
that created the Megolm session.";
pub const SENT_IN_CLEAR: &str = "Not encrypted.";
/// Represents the state of verification for a decrypted message sent by a
/// device.

View File

@@ -18,6 +18,10 @@ All notable changes to this project will be documented in this file.
### Features
- [**breaking**] `EventTimelineItem::get_shield` now returns a new type,
`TimelineEventShieldState`, which extends the old `ShieldState` with a code
for `SentInClear`, now that the latter has been removed from `ShieldState`.
([#5959](https://github.com/matrix-org/matrix-rust-sdk/pull/5959))
- Add `SpaceService::get_space_room` to get a space
given its id from the space graph if available.
([#5944](https://github.com/matrix-org/matrix-rust-sdk/pull/5944))

View File

@@ -24,7 +24,7 @@ use matrix_sdk::{
deserialized_responses::{EncryptionInfo, ShieldState},
send_queue::{SendHandle, SendReactionHandle},
};
use matrix_sdk_base::deserialized_responses::{SENT_IN_CLEAR, ShieldStateCode};
use matrix_sdk_base::deserialized_responses::ShieldStateCode;
use once_cell::sync::Lazy;
use ruma::{
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
@@ -309,16 +309,16 @@ impl EventTimelineItem {
}
}
/// Gets the [`ShieldState`] which can be used to decorate
/// Gets the [`TimelineEventShieldState`] which can be used to decorate
/// messages in the recommended way.
pub fn get_shield(&self, strict: bool) -> ShieldState {
pub fn get_shield(&self, strict: bool) -> TimelineEventShieldState {
if !self.is_room_encrypted || self.is_local_echo() {
return ShieldState::None;
return TimelineEventShieldState::None;
}
// An unable-to-decrypt message has no authenticity shield.
if self.content().is_unable_to_decrypt() {
return ShieldState::None;
return TimelineEventShieldState::None;
}
match self.encryption_info() {
@@ -329,10 +329,9 @@ impl EventTimelineItem {
info.verification_state.to_shield_state_lax().into()
}
}
None => ShieldState::Red {
code: ShieldStateCode::SentInClear,
message: SENT_IN_CLEAR,
},
None => {
TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
}
}
}
@@ -693,3 +692,72 @@ impl ReactionsByKeyBySender {
None
}
}
/// Extends [`ShieldState`] to allow for a `SentInClear` code.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimelineEventShieldState {
/// A red shield with a tooltip containing a message appropriate to the
/// associated code should be presented.
Red {
/// A machine-readable representation.
code: TimelineEventShieldStateCode,
},
/// A grey shield with a tooltip containing a message appropriate to the
/// associated code should be presented.
Grey {
/// A machine-readable representation.
code: TimelineEventShieldStateCode,
},
/// No shield should be presented.
None,
}
impl From<ShieldState> for TimelineEventShieldState {
fn from(value: ShieldState) -> Self {
match value {
ShieldState::Red { code, message: _ } => {
TimelineEventShieldState::Red { code: code.into() }
}
ShieldState::Grey { code, message: _ } => {
TimelineEventShieldState::Grey { code: code.into() }
}
ShieldState::None => TimelineEventShieldState::None,
}
}
}
/// Extends [`ShieldStateCode`] to allow for a `SentInClear` code.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum TimelineEventShieldStateCode {
/// Not enough information available to check the authenticity.
AuthenticityNotGuaranteed,
/// The sending device isn't yet known by the Client.
UnknownDevice,
/// The sending device hasn't been verified by the sender.
UnsignedDevice,
/// The sender hasn't been verified by the Client's user.
UnverifiedIdentity,
/// The sender was previously verified but changed their identity.
VerificationViolation,
/// The `sender` field on the event does not match the owner of the device
/// that established the Megolm session.
MismatchedSender,
/// An unencrypted event in an encrypted room.
SentInClear,
}
impl From<ShieldStateCode> for TimelineEventShieldStateCode {
fn from(value: ShieldStateCode) -> Self {
use TimelineEventShieldStateCode::*;
match value {
ShieldStateCode::AuthenticityNotGuaranteed => AuthenticityNotGuaranteed,
ShieldStateCode::UnknownDevice => UnknownDevice,
ShieldStateCode::UnsignedDevice => UnsignedDevice,
ShieldStateCode::UnverifiedIdentity => UnverifiedIdentity,
ShieldStateCode::SentInClear => SentInClear,
ShieldStateCode::VerificationViolation => VerificationViolation,
ShieldStateCode::MismatchedSender => MismatchedSender,
}
}
}

View File

@@ -97,7 +97,8 @@ pub use self::{
MemberProfileChange, MembershipChange, Message, MsgLikeContent, MsgLikeKind,
OtherMessageLike, OtherState, PollResult, PollState, Profile, ReactionInfo, ReactionStatus,
ReactionsByKeyBySender, RoomMembershipChange, RoomPinnedEventsChange, Sticker,
ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineItemContent,
ThreadSummary, TimelineDetails, TimelineEventItemId, TimelineEventShieldState,
TimelineEventShieldStateCode, TimelineItemContent,
},
event_type_filter::TimelineEventTypeFilter,
item::{TimelineItem, TimelineItemKind, TimelineUniqueId},

View File

@@ -1,6 +1,5 @@
use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use matrix_sdk_base::deserialized_responses::{ShieldState, ShieldStateCode};
use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
use ruma::{
event_id,
@@ -17,7 +16,7 @@ use ruma::{
use stream_assert::{assert_next_matches, assert_pending};
use crate::timeline::{
EventSendState,
EventSendState, TimelineEventShieldState, TimelineEventShieldStateCode,
tests::{TestTimeline, TestTimelineBuilder},
};
@@ -31,7 +30,7 @@ async fn test_no_shield_in_unencrypted_room() {
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let shield = item.as_event().unwrap().get_shield(false);
assert_eq!(shield, ShieldState::None);
assert_eq!(shield, TimelineEventShieldState::None);
}
#[async_test]
@@ -46,7 +45,7 @@ async fn test_sent_in_clear_shield() {
let shield = item.as_event().unwrap().get_shield(false);
assert_eq!(
shield,
ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }
TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
);
}
@@ -75,7 +74,7 @@ async fn test_local_sent_in_clear_shield() {
// available).
assert!(event_item.is_local_echo());
let shield = event_item.get_shield(false);
assert_eq!(shield, ShieldState::None);
assert_eq!(shield, TimelineEventShieldState::None);
{
// The date divider comes in late.
@@ -96,7 +95,7 @@ async fn test_local_sent_in_clear_shield() {
// Then the local echo still should not have a shield.
assert!(event_item.is_local_echo());
let shield = event_item.get_shield(false);
assert_eq!(shield, ShieldState::None);
assert_eq!(shield, TimelineEventShieldState::None);
// When the remote echo comes in.
timeline
@@ -118,7 +117,7 @@ async fn test_local_sent_in_clear_shield() {
let shield = event_item.get_shield(false);
assert_eq!(
shield,
ShieldState::Red { code: ShieldStateCode::SentInClear, message: "Not encrypted." }
TimelineEventShieldState::Red { code: TimelineEventShieldStateCode::SentInClear }
);
// Date divider is adjusted.
@@ -168,5 +167,5 @@ async fn test_utd_shield() {
// Then the message is displayed with no shield
let item = assert_next_matches!(stream, VectorDiff::PushBack { value } => value);
let shield = item.as_event().unwrap().get_shield(false);
assert_eq!(shield, ShieldState::None);
assert_eq!(shield, TimelineEventShieldState::None);
}

View File

@@ -22,7 +22,6 @@ use matrix_sdk::{
linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update},
test_utils::mocks::{MatrixMockServer, RoomContextResponseTemplate},
};
use matrix_sdk_common::deserialized_responses::ShieldState;
use matrix_sdk_test::{
ALICE, BOB, JoinedRoomBuilder, RoomAccountDataTestEvent, StateTestEvent, async_test,
event_factory::EventFactory,
@@ -31,8 +30,8 @@ use matrix_sdk_ui::{
Timeline,
timeline::{
AnyOtherFullStateEventContent, Error, EventSendState, MsgLikeKind, OtherMessageLike,
RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineFocus,
TimelineItemContent, VirtualTimelineItem, default_event_filter,
RedactError, RoomExt, TimelineBuilder, TimelineEventItemId, TimelineEventShieldState,
TimelineFocus, TimelineItemContent, VirtualTimelineItem, default_event_filter,
},
};
use ruma::{
@@ -758,7 +757,7 @@ async fn test_timeline_without_encryption_info() {
assert_eq!(items.len(), 2);
assert!(items[0].as_virtual().is_some());
// No encryption, no shields.
assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None);
assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None);
}
#[async_test]
@@ -788,7 +787,7 @@ async fn test_timeline_without_encryption_can_update() {
assert_eq!(items.len(), 2);
assert!(items[0].as_virtual().is_some());
// No encryption, no shields
assert_eq!(items[1].as_event().unwrap().get_shield(false), ShieldState::None);
assert_eq!(items[1].as_event().unwrap().get_shield(false), TimelineEventShieldState::None);
let encryption_event_content = RoomEncryptionEventContent::with_recommended_defaults();
server
@@ -806,17 +805,17 @@ async fn test_timeline_without_encryption_can_update() {
// Previous timeline event now has a shield.
assert_let!(VectorDiff::Set { index, value } = &timeline_updates[0]);
assert_eq!(*index, 1);
assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None);
assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None);
// Room encryption event is received.
assert_let!(VectorDiff::PushBack { value } = &timeline_updates[1]);
assert_let!(TimelineItemContent::OtherState(other_state) = value.as_event().unwrap().content());
assert_let!(AnyOtherFullStateEventContent::RoomEncryption(_) = other_state.content());
assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None);
assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None);
// New message event is received and has a shield.
assert_let!(VectorDiff::PushBack { value } = &timeline_updates[2]);
assert_ne!(value.as_event().unwrap().get_shield(false), ShieldState::None);
assert_ne!(value.as_event().unwrap().get_shield(false), TimelineEventShieldState::None);
assert_pending!(stream);
}