diff --git a/bindings/matrix-sdk-ffi/src/lib.rs b/bindings/matrix-sdk-ffi/src/lib.rs index f475f7e86..91c7fb634 100644 --- a/bindings/matrix-sdk-ffi/src/lib.rs +++ b/bindings/matrix-sdk-ffi/src/lib.rs @@ -1,6 +1,6 @@ // TODO: target-os conditional would be good. -#![allow(unused_qualifications, clippy::new_without_default)] +#![allow(unused_qualifications, clippy::new_without_default, unused_macros)] macro_rules! unwrap_or_clone_arc_into_variant { ( diff --git a/bindings/matrix-sdk-ffi/src/room_list.rs b/bindings/matrix-sdk-ffi/src/room_list.rs index 48a6c1003..df6926bc8 100644 --- a/bindings/matrix-sdk-ffi/src/room_list.rs +++ b/bindings/matrix-sdk-ffi/src/room_list.rs @@ -702,8 +702,8 @@ impl RoomListItem { self.inner.is_encrypted().await.unwrap_or(false) } - async fn latest_event(&self) -> Option> { - self.inner.latest_event().await.map(EventTimelineItem).map(Arc::new) + async fn latest_event(&self) -> Option { + self.inner.latest_event().await.map(|e| e.into()) } } diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 89b9c23f7..2302e3768 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -54,6 +54,7 @@ use tracing::info; use crate::{ error::{ClientError, MediaInfoError}, helpers::unwrap_or_clone_arc, + timeline::MessageContent, utils::u64_to_uint, }; @@ -227,6 +228,7 @@ pub impl RoomMessageEventContentWithoutRelationExt for RoomMessageEventContentWi } } +#[derive(Clone)] pub struct Mentions { pub user_ids: Vec, pub room: bool, @@ -861,3 +863,13 @@ impl From for PollKind { } } } + +/// Creates a [`RoomMessageEventContentWithoutRelation`] given a +/// [`MessageContent`] value. +#[uniffi::export] +pub fn content_without_relation_from_message( + message: MessageContent, +) -> Result { + let msg_type = message.msg_type.try_into()?; + Ok(RoomMessageEventContentWithoutRelation::new(msg_type)) +} diff --git a/bindings/matrix-sdk-ffi/src/timeline/content.rs b/bindings/matrix-sdk-ffi/src/timeline/content.rs index ffe2cecb5..0c8723cff 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/content.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/content.rs @@ -16,45 +16,40 @@ use std::{collections::HashMap, sync::Arc}; use matrix_sdk::{crypto::types::events::UtdCause, room::power_levels::power_level_user_changes}; use matrix_sdk_ui::timeline::{PollResult, RoomPinnedEventsChange, TimelineDetails}; -use ruma::events::room::{message::RoomMessageEventContentWithoutRelation, MediaSource}; -use tracing::warn; +use ruma::events::room::MediaSource; use super::ProfileDetails; -use crate::ruma::{ImageInfo, MessageType, PollKind}; +use crate::ruma::{ImageInfo, Mentions, MessageType, PollKind}; -#[derive(Clone, uniffi::Object)] -pub struct TimelineItemContent(pub(crate) matrix_sdk_ui::timeline::TimelineItemContent); - -#[uniffi::export] -impl TimelineItemContent { - pub fn kind(&self) -> TimelineItemContentKind { +impl From<&matrix_sdk_ui::timeline::TimelineItemContent> for TimelineItemContent { + fn from(value: &matrix_sdk_ui::timeline::TimelineItemContent) -> Self { use matrix_sdk_ui::timeline::TimelineItemContent as Content; - match &self.0 { - Content::Message(_) => TimelineItemContentKind::Message, + match value { + Content::Message(message) => TimelineItemContent::Message { content: message.into() }, - Content::RedactedMessage => TimelineItemContentKind::RedactedMessage, + Content::RedactedMessage => TimelineItemContent::RedactedMessage, Content::Sticker(sticker) => { let content = sticker.content(); - TimelineItemContentKind::Sticker { + TimelineItemContent::Sticker { body: content.body.clone(), info: (&content.info).into(), source: Arc::new(MediaSource::from(content.source.clone())), } } - Content::Poll(poll_state) => TimelineItemContentKind::from(poll_state.results()), + Content::Poll(poll_state) => TimelineItemContent::from(poll_state.results()), - Content::CallInvite => TimelineItemContentKind::CallInvite, + Content::CallInvite => TimelineItemContent::CallInvite, - Content::CallNotify => TimelineItemContentKind::CallNotify, + Content::CallNotify => TimelineItemContent::CallNotify, Content::UnableToDecrypt(msg) => { - TimelineItemContentKind::UnableToDecrypt { msg: EncryptedMessage::new(msg) } + TimelineItemContent::UnableToDecrypt { msg: EncryptedMessage::new(msg) } } - Content::MembershipChange(membership) => TimelineItemContentKind::RoomMembership { + Content::MembershipChange(membership) => TimelineItemContent::RoomMembership { user_id: membership.user_id().to_string(), user_display_name: membership.display_name(), change: membership.change().map(Into::into), @@ -74,7 +69,7 @@ impl TimelineItemContent { ) }) .unzip(); - TimelineItemContentKind::ProfileChange { + TimelineItemContent::ProfileChange { display_name: display_name.flatten(), prev_display_name: prev_display_name.flatten(), avatar_url: avatar_url.flatten(), @@ -82,20 +77,20 @@ impl TimelineItemContent { } } - Content::OtherState(state) => TimelineItemContentKind::State { + Content::OtherState(state) => TimelineItemContent::State { state_key: state.state_key().to_owned(), content: state.content().into(), }, Content::FailedToParseMessageLike { event_type, error } => { - TimelineItemContentKind::FailedToParseMessageLike { + TimelineItemContent::FailedToParseMessageLike { event_type: event_type.to_string(), error: error.to_string(), } } Content::FailedToParseState { event_type, state_key, error } => { - TimelineItemContentKind::FailedToParseState { + TimelineItemContent::FailedToParseState { event_type: event_type.to_string(), state_key: state_key.to_string(), error: error.to_string(), @@ -103,16 +98,45 @@ impl TimelineItemContent { } } } +} - pub fn as_message(self: Arc) -> Option> { - use matrix_sdk_ui::timeline::TimelineItemContent as Content; - unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg))) +#[derive(Clone, uniffi::Record)] +pub struct MessageContent { + pub msg_type: MessageType, + pub body: String, + pub in_reply_to: Option>, + pub thread_root: Option, + pub is_edited: bool, + pub mentions: Option, +} + +impl From<&matrix_sdk_ui::timeline::Message> for MessageContent { + fn from(value: &matrix_sdk_ui::timeline::Message) -> Self { + Self { + msg_type: value.msgtype().clone().into(), + body: value.body().to_owned(), + in_reply_to: value.in_reply_to().map(|r| Arc::new(r.into())), + is_edited: value.is_edited(), + thread_root: value.thread_root().map(|id| id.to_string()), + mentions: value.mentions().cloned().map(|m| m.into()), + } } } -#[derive(uniffi::Enum)] -pub enum TimelineItemContentKind { - Message, +impl From for Mentions { + fn from(value: ruma::events::Mentions) -> Self { + Self { + user_ids: value.user_ids.iter().map(|id| id.to_string()).collect(), + room: value.room, + } + } +} + +#[derive(Clone, uniffi::Enum)] +pub enum TimelineItemContent { + Message { + content: MessageContent, + }, RedactedMessage, Sticker { body: String, @@ -160,36 +184,6 @@ pub enum TimelineItemContentKind { } #[derive(Clone, uniffi::Object)] -pub struct Message(matrix_sdk_ui::timeline::Message); - -#[uniffi::export] -impl Message { - pub fn msgtype(&self) -> MessageType { - self.0.msgtype().clone().into() - } - - pub fn body(&self) -> String { - self.0.msgtype().body().to_owned() - } - - pub fn in_reply_to(&self) -> Option { - self.0.in_reply_to().map(InReplyToDetails::from) - } - - pub fn is_threaded(&self) -> bool { - self.0.is_threaded() - } - - pub fn is_edited(&self) -> bool { - self.0.is_edited() - } - - pub fn content(&self) -> Arc { - Arc::new(RoomMessageEventContentWithoutRelation::new(self.0.msgtype().clone())) - } -} - -#[derive(uniffi::Record)] pub struct InReplyToDetails { event_id: String, event: RepliedToEventDetails, @@ -201,6 +195,17 @@ impl InReplyToDetails { } } +#[uniffi::export] +impl InReplyToDetails { + pub fn event_id(&self) -> String { + self.event_id.clone() + } + + pub fn event(&self) -> RepliedToEventDetails { + self.event.clone() + } +} + impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self { let event_id = inner.event_id.to_string(); @@ -208,7 +213,7 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable, TimelineDetails::Pending => RepliedToEventDetails::Pending, TimelineDetails::Ready(event) => RepliedToEventDetails::Ready { - content: Arc::new(TimelineItemContent(event.content().to_owned())), + content: event.content().into(), sender: event.sender().to_string(), sender_profile: event.sender_profile().into(), }, @@ -221,11 +226,11 @@ impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails { } } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum RepliedToEventDetails { Unavailable, Pending, - Ready { content: Arc, sender: String, sender_profile: ProfileDetails }, + Ready { content: TimelineItemContent, sender: String, sender_profile: ProfileDetails }, Error { message: String }, } @@ -419,15 +424,15 @@ impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherStat } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct PollAnswer { pub id: String, pub text: String, } -impl From for TimelineItemContentKind { +impl From for TimelineItemContent { fn from(value: PollResult) -> Self { - TimelineItemContentKind::Poll { + TimelineItemContent::Poll { question: value.question, kind: PollKind::from(value.kind), max_selections: value.max_selections, diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index f28b0e9a0..ccfa0f16e 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -78,6 +78,8 @@ use crate::{ mod content; +pub use content::MessageContent; + #[derive(uniffi::Object)] #[repr(transparent)] pub struct Timeline { @@ -423,11 +425,11 @@ impl Timeline { pub async fn send_poll_response( self: Arc, - poll_start_id: String, + poll_start_event_id: String, answers: Vec, ) -> Result<(), ClientError> { let poll_start_event_id = - EventId::parse(poll_start_id).context("Failed to parse EventId")?; + EventId::parse(poll_start_event_id).context("Failed to parse EventId")?; let poll_response_event_content = UnstablePollResponseEventContent::new(answers, poll_start_event_id); let event_content = @@ -442,11 +444,11 @@ impl Timeline { pub fn end_poll( self: Arc, - poll_start_id: String, + poll_start_event_id: String, text: String, ) -> Result<(), ClientError> { let poll_start_event_id = - EventId::parse(poll_start_id).context("Failed to parse EventId")?; + EventId::parse(poll_start_event_id).context("Failed to parse EventId")?; let poll_end_event_content = UnstablePollEndEventContent::new(text, poll_start_event_id); let event_content = AnyMessageLikeEventContent::UnstablePollEnd(poll_end_event_content); @@ -486,13 +488,19 @@ impl Timeline { /// /// Returns whether the edit did happen. It can only return false for /// local events that are being processed. - pub async fn edit( - &self, - item: Arc, - new_content: EditedContent, - ) -> Result { - let new_content: SdkEditedContent = new_content.try_into()?; - self.inner.edit(&item.0, new_content).await.map_err(ClientError::from) + pub async fn edit(&self, id: String, new_content: EditedContent) -> Result { + let event = if let Ok(event_id) = EventId::parse(&id) { + self.inner.item_by_event_id(&event_id).await + } else { + let transaction_id: OwnedTransactionId = id.into(); + self.inner.local_item_by_transaction_id(&transaction_id).await + }; + if let Some(event) = event { + let new_content: SdkEditedContent = new_content.try_into()?; + self.inner.edit(&event, new_content).await.map_err(ClientError::from) + } else { + Ok(false) + } } pub async fn send_location( @@ -558,14 +566,14 @@ impl Timeline { pub async fn get_event_timeline_item_by_event_id( &self, event_id: String, - ) -> Result, ClientError> { + ) -> Result { let event_id = EventId::parse(event_id)?; let item = self .inner .item_by_event_id(&event_id) .await .context("Item with given event ID not found")?; - Ok(Arc::new(EventTimelineItem(item))) + Ok(item.into()) } /// Get the current timeline item for the given transaction ID, if any. @@ -578,14 +586,14 @@ impl Timeline { pub async fn get_event_timeline_item_by_transaction_id( &self, transaction_id: String, - ) -> Result, ClientError> { + ) -> Result { let transaction_id: OwnedTransactionId = transaction_id.into(); let item = self .inner .local_item_by_transaction_id(&transaction_id) .await .context("Item with given transaction ID not found")?; - Ok(Arc::new(EventTimelineItem(item))) + Ok(item.into()) } /// Redacts an event from the timeline. @@ -600,16 +608,26 @@ impl Timeline { /// local events that are being processed. pub async fn redact_event( &self, - item: Arc, + id: String, reason: Option, ) -> Result { - let removed = self - .inner - .redact(&item.0, reason.as_deref()) - .await - .map_err(|err| anyhow::anyhow!(err))?; + let event = if let Ok(event_id) = EventId::parse(&id) { + self.inner.item_by_event_id(&event_id).await + } else { + let transaction_id: OwnedTransactionId = id.into(); + self.inner.local_item_by_transaction_id(&transaction_id).await + }; + if let Some(event) = event { + let removed = self + .inner + .redact(&event, reason.as_deref()) + .await + .map_err(|err| anyhow::anyhow!(err))?; - Ok(removed) + Ok(removed) + } else { + Ok(false) + } } /// Load the reply details for the given event id. @@ -640,7 +658,7 @@ impl Timeline { Ok(replied_to) => Ok(InReplyToDetails::new( event_id_str, RepliedToEventDetails::Ready { - content: Arc::new(TimelineItemContent(replied_to.content().clone())), + content: replied_to.content().into(), sender: replied_to.sender().to_string(), sender_profile: replied_to.sender_profile().into(), }, @@ -672,6 +690,14 @@ impl Timeline { let event_id = EventId::parse(event_id).map_err(ClientError::from)?; self.inner.unpin_event(&event_id).await.map_err(ClientError::from) } + + pub fn create_message_content( + &self, + msg_type: crate::ruma::MessageType, + ) -> Option> { + let msg_type: Option = msg_type.try_into().ok(); + msg_type.map(|m| Arc::new(RoomMessageEventContentWithoutRelation::new(m))) + } } #[derive(uniffi::Object)] @@ -871,9 +897,9 @@ impl TimelineItem { #[uniffi::export] impl TimelineItem { - pub fn as_event(self: Arc) -> Option> { + pub fn as_event(self: Arc) -> Option { let event_item = self.0.as_event()?; - Some(Arc::new(EventTimelineItem(event_item.clone()))) + Some(event_item.clone().into()) } pub fn as_virtual(self: Arc) -> Option { @@ -995,7 +1021,7 @@ fn event_send_state_from_sending_failed(error: &Error, is_recoverable: bool) -> /// Recommended decorations for decrypted messages, representing the message's /// authenticity properties. -#[derive(uniffi::Enum)] +#[derive(uniffi::Enum, Clone)] pub enum ShieldState { /// A red shield with a tooltip containing the associated message should be /// presented. @@ -1021,100 +1047,73 @@ impl From for ShieldState { } } -#[derive(uniffi::Object)] -pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem); +#[derive(Clone, uniffi::Record)] +pub struct EventTimelineItem { + is_local: bool, + is_remote: bool, + transaction_id: Option, + event_id: Option, + sender: String, + sender_profile: ProfileDetails, + is_own: bool, + is_editable: bool, + content: TimelineItemContent, + timestamp: u64, + reactions: Vec, + debug_info: EventTimelineItemDebugInfo, + local_send_state: Option, + read_receipts: HashMap, + origin: Option, + can_be_replied_to: bool, + message_shield: Option, +} -#[uniffi::export] -impl EventTimelineItem { - pub fn is_local(&self) -> bool { - self.0.is_local_echo() - } - - pub fn is_remote(&self) -> bool { - !self.0.is_local_echo() - } - - pub fn transaction_id(&self) -> Option { - self.0.transaction_id().map(ToString::to_string) - } - - pub fn event_id(&self) -> Option { - self.0.event_id().map(ToString::to_string) - } - - pub fn sender(&self) -> String { - self.0.sender().to_string() - } - - pub fn sender_profile(&self) -> ProfileDetails { - self.0.sender_profile().into() - } - - pub fn is_own(&self) -> bool { - self.0.is_own() - } - - pub fn is_editable(&self) -> bool { - self.0.is_editable() - } - - pub fn content(&self) -> Arc { - Arc::new(TimelineItemContent(self.0.content().clone())) - } - - pub fn timestamp(&self) -> u64 { - self.0.timestamp().0.into() - } - - pub fn reactions(&self) -> Vec { - self.0 +impl From for EventTimelineItem { + fn from(value: matrix_sdk_ui::timeline::EventTimelineItem) -> Self { + let reactions = value .reactions() .iter() .map(|(k, v)| Reaction { key: k.to_owned(), senders: v - .iter() + .into_iter() .map(|(sender_id, info)| ReactionSenderData { sender_id: sender_id.to_string(), timestamp: info.timestamp.0.into(), }) .collect(), }) - .collect() - } - - pub fn debug_info(&self) -> EventTimelineItemDebugInfo { - EventTimelineItemDebugInfo { - model: format!("{:#?}", self.0), - original_json: self.0.original_json().map(|raw| raw.json().get().to_owned()), - latest_edit_json: self.0.latest_edit_json().map(|raw| raw.json().get().to_owned()), + .collect(); + let debug_info = EventTimelineItemDebugInfo { + model: format!("{:#?}", value), + original_json: value.original_json().map(|raw| raw.json().get().to_owned()), + latest_edit_json: value.latest_edit_json().map(|raw| raw.json().get().to_owned()), + }; + let read_receipts = + value.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect(); + Self { + is_local: value.is_local_echo(), + is_remote: !value.is_local_echo(), + transaction_id: value.transaction_id().map(|t| t.to_string()), + event_id: value.event_id().map(|e| e.to_string()), + sender: value.sender().to_string(), + sender_profile: value.sender_profile().into(), + is_own: value.is_own(), + is_editable: value.is_editable(), + content: value.content().into(), + timestamp: value.timestamp().0.into(), + reactions, + debug_info, + local_send_state: value.send_state().map(|s| s.into()), + read_receipts, + origin: value.origin(), + can_be_replied_to: value.can_be_replied_to(), + message_shield: value.get_shield(false).map(Into::into), } } - - pub fn local_send_state(&self) -> Option { - self.0.send_state().map(Into::into) - } - - pub fn read_receipts(&self) -> HashMap { - self.0.read_receipts().iter().map(|(k, v)| (k.to_string(), v.clone().into())).collect() - } - - pub fn origin(&self) -> Option { - self.0.origin() - } - - pub fn can_be_replied_to(&self) -> bool { - self.0.can_be_replied_to() - } - - /// Gets the [`ShieldState`] which can be used to decorate messages in the - /// recommended way. - pub fn get_shield(&self, strict: bool) -> Option { - self.0.get_shield(strict).map(Into::into) - } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct Receipt { pub timestamp: Option, } @@ -1125,14 +1124,14 @@ impl From for Receipt { } } -#[derive(uniffi::Record)] +#[derive(Clone, uniffi::Record)] pub struct EventTimelineItemDebugInfo { model: String, original_json: Option, latest_edit_json: Option, } -#[derive(uniffi::Enum)] +#[derive(Clone, uniffi::Enum)] pub enum ProfileDetails { Unavailable, Pending, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs index 808204210..8730d9fc8 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/content/message.rs @@ -117,6 +117,11 @@ impl Message { Self { msgtype, in_reply_to, thread_root, edited, mentions } } + /// Create a forwarded message from a [`MessageType`]. + pub fn forwarded(msgtype: MessageType) -> Self { + Self { msgtype, in_reply_to: None, thread_root: None, edited: false, mentions: None } + } + /// Get the `msgtype`-specific data of this message. pub fn msgtype(&self) -> &MessageType { &self.msgtype @@ -139,6 +144,11 @@ impl Message { self.thread_root.is_some() } + /// Get the [`OwnedEventId`] of the root event of a thread if it exists. + pub fn thread_root(&self) -> Option { + self.thread_root.clone() + } + /// Get the edit state of this message (has been edited: `true` / /// `false`). pub fn is_edited(&self) -> bool {