matrix_sdk_ui/timeline/event_item/content/
message.rsuse std::{fmt, sync::Arc};
use imbl::{vector, Vector};
use matrix_sdk::{deserialized_responses::TimelineEvent, Room};
use ruma::{
assign,
events::{
poll::unstable_start::{
NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent,
UnstablePollStartEventContent,
},
relation::{InReplyTo, Thread},
room::message::{
MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
SyncRoomMessageEvent,
},
AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
BundledMessageLikeRelations, Mentions,
},
html::RemoveReplyFallback,
serde::Raw,
OwnedEventId, OwnedUserId, UserId,
};
use tracing::{error, trace};
use super::TimelineItemContent;
use crate::{
timeline::{
event_item::{EventTimelineItem, Profile, TimelineDetails},
traits::RoomDataProvider,
Error as TimelineError, TimelineItem,
},
DEFAULT_SANITIZER_MODE,
};
#[derive(Clone)]
pub struct Message {
pub(in crate::timeline) msgtype: MessageType,
pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
pub(in crate::timeline) thread_root: Option<OwnedEventId>,
pub(in crate::timeline) edited: bool,
pub(in crate::timeline) mentions: Option<Mentions>,
}
impl Message {
pub(in crate::timeline) fn from_event(
c: RoomMessageEventContent,
edit: Option<RoomMessageEventContentWithoutRelation>,
timeline_items: &Vector<Arc<TimelineItem>>,
) -> Self {
let mut thread_root = None;
let in_reply_to = c.relates_to.and_then(|relation| match relation {
Relation::Reply { in_reply_to } => {
Some(InReplyToDetails::new(in_reply_to.event_id, timeline_items))
}
Relation::Thread(thread) => {
thread_root = Some(thread.event_id);
thread
.in_reply_to
.map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
}
_ => None,
});
let remove_reply_fallback =
if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
let mut msgtype = c.msgtype;
msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
let mut ret =
Self { msgtype, in_reply_to, thread_root, edited: false, mentions: c.mentions };
if let Some(edit) = edit {
ret.apply_edit(edit);
}
ret
}
pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) {
trace!("applying edit to a Message");
new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
self.msgtype = new_content.msgtype;
self.mentions = new_content.mentions;
self.edited = true;
}
pub fn msgtype(&self) -> &MessageType {
&self.msgtype
}
pub fn body(&self) -> &str {
self.msgtype.body()
}
pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
self.in_reply_to.as_ref()
}
pub fn is_threaded(&self) -> bool {
self.thread_root.is_some()
}
pub fn thread_root(&self) -> Option<&OwnedEventId> {
self.thread_root.as_ref()
}
pub fn is_edited(&self) -> bool {
self.edited
}
pub fn mentions(&self) -> Option<&Mentions> {
self.mentions.as_ref()
}
pub(in crate::timeline) fn to_content(&self) -> RoomMessageEventContent {
let relates_to = make_relates_to(
self.thread_root.clone(),
self.in_reply_to.as_ref().map(|details| details.event_id.clone()),
);
assign!(RoomMessageEventContent::new(self.msgtype.clone()), { relates_to })
}
pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
Self { in_reply_to: Some(in_reply_to), ..self.clone() }
}
}
impl From<Message> for RoomMessageEventContent {
fn from(msg: Message) -> Self {
let relates_to =
make_relates_to(msg.thread_root, msg.in_reply_to.map(|details| details.event_id));
assign!(Self::new(msg.msgtype), { relates_to })
}
}
pub(crate) fn extract_bundled_edit_event_json(
raw: &Raw<AnySyncTimelineEvent>,
) -> Option<Raw<AnySyncTimelineEvent>> {
let raw_unsigned: Raw<serde_json::Value> = raw.get_field("unsigned").ok()??;
let raw_relations: Raw<serde_json::Value> = raw_unsigned.get_field("m.relations").ok()??;
raw_relations.get_field::<Raw<AnySyncTimelineEvent>>("m.replace").ok()?
}
pub(crate) fn extract_room_msg_edit_content(
relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
) -> Option<RoomMessageEventContentWithoutRelation> {
match *relations.replace? {
AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
.content
.relates_to
{
Some(Relation::Replacement(re)) => {
trace!("found a bundled edit event in a room message");
Some(re.new_content)
}
_ => {
error!("got m.room.message event with an edit without a valid m.replace relation");
None
}
},
AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
_ => {
error!("got m.room.message event with an edit of a different event type");
None
}
}
}
pub(crate) fn extract_poll_edit_content(
relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
) -> Option<NewUnstablePollStartEventContentWithoutRelation> {
match *relations.replace? {
AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => {
match ev.content {
UnstablePollStartEventContent::Replacement(re) => {
trace!("found a bundled edit event in a poll");
Some(re.relates_to.new_content)
}
_ => {
error!("got new poll start event in a bundled edit");
None
}
}
}
AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None,
_ => {
error!("got poll edit event with an edit of a different event type");
None
}
}
}
fn make_relates_to(
thread_root: Option<OwnedEventId>,
in_reply_to: Option<OwnedEventId>,
) -> Option<Relation<RoomMessageEventContentWithoutRelation>> {
match (thread_root, in_reply_to) {
(Some(thread_root), Some(in_reply_to)) => {
Some(Relation::Thread(Thread::plain(thread_root, in_reply_to)))
}
(Some(thread_root), None) => Some(Relation::Thread(Thread::without_fallback(thread_root))),
(None, Some(in_reply_to)) => {
Some(Relation::Reply { in_reply_to: InReplyTo::new(in_reply_to) })
}
(None, None) => None,
}
}
#[cfg(not(tarpaulin_include))]
impl fmt::Debug for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { msgtype: _, in_reply_to, thread_root, edited, mentions: _ } = self;
f.debug_struct("Message")
.field("in_reply_to", in_reply_to)
.field("thread_root", thread_root)
.field("edited", edited)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct InReplyToDetails {
pub event_id: OwnedEventId,
pub event: TimelineDetails<Box<RepliedToEvent>>,
}
impl InReplyToDetails {
pub fn new(
event_id: OwnedEventId,
timeline_items: &Vector<Arc<TimelineItem>>,
) -> InReplyToDetails {
let event = timeline_items
.iter()
.filter_map(|it| it.as_event())
.find(|it| it.event_id() == Some(&*event_id))
.map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
}
}
#[derive(Clone, Debug)]
pub struct RepliedToEvent {
pub(in crate::timeline) content: TimelineItemContent,
pub(in crate::timeline) sender: OwnedUserId,
pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
}
impl RepliedToEvent {
pub fn content(&self) -> &TimelineItemContent {
&self.content
}
pub fn sender(&self) -> &UserId {
&self.sender
}
pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
&self.sender_profile
}
pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
Self {
content: timeline_item.content.clone(),
sender: timeline_item.sender.clone(),
sender_profile: timeline_item.sender_profile.clone(),
}
}
pub async fn try_from_timeline_event_for_room(
timeline_event: TimelineEvent,
room_data_provider: &Room,
) -> Result<Self, TimelineError> {
Self::try_from_timeline_event(timeline_event, room_data_provider).await
}
pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
timeline_event: TimelineEvent,
room_data_provider: &P,
) -> Result<Self, TimelineError> {
let event = match timeline_event.raw().deserialize() {
Ok(AnySyncTimelineEvent::MessageLike(event)) => event,
_ => {
return Err(TimelineError::UnsupportedEvent);
}
};
let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else {
return Err(TimelineError::UnsupportedEvent);
};
let content = TimelineItemContent::Message(Message::from_event(
c,
extract_room_msg_edit_content(event.relations()),
&vector![],
));
let sender = event.sender().to_owned();
let sender_profile = TimelineDetails::from_initial_value(
room_data_provider.profile_from_user_id(&sender).await,
);
Ok(Self { content, sender, sender_profile })
}
}