use std::sync::Arc;
use as_variant::as_variant;
use indexmap::IndexMap;
use matrix_sdk::{
deserialized_responses::{EncryptionInfo, ShieldState},
send_queue::SendHandle,
Client, Error,
};
use matrix_sdk_base::{deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent};
use once_cell::sync::Lazy;
use ruma::{
events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
serde::Raw,
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedTransactionId,
OwnedUserId, RoomId, RoomVersionId, TransactionId, UserId,
};
use tracing::warn;
mod content;
mod local;
mod remote;
pub use self::{
content::{
AnyOtherFullStateEventContent, EncryptedMessage, InReplyToDetails, MemberProfileChange,
MembershipChange, Message, OtherState, RepliedToEvent, RoomMembershipChange, Sticker,
TimelineItemContent,
},
local::EventSendState,
};
pub(super) use self::{
local::LocalEventTimelineItem,
remote::{RemoteEventOrigin, RemoteEventTimelineItem},
};
use super::{RepliedToInfo, ReplyContent, UnsupportedReplyItem};
#[derive(Clone, Debug)]
pub struct EventTimelineItem {
pub(super) sender: OwnedUserId,
pub(super) sender_profile: TimelineDetails<Profile>,
pub(super) timestamp: MilliSecondsSinceUnixEpoch,
pub(super) content: TimelineItemContent,
pub(super) kind: EventTimelineItemKind,
}
#[derive(Clone, Debug)]
pub(super) enum EventTimelineItemKind {
Local(LocalEventTimelineItem),
Remote(RemoteEventTimelineItem),
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum TimelineEventItemId {
TransactionId(OwnedTransactionId),
EventId(OwnedEventId),
}
pub(crate) enum TimelineItemHandle<'a> {
Remote(&'a EventId),
Local(&'a SendHandle),
}
impl EventTimelineItem {
pub(super) fn new(
sender: OwnedUserId,
sender_profile: TimelineDetails<Profile>,
timestamp: MilliSecondsSinceUnixEpoch,
content: TimelineItemContent,
kind: EventTimelineItemKind,
) -> Self {
Self { sender, sender_profile, timestamp, content, kind }
}
pub async fn from_latest_event(
client: Client,
room_id: &RoomId,
latest_event: LatestEvent,
) -> Option<EventTimelineItem> {
use super::traits::RoomDataProvider;
let SyncTimelineEvent { event: raw_sync_event, encryption_info, .. } =
latest_event.event().clone();
let Ok(event) = raw_sync_event.deserialize_as::<AnySyncTimelineEvent>() else {
warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!");
return None;
};
let timestamp = event.origin_server_ts();
let sender = event.sender().to_owned();
let event_id = event.event_id().to_owned();
let is_own = client.user_id().map(|uid| uid == sender).unwrap_or(false);
let item_content = TimelineItemContent::from_latest_event_content(event)?;
let reactions = IndexMap::new();
let read_receipts = IndexMap::new();
let is_highlighted = false;
let latest_edit_json = None;
let origin = RemoteEventOrigin::Sync;
let event_kind = RemoteEventTimelineItem {
event_id,
transaction_id: None,
reactions,
read_receipts,
is_own,
is_highlighted,
encryption_info,
original_json: Some(raw_sync_event),
latest_edit_json,
origin,
}
.into();
let room = client.get_room(room_id);
let sender_profile = if let Some(room) = room {
let mut profile = room.profile_from_latest_event(&latest_event).await;
if profile.is_none() {
profile = room.profile_from_user_id(&sender).await;
}
profile.map(TimelineDetails::Ready).unwrap_or(TimelineDetails::Unavailable)
} else {
TimelineDetails::Unavailable
};
Some(Self::new(sender, sender_profile, timestamp, item_content, event_kind))
}
pub fn is_local_echo(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Local(_))
}
pub(super) fn is_remote_event(&self) -> bool {
matches!(self.kind, EventTimelineItemKind::Remote(_))
}
pub(super) fn as_local(&self) -> Option<&LocalEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local_event_item) => local_event_item)
}
pub(super) fn as_remote(&self) -> Option<&RemoteEventTimelineItem> {
as_variant!(&self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub(super) fn as_remote_mut(&mut self) -> Option<&mut RemoteEventTimelineItem> {
as_variant!(&mut self.kind, EventTimelineItemKind::Remote(remote_event_item) => remote_event_item)
}
pub fn send_state(&self) -> Option<&EventSendState> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.send_state)
}
pub(crate) fn identifier(&self) -> TimelineEventItemId {
match &self.kind {
EventTimelineItemKind::Local(local) => local.identifier(),
EventTimelineItemKind::Remote(remote) => {
TimelineEventItemId::EventId(remote.event_id.clone())
}
}
}
pub fn transaction_id(&self) -> Option<&TransactionId> {
as_variant!(&self.kind, EventTimelineItemKind::Local(local) => &local.transaction_id)
}
pub fn event_id(&self) -> Option<&EventId> {
match &self.kind {
EventTimelineItemKind::Local(local_event) => local_event.event_id(),
EventTimelineItemKind::Remote(remote_event) => Some(&remote_event.event_id),
}
}
pub fn sender(&self) -> &UserId {
&self.sender
}
pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
&self.sender_profile
}
pub fn content(&self) -> &TimelineItemContent {
&self.content
}
pub fn reactions(&self) -> &ReactionsByKeyBySender {
static EMPTY_REACTIONS: Lazy<ReactionsByKeyBySender> = Lazy::new(Default::default);
match &self.kind {
EventTimelineItemKind::Local(_) => &EMPTY_REACTIONS,
EventTimelineItemKind::Remote(remote_event) => &remote_event.reactions,
}
}
pub fn read_receipts(&self) -> &IndexMap<OwnedUserId, Receipt> {
static EMPTY_RECEIPTS: Lazy<IndexMap<OwnedUserId, Receipt>> = Lazy::new(Default::default);
match &self.kind {
EventTimelineItemKind::Local(_) => &EMPTY_RECEIPTS,
EventTimelineItemKind::Remote(remote_event) => &remote_event.read_receipts,
}
}
pub fn timestamp(&self) -> MilliSecondsSinceUnixEpoch {
self.timestamp
}
pub fn is_own(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => true,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_own,
}
}
pub fn is_editable(&self) -> bool {
if !self.is_own() {
return false;
}
match self.content() {
TimelineItemContent::Message(message) => {
matches!(message.msgtype(), MessageType::Text(_) | MessageType::Emote(_))
}
TimelineItemContent::Poll(poll) => {
poll.response_data.is_empty() && poll.end_event_timestamp.is_none()
}
_ => {
false
}
}
}
pub fn is_highlighted(&self) -> bool {
match &self.kind {
EventTimelineItemKind::Local(_) => false,
EventTimelineItemKind::Remote(remote_event) => remote_event.is_highlighted,
}
}
pub fn encryption_info(&self) -> Option<&EncryptionInfo> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.encryption_info.as_ref(),
}
}
pub fn get_shield(&self, strict: bool) -> Option<ShieldState> {
self.encryption_info().map(|info| {
if strict {
info.verification_state.to_shield_state_strict()
} else {
info.verification_state.to_shield_state_lax()
}
})
}
pub fn can_be_replied_to(&self) -> bool {
if self.event_id().is_none() {
false
} else if let TimelineItemContent::Message(_) = self.content() {
true
} else {
self.latest_json().is_some()
}
}
pub fn original_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.original_json.as_ref(),
}
}
pub fn latest_edit_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
match &self.kind {
EventTimelineItemKind::Local(_) => None,
EventTimelineItemKind::Remote(remote_event) => remote_event.latest_edit_json.as_ref(),
}
}
pub fn latest_json(&self) -> Option<&Raw<AnySyncTimelineEvent>> {
self.latest_edit_json().or_else(|| self.original_json())
}
pub fn origin(&self) -> Option<EventItemOrigin> {
match &self.kind {
EventTimelineItemKind::Local(_) => Some(EventItemOrigin::Local),
EventTimelineItemKind::Remote(remote_event) => match remote_event.origin {
RemoteEventOrigin::Sync => Some(EventItemOrigin::Sync),
RemoteEventOrigin::Pagination => Some(EventItemOrigin::Pagination),
_ => None,
},
}
}
pub(super) fn set_content(&mut self, content: TimelineItemContent) {
self.content = content;
}
pub(super) fn with_kind(&self, kind: impl Into<EventTimelineItemKind>) -> Self {
Self { kind: kind.into(), ..self.clone() }
}
pub(super) fn with_content(
&self,
new_content: TimelineItemContent,
edit_json: Option<Raw<AnySyncTimelineEvent>>,
) -> Self {
let mut new = self.clone();
new.content = new_content;
if let EventTimelineItemKind::Remote(r) = &mut new.kind {
r.latest_edit_json = edit_json;
}
new
}
pub(super) fn with_sender_profile(&self, sender_profile: TimelineDetails<Profile>) -> Self {
Self { sender_profile, ..self.clone() }
}
pub(super) fn redact(&self, room_version: &RoomVersionId) -> Self {
let content = self.content.redact(room_version);
let kind = match &self.kind {
EventTimelineItemKind::Local(l) => EventTimelineItemKind::Local(l.clone()),
EventTimelineItemKind::Remote(r) => EventTimelineItemKind::Remote(r.redact()),
};
Self {
sender: self.sender.clone(),
sender_profile: self.sender_profile.clone(),
timestamp: self.timestamp,
content,
kind,
}
}
pub fn replied_to_info(&self) -> Result<RepliedToInfo, UnsupportedReplyItem> {
let reply_content = match self.content() {
TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()),
_ => {
let Some(raw_event) = self.latest_json() else {
return Err(UnsupportedReplyItem::MissingJson);
};
ReplyContent::Raw(raw_event.clone())
}
};
let Some(event_id) = self.event_id() else {
return Err(UnsupportedReplyItem::MissingEventId);
};
Ok(RepliedToInfo {
event_id: event_id.to_owned(),
sender: self.sender().to_owned(),
timestamp: self.timestamp(),
content: reply_content,
})
}
pub(super) fn handle(&self) -> TimelineItemHandle<'_> {
match &self.kind {
EventTimelineItemKind::Local(local) => {
if let Some(event_id) = local.event_id() {
TimelineItemHandle::Remote(event_id)
} else {
TimelineItemHandle::Local(
local.send_handle.as_ref().expect("Unexpected missing send_handle"),
)
}
}
EventTimelineItemKind::Remote(remote) => TimelineItemHandle::Remote(&remote.event_id),
}
}
}
impl From<LocalEventTimelineItem> for EventTimelineItemKind {
fn from(value: LocalEventTimelineItem) -> Self {
EventTimelineItemKind::Local(value)
}
}
impl From<RemoteEventTimelineItem> for EventTimelineItemKind {
fn from(value: RemoteEventTimelineItem) -> Self {
EventTimelineItemKind::Remote(value)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Profile {
pub display_name: Option<String>,
pub display_name_ambiguous: bool,
pub avatar_url: Option<OwnedMxcUri>,
}
#[derive(Clone, Debug)]
pub enum TimelineDetails<T> {
Unavailable,
Pending,
Ready(T),
Error(Arc<Error>),
}
impl<T> TimelineDetails<T> {
pub(crate) fn from_initial_value(value: Option<T>) -> Self {
match value {
Some(v) => Self::Ready(v),
None => Self::Unavailable,
}
}
pub(crate) fn is_unavailable(&self) -> bool {
matches!(self, Self::Unavailable)
}
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready(_))
}
}
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum EventItemOrigin {
Local,
Sync,
Pagination,
}
#[derive(Clone, Debug)]
pub struct ReactionInfo {
pub timestamp: MilliSecondsSinceUnixEpoch,
pub id: TimelineEventItemId,
}
pub type ReactionsByKeyBySender = IndexMap<String, IndexMap<OwnedUserId, ReactionInfo>>;
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use matrix_sdk::test_utils::logged_in_client;
use matrix_sdk_base::{
deserialized_responses::SyncTimelineEvent, latest_event::LatestEvent, sliding_sync::http,
MinimalStateEvent, OriginalMinimalStateEvent,
};
use matrix_sdk_test::{async_test, sync_timeline_event};
use ruma::{
events::{
room::{
member::RoomMemberEventContent,
message::{MessageFormat, MessageType},
},
AnySyncTimelineEvent,
},
room_id,
serde::Raw,
user_id, RoomId, UInt, UserId,
};
use super::{EventTimelineItem, Profile};
use crate::timeline::TimelineDetails;
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item() {
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_eq!(timeline_item.sender, user_id);
assert_matches!(timeline_item.sender_profile, TimelineDetails::Unavailable);
assert_eq!(timeline_item.timestamp.0, UInt::new(122344).unwrap());
if let MessageType::Text(txt) = timeline_item.content.as_message().unwrap().msgtype() {
assert_eq!(txt.body, "**My M**");
let formatted = txt.formatted.as_ref().unwrap();
assert_eq!(formatted.format, MessageFormat::Html);
assert_eq!(formatted.body, "<b>My M</b>");
} else {
panic!("Unexpected message type");
}
}
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_storage(
) {
use ruma::owned_mxc_uri;
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let mut room = http::response::Room::new();
room.timeline.push(member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs"));
let response = response_with_room(room_id, room);
client.process_sliding_sync_test_helper(&response).await.unwrap();
let timeline_item =
EventTimelineItem::from_latest_event(client, room_id, LatestEvent::new(event))
.await
.unwrap();
assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
assert_eq!(
profile,
Profile {
display_name: Some("Alice Margatroid".to_owned()),
display_name_ambiguous: false,
avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
}
);
}
#[async_test]
async fn test_latest_message_event_can_be_wrapped_as_a_timeline_item_with_sender_from_the_cache(
) {
use ruma::owned_mxc_uri;
let room_id = room_id!("!q:x.uk");
let user_id = user_id!("@t:o.uk");
let event = message_event(room_id, user_id, "**My M**", "<b>My M</b>", 122344);
let client = logged_in_client(None).await;
let member_event = MinimalStateEvent::Original(
member_event(room_id, user_id, "Alice Margatroid", "mxc://e.org/SEs")
.deserialize_as::<OriginalMinimalStateEvent<RoomMemberEventContent>>()
.unwrap(),
);
let room = http::response::Room::new();
let response = response_with_room(room_id, room);
client.process_sliding_sync_test_helper(&response).await.unwrap();
let timeline_item = EventTimelineItem::from_latest_event(
client,
room_id,
LatestEvent::new_with_sender_details(event, Some(member_event), None),
)
.await
.unwrap();
assert_let!(TimelineDetails::Ready(profile) = timeline_item.sender_profile);
assert_eq!(
profile,
Profile {
display_name: Some("Alice Margatroid".to_owned()),
display_name_ambiguous: false,
avatar_url: Some(owned_mxc_uri!("mxc://e.org/SEs"))
}
);
}
fn member_event(
room_id: &RoomId,
user_id: &UserId,
display_name: &str,
avatar_url: &str,
) -> Raw<AnySyncTimelineEvent> {
sync_timeline_event!({
"type": "m.room.member",
"content": {
"avatar_url": avatar_url,
"displayname": display_name,
"membership": "join",
"reason": ""
},
"event_id": "$143273582443PhrSn:example.org",
"origin_server_ts": 143273583,
"room_id": room_id,
"sender": "@example:example.org",
"state_key": user_id,
"type": "m.room.member",
"unsigned": {
"age": 1234
}
})
}
fn response_with_room(room_id: &RoomId, room: http::response::Room) -> http::Response {
let mut response = http::Response::new("6".to_owned());
response.rooms.insert(room_id.to_owned(), room);
response
}
fn message_event(
room_id: &RoomId,
user_id: &UserId,
body: &str,
formatted_body: &str,
ts: u64,
) -> SyncTimelineEvent {
sync_timeline_event!({
"event_id": "$eventid6",
"sender": user_id,
"origin_server_ts": ts,
"type": "m.room.message",
"room_id": room_id.to_string(),
"content": {
"body": body,
"format": "org.matrix.custom.html",
"formatted_body": formatted_body,
"msgtype": "m.text"
},
})
.into()
}
}