diff --git a/bindings/matrix-sdk-ffi/src/room.rs b/bindings/matrix-sdk-ffi/src/room.rs index 3d25e09e5df..d1e7ae37d48 100644 --- a/bindings/matrix-sdk-ffi/src/room.rs +++ b/bindings/matrix-sdk-ffi/src/room.rs @@ -696,20 +696,17 @@ impl Room { /// Stores the given `ComposerDraft` in the state store using the current /// room id, as identifier. pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<(), ClientError> { - self.inner.save_composer_draft(draft).await?; - Ok(()) + Ok(self.inner.save_composer_draft(draft).await?) } /// Retrieves the `ComposerDraft` stored in the state store for this room. pub async fn restore_composer_draft(&self) -> Result, ClientError> { - let draft = self.inner.restore_composer_draft().await?; - Ok(draft) + Ok(self.inner.restore_composer_draft().await?) } /// Removes the `ComposerDraft` stored in the state store for this room. pub async fn clear_composer_draft(&self) -> Result<(), ClientError> { - self.inner.clear_composer_draft().await?; - Ok(()) + Ok(self.inner.clear_composer_draft().await?) } /// Loads the reply details for the given event id. diff --git a/bindings/matrix-sdk-ffi/src/timeline/mod.rs b/bindings/matrix-sdk-ffi/src/timeline/mod.rs index d2235ef85f5..d18eae4003c 100644 --- a/bindings/matrix-sdk-ffi/src/timeline/mod.rs +++ b/bindings/matrix-sdk-ffi/src/timeline/mod.rs @@ -456,13 +456,13 @@ impl Timeline { event_id: String, ) -> Result<(), ClientError> { let event_id = EventId::parse(event_id)?; - let editing_info = self + let edit_info = self .inner .get_edit_info_from_event_id(&event_id) .await .map_err(|err| anyhow::anyhow!(err))?; self.inner - .edit((*new_content).clone(), editing_info) + .edit((*new_content).clone(), edit_info) .await .map_err(|err| anyhow::anyhow!(err))?; Ok(()) diff --git a/crates/matrix-sdk-base/src/store/memory_store.rs b/crates/matrix-sdk-base/src/store/memory_store.rs index b2cba546385..eb99c3fb130 100644 --- a/crates/matrix-sdk-base/src/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/store/memory_store.rs @@ -49,7 +49,7 @@ use crate::{ #[derive(Debug)] pub struct MemoryStore { recently_visited_rooms: StdRwLock>>, - composer_drafts: StdRwLock>, + composer_drafts: StdRwLock>, user_avatar_url: StdRwLock>, sync_token: StdRwLock>, filters: StdRwLock>, @@ -192,7 +192,7 @@ impl StateStore for MemoryStore { .composer_drafts .read() .unwrap() - .get(room_id.as_str()) + .get(room_id) .cloned() .map(StateStoreDataValue::ComposerDraft), }) @@ -230,7 +230,7 @@ impl StateStore for MemoryStore { } StateStoreDataKey::ComposerDraft(room_id) => { self.composer_drafts.write().unwrap().insert( - room_id.to_string(), + room_id.to_owned(), value.into_composer_draft().expect("Session data not a composer draft"), ); } @@ -252,7 +252,7 @@ impl StateStore for MemoryStore { self.recently_visited_rooms.write().unwrap().remove(user_id.as_str()); } StateStoreDataKey::ComposerDraft(room_id) => { - self.composer_drafts.write().unwrap().remove(room_id.as_str()); + self.composer_drafts.write().unwrap().remove(room_id); } } Ok(()) diff --git a/crates/matrix-sdk-base/src/store/traits.rs b/crates/matrix-sdk-base/src/store/traits.rs index b3cbbc4072e..60f9dc05d09 100644 --- a/crates/matrix-sdk-base/src/store/traits.rs +++ b/crates/matrix-sdk-base/src/store/traits.rs @@ -811,13 +811,13 @@ pub enum StateStoreDataValue { RecentlyVisitedRooms(Vec), /// A composer draft for the room. - /// /// To learn more, see [`ComposerDraft`]. + /// To learn more, see [`ComposerDraft`]. /// - /// [`ComposerDraft`]: self::ComposerDraft + /// [`ComposerDraft`]: Self::ComposerDraft ComposerDraft(ComposerDraft), } -/// Struct that represents the current draft of the composer for the room +/// Current draft of the composer for the room. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] pub struct ComposerDraft { @@ -830,7 +830,7 @@ pub struct ComposerDraft { draft_type: DraftType, } -/// Struct that represents the type of draft the composer is composing. +/// The type of draft of the composer. #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] pub enum DraftType { @@ -884,7 +884,10 @@ pub enum StateStoreDataKey<'a> { /// Recently visited room identifiers RecentlyVisitedRooms(&'a UserId), - /// Composer draft for the room + /// A composer draft for the room. + /// To learn more, see [`ComposerDraft`]. + /// + /// [`ComposerDraft`]: Self::ComposerDraft ComposerDraft(&'a RoomId), } @@ -901,7 +904,7 @@ impl StateStoreDataKey<'_> { /// [`RecentlyVisitedRooms`][Self::RecentlyVisitedRooms] variant. pub const RECENTLY_VISITED_ROOMS: &'static str = "recently_visited_rooms"; - /// Key prefix to user for the [`ComposerDraft`][Self::ComposerDraft] + /// Key prefix to use for the [`ComposerDraft`][Self::ComposerDraft] /// variant. pub const COMPOSER_DRAFT: &'static str = "composer_draft"; } diff --git a/crates/matrix-sdk-sqlite/src/state_store.rs b/crates/matrix-sdk-sqlite/src/state_store.rs index 082ef8557a9..cc7250b4237 100644 --- a/crates/matrix-sdk-sqlite/src/state_store.rs +++ b/crates/matrix-sdk-sqlite/src/state_store.rs @@ -278,8 +278,8 @@ impl SqliteStateStore { StateStoreDataKey::RecentlyVisitedRooms(b) => { Cow::Owned(format!("{}:{b}", StateStoreDataKey::RECENTLY_VISITED_ROOMS)) } - StateStoreDataKey::ComposerDraft(r) => { - Cow::Owned(format!("{}:{r}", StateStoreDataKey::COMPOSER_DRAFT)) + StateStoreDataKey::ComposerDraft(room_id) => { + Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::COMPOSER_DRAFT)) } }; diff --git a/crates/matrix-sdk-ui/src/timeline/error.rs b/crates/matrix-sdk-ui/src/timeline/error.rs index 2d844e378c8..1c3b69a3fe3 100644 --- a/crates/matrix-sdk-ui/src/timeline/error.rs +++ b/crates/matrix-sdk-ui/src/timeline/error.rs @@ -107,7 +107,7 @@ enum UnsupportedReplyItemInner { MissingEvent, #[error("error deserializing event")] DeserializationError, - #[error("could not get content")] + #[error("could not get event content")] MissingContent, } @@ -120,8 +120,8 @@ impl UnsupportedEditItem { pub(super) const MISSING_EVENT: Self = Self(UnsupportedEditItemInner::MissingEvent); pub(super) const NOT_ROOM_MESSAGE: Self = Self(UnsupportedEditItemInner::NotRoomMessage); pub(super) const NOT_POLL_EVENT: Self = Self(UnsupportedEditItemInner::NotPollEvent); - pub(super) const DESERIALIZATION_ERROR: Self = - Self(UnsupportedEditItemInner::DeserializationError); + pub(super) const FAILED_TO_DESERIALIZE_EVENT: Self = + Self(UnsupportedEditItemInner::FailedToDeserializeEvent); pub(super) const MISSING_CONTENT: Self = Self(UnsupportedEditItemInner::MissingContent); } @@ -143,7 +143,7 @@ enum UnsupportedEditItemInner { #[error("event could not be obtained")] MissingEvent, #[error("error deserializing event")] - DeserializationError, - #[error("could not get content")] + FailedToDeserializeEvent, + #[error("could not get event content")] MissingContent, } 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 a6b609502d3..1f48235e28d 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 @@ -238,6 +238,8 @@ impl InReplyToDetails { InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) } } + /// Create a new `InReplyToDetails` with the given event ID and the event + /// timeline details. pub fn new_with_timeline_details( event_id: OwnedEventId, event: TimelineDetails>, diff --git a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs index e2d826a0ce3..2da77332e73 100644 --- a/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/event_item/mod.rs @@ -45,6 +45,7 @@ pub(super) use self::{ local::LocalEventTimelineItem, remote::{RemoteEventOrigin, RemoteEventTimelineItem}, }; +use super::{EditInfo, RepliedToInfo, ReplyContent, UnsupportedEditItem, UnsupportedReplyItem}; /// An item in the timeline that represents at least one event. /// @@ -417,6 +418,44 @@ impl EventTimelineItem { kind, } } + + /// Gives the information needed to reply to the event of the item. + pub fn get_replied_to_info(&self) -> Result { + 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::MISSING_JSON); + }; + + ReplyContent::Raw(raw_event.clone()) + } + }; + + let Some(event_id) = self.event_id() else { + return Err(UnsupportedReplyItem::MISSING_EVENT_ID); + }; + + Ok(RepliedToInfo { + event_id: event_id.to_owned(), + sender: self.sender().to_owned(), + timestamp: self.timestamp(), + content: reply_content, + }) + } + + /// Gives the information needed to edit the event of the item. + pub fn get_edit_info(&self) -> Result { + // Early returns here must be in sync with + // `EventTimelineItem::can_be_edited` + let Some(event_id) = self.event_id() else { + return Err(UnsupportedEditItem::MISSING_EVENT_ID); + }; + let TimelineItemContent::Message(original_content) = self.content() else { + return Err(UnsupportedEditItem::NOT_ROOM_MESSAGE); + }; + Ok(EditInfo { event_id: event_id.to_owned(), original_message: original_content.clone() }) + } } impl From for EventTimelineItemKind { diff --git a/crates/matrix-sdk-ui/src/timeline/mod.rs b/crates/matrix-sdk-ui/src/timeline/mod.rs index e2add34e0a2..3023e445020 100644 --- a/crates/matrix-sdk-ui/src/timeline/mod.rs +++ b/crates/matrix-sdk-ui/src/timeline/mod.rs @@ -110,28 +110,34 @@ use self::{ util::rfind_event_by_id, }; +/// Information needed to edit an event. #[derive(Debug)] pub struct EditInfo { + /// The event ID of the event that needs editing. event_id: OwnedEventId, + /// The original content of the event that needs editing. original_message: Message, } /// Information needed to reply to an event. #[derive(Debug)] pub struct RepliedToInfo { - /// The event ID of the event to reply to + /// The event ID of the event to reply to. event_id: OwnedEventId, - /// The sender of the event to reply to + /// The sender of the event to reply to. sender: OwnedUserId, - /// The timestamp of the event to reply to + /// The timestamp of the event to reply to. timestamp: MilliSecondsSinceUnixEpoch, - /// The content of the event to reply to + /// The content of the event to reply to. content: ReplyContent, } +/// The content of a reply. #[derive(Debug)] enum ReplyContent { + /// Content of a message event. Message(Message), + /// Content of any other kind of event stored as raw. Raw(Raw), } @@ -385,47 +391,21 @@ impl Timeline { Ok(()) } - /// Gives the information needed to reply to an event from a timeline item. - pub fn get_replied_to_info_from_event_timeline_item( - &self, - reply_item: &EventTimelineItem, - ) -> Result { - let reply_content = match reply_item.content() { - TimelineItemContent::Message(msg) => ReplyContent::Message(msg.to_owned()), - _ => { - let Some(raw_event) = reply_item.latest_json() else { - return Err(UnsupportedReplyItem::MISSING_JSON); - }; - - ReplyContent::Raw(raw_event.clone()) - } - }; - - let Some(event_id) = reply_item.event_id() else { - return Err(UnsupportedReplyItem::MISSING_EVENT_ID); - }; - - let reply_info = RepliedToInfo { - event_id: event_id.to_owned(), - sender: reply_item.sender().to_owned(), - timestamp: reply_item.timestamp(), - content: reply_content, - }; - - Ok(reply_info) - } - /// Gives the information needed to reply to an event from an event id. pub async fn get_replied_to_info_from_event_id( &self, event_id: &EventId, ) -> Result { if let Some(timeline_item) = self.item_by_event_id(event_id).await { - return self.get_replied_to_info_from_event_timeline_item(&timeline_item); + return timeline_item.get_replied_to_info(); } - let Ok(event) = self.room().event(event_id).await else { - return Err(UnsupportedReplyItem::MISSING_EVENT); + let event = match self.room().event(event_id).await { + Ok(event) => event, + Err(error) => { + error!("Failed to fetch event with ID {event_id} with error: {error}"); + return Err(UnsupportedReplyItem::MISSING_EVENT); + } }; let raw_sync_event: Raw = event.event.cast(); @@ -445,14 +425,12 @@ impl Timeline { _ => ReplyContent::Raw(raw_sync_event), }; - let reply_info = RepliedToInfo { + Ok(RepliedToInfo { event_id: event_id.to_owned(), sender: sync_event.sender().to_owned(), timestamp: sync_event.origin_server_ts(), content: reply_content, - }; - - Ok(reply_info) + }) } /// Send an edit to the given event. @@ -503,40 +481,31 @@ impl Timeline { Ok(()) } - pub fn get_edit_info_from_event_timeline_item( - &self, - edit_item: &EventTimelineItem, - ) -> Result { - // Early returns here must be in sync with - // `EventTimelineItem::can_be_edited` - let Some(event_id) = edit_item.event_id() else { - return Err(UnsupportedEditItem::MISSING_EVENT_ID); - }; - let TimelineItemContent::Message(original_content) = edit_item.content() else { - return Err(UnsupportedEditItem::NOT_ROOM_MESSAGE); - }; - let edit_info = - EditInfo { event_id: event_id.to_owned(), original_message: original_content.clone() }; - Ok(edit_info) - } - + /// Gives the information needed to edit an event from an event id. pub async fn get_edit_info_from_event_id( &self, event_id: &EventId, ) -> Result { if let Some(timeline_item) = self.item_by_event_id(event_id).await { - return self.get_edit_info_from_event_timeline_item(&timeline_item); + return timeline_item.get_edit_info(); } - let Ok(event) = self.room().event(event_id).await else { - return Err(UnsupportedEditItem::MISSING_EVENT); + let event = match self.room().event(event_id).await { + Ok(event) => event, + Err(error) => { + error!("Failed to fetch event with ID {event_id} with error: {error}"); + return Err(UnsupportedEditItem::MISSING_EVENT); + } }; let raw_sync_event: Raw = event.event.cast(); - let Ok(event) = raw_sync_event.deserialize_as::() else { - warn!("Unable to deserialize latest_event as an AnySyncTimelineEvent!"); - return Err(UnsupportedEditItem::DESERIALIZATION_ERROR); + let event = match raw_sync_event.deserialize() { + Ok(event) => event, + Err(error) => { + error!("Failed to deserialize event with ID {event_id} with error: {error}"); + return Err(UnsupportedEditItem::FAILED_TO_DESERIALIZE_EVENT); + } }; // Likely needs to be changed or renamed? @@ -548,8 +517,7 @@ impl Timeline { return Err(UnsupportedEditItem::NOT_ROOM_MESSAGE); }; - let edit_info = EditInfo { event_id: event_id.to_owned(), original_message: message }; - Ok(edit_info) + Ok(EditInfo { event_id: event_id.to_owned(), original_message: message }) } pub async fn edit_poll( diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs index eb024d567ed..ee5668f430f 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/edit.rs @@ -201,7 +201,7 @@ async fn test_send_edit() { .mount(&server) .await; - let edit_info = timeline.get_edit_info_from_event_timeline_item(&hello_world_item).unwrap(); + let edit_info = hello_world_item.get_edit_info().unwrap(); timeline .edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), edit_info) .await @@ -383,7 +383,7 @@ async fn test_send_reply_edit() { .mount(&server) .await; - let edit_info = timeline.get_edit_info_from_event_timeline_item(&reply_item).unwrap(); + let edit_info = reply_item.get_edit_info().unwrap(); timeline .edit(RoomMessageEventContentWithoutRelation::text_plain("Hello, Room!"), edit_info) .await diff --git a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs index 476bcca657e..d1de98cce52 100644 --- a/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs +++ b/crates/matrix-sdk-ui/tests/integration/timeline/replies.rs @@ -313,8 +313,7 @@ async fn test_send_reply() { .mount(&server) .await; - let replied_to_info = - timeline.get_replied_to_info_from_event_timeline_item(&event_from_bob).unwrap(); + let replied_to_info = event_from_bob.get_replied_to_info().unwrap(); timeline .send_reply( RoomMessageEventContentWithoutRelation::text_plain("Replying to Bob"), @@ -331,13 +330,6 @@ async fn test_send_reply() { assert_eq!(reply_message.body(), "Replying to Bob"); let in_reply_to = reply_message.in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id_from_bob); - // Right now, we don't pass along the replied-to event to the event handler, - // so it's not available if the timeline got cleared. Not critical, but - // there's notable room for improvement here. - // - // let replied_to_event = - // assert_matches!(&in_reply_to.event, TimelineDetails::Ready(ev) => ev); - // assert_eq!(replied_to_event.sender(), *BOB); let diff = timeout(timeline_stream.next(), Duration::from_secs(1)).await.unwrap().unwrap(); assert_let!(VectorDiff::Set { index: 0, value: reply_item_remote_echo } = diff); @@ -347,11 +339,6 @@ async fn test_send_reply() { assert_eq!(reply_message.body(), "Replying to Bob"); let in_reply_to = reply_message.in_reply_to().unwrap(); assert_eq!(in_reply_to.event_id, event_id_from_bob); - // Same as above. - // - // let replied_to_event = - // assert_matches!(&in_reply_to.event, TimelineDetails::Ready(ev) => - // ev); assert_eq!(replied_to_event.sender(), *BOB); server.verify().await; } @@ -388,7 +375,9 @@ async fn test_send_reply_with_event_id() { let _response = client.sync_once(sync_settings.clone()).await.unwrap(); server.reset().await; - assert_next_matches!(timeline_stream, VectorDiff::PushBack { .. }); + let event_from_bob = + assert_next_matches!(timeline_stream, VectorDiff::PushBack { value } => value); + assert_eq!(event_from_bob.event_id().unwrap(), event_id_from_bob); // Clear the timeline to make sure the old item does not need to be // available in it for the reply to work. @@ -548,8 +537,7 @@ async fn test_send_reply_to_self() { .mount(&server) .await; - let replied_to_info = - timeline.get_replied_to_info_from_event_timeline_item(&event_from_self).unwrap(); + let replied_to_info = event_from_self.get_replied_to_info().unwrap(); timeline .send_reply( RoomMessageEventContentWithoutRelation::text_plain("Replying to self"), @@ -637,8 +625,7 @@ async fn test_send_reply_to_threaded() { .mount(&server) .await; - let replied_to_info = - timeline.get_replied_to_info_from_event_timeline_item(&hello_world_item).unwrap(); + let replied_to_info = hello_world_item.get_replied_to_info().unwrap(); timeline .send_reply( RoomMessageEventContentWithoutRelation::text_plain("Hello, Bob!"), diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 7ce2cb8533e..6163de1c265 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -2687,8 +2687,13 @@ impl Room { /// room id, as identifier. pub async fn save_composer_draft(&self, draft: ComposerDraft) -> Result<()> { let room_id = self.room_id().to_owned(); - let data = StateStoreDataValue::ComposerDraft(draft); - self.client.store().set_kv_data(StateStoreDataKey::ComposerDraft(&room_id), data).await?; + self.client + .store() + .set_kv_data( + StateStoreDataKey::ComposerDraft(&room_id), + StateStoreDataValue::ComposerDraft(draft), + ) + .await?; Ok(()) }