From d57d3c4124df97e4c8b8d9c196ee70bac51cee8f Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Mon, 28 Jul 2025 16:43:57 +0200 Subject: [PATCH] feat(sdk): save the unsubscribed status in the store, and use it to return something more precise than unknown when fetching a subscription --- bindings/matrix-sdk-ffi/src/room/mod.rs | 29 ++++---- crates/matrix-sdk/src/room/mod.rs | 66 ++++++++++++++----- .../tests/integration/room/thread.rs | 12 +++- labs/multiverse/src/widgets/room_view/mod.rs | 12 +++- 4 files changed, 88 insertions(+), 31 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/room/mod.rs b/bindings/matrix-sdk-ffi/src/room/mod.rs index 6ef33ad61..2a9e46774 100644 --- a/bindings/matrix-sdk-ffi/src/room/mod.rs +++ b/bindings/matrix-sdk-ffi/src/room/mod.rs @@ -1129,22 +1129,29 @@ impl Room { pub async fn fetch_thread_subscription( &self, thread_root_event_id: String, - ) -> Result, ClientError> { + ) -> Result, ClientError> { let thread_root = EventId::parse(thread_root_event_id)?; - Ok(self - .inner - .fetch_thread_subscription(thread_root) - .await? - .map(|sub| ThreadSubscription { automatic: sub.automatic })) + Ok(self.inner.fetch_thread_subscription(thread_root).await?.map(|sub| match sub { + matrix_sdk::room::ThreadStatus::Subscribed { automatic } => { + ThreadStatus::Subscribed { automatic } + } + matrix_sdk::room::ThreadStatus::Unsubscribed => ThreadStatus::Unsubscribed, + })) } } /// Status of a thread subscription (MSC4306). -#[derive(uniffi::Record)] -pub struct ThreadSubscription { - /// Whether the thread subscription happened automatically (e.g. after a - /// mention) or if it was manually requested by the user. - automatic: bool, +#[derive(uniffi::Enum)] +pub enum ThreadStatus { + /// The thread is subscribed to. + Subscribed { + /// Whether the thread subscription happened automatically (e.g. after a + /// mention) or if it was manually requested by the user. + automatic: bool, + }, + + /// The thread is not subscribed to. + Unsubscribed, } /// A listener for receiving new live location shares in a room. diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index 052fec5be..9db41fa10 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -34,6 +34,7 @@ use http::StatusCode; pub use identity_status_changes::IdentityStatusChanges; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::crypto::{IdentityStatusChange, RoomIdentityProvider, UserIdentity}; +pub use matrix_sdk_base::store::ThreadStatus; #[cfg(feature = "e2e-encryption")] use matrix_sdk_base::{crypto::RoomEventDecryptionResult, deserialized_responses::EncryptionInfo}; use matrix_sdk_base::{ @@ -3657,10 +3658,21 @@ impl Room { self.client .send(subscribe_thread::unstable::Request::new( self.room_id().to_owned(), - thread_root, + thread_root.clone(), automatic, )) .await?; + + // Immediately save the result into the database. + self.client + .state_store() + .upsert_thread_subscription( + self.room_id(), + &thread_root, + ThreadStatus::Subscribed { automatic }, + ) + .await?; + Ok(()) } @@ -3679,9 +3691,16 @@ impl Room { self.client .send(unsubscribe_thread::unstable::Request::new( self.room_id().to_owned(), - thread_root, + thread_root.clone(), )) .await?; + + // Immediately save the result into the database. + self.client + .state_store() + .upsert_thread_subscription(self.room_id(), &thread_root, ThreadStatus::Unsubscribed) + .await?; + Ok(()) } @@ -3695,8 +3714,8 @@ impl Room { /// /// # Returns /// - /// - An `Ok` result with `Some(ThreadSubscription)` if the subscription - /// exists. + /// - An `Ok` result with `Some(ThreadStatus)` if we have some subscription + /// information. /// - An `Ok` result with `None` if the subscription does not exist, or the /// event couldn't be found, or the event isn't a thread. /// - An error if the request fails for any other reason, such as a network @@ -3704,33 +3723,48 @@ impl Room { pub async fn fetch_thread_subscription( &self, thread_root: OwnedEventId, - ) -> Result> { + ) -> Result> { let result = self .client .send(get_thread_subscription::unstable::Request::new( self.room_id().to_owned(), - thread_root, + thread_root.clone(), )) .await; match result { - Ok(response) => Ok(Some(ThreadSubscription { automatic: response.automatic })), + Ok(response) => Ok(Some(ThreadStatus::Subscribed { automatic: response.automatic })), Err(http_error) => match http_error.as_client_api_error() { - Some(error) if error.status_code == StatusCode::NOT_FOUND => Ok(None), + Some(error) if error.status_code == StatusCode::NOT_FOUND => { + // At this point the server returned no subscriptions, which can mean that the + // endpoint doesn't exist (not enabled/implemented yet on the server), or that + // the thread doesn't exist, or that the user has unsubscribed from it + // previously. + // + // If we had any information about prior unsubscription, we can use it here to + // return something slightly more precise than what the server returned. + let stored_status = self + .client + .state_store() + .load_thread_subscription(self.room_id(), &thread_root) + .await?; + + if let Some(ThreadStatus::Unsubscribed) = stored_status { + // The thread was unsubscribed from before, so maintain this information. + Ok(Some(ThreadStatus::Unsubscribed)) + } else { + // We either have stale information (the thread was marked as subscribed + // to, but the server said it wasn't), or we didn't have any information. + // Return unknown. + Ok(None) + } + } _ => Err(http_error.into()), }, } } } -/// Status of a thread subscription. -#[derive(Debug, Clone, Copy)] -pub struct ThreadSubscription { - /// Whether the subscription was made automatically by a client, not by - /// manual user choice. - pub automatic: bool, -} - #[cfg(feature = "e2e-encryption")] impl RoomIdentityProvider for Room { fn is_member<'a>(&'a self, user_id: &'a UserId) -> BoxFuture<'a, bool> { diff --git a/crates/matrix-sdk/tests/integration/room/thread.rs b/crates/matrix-sdk/tests/integration/room/thread.rs index 043818cf7..cd5edc00a 100644 --- a/crates/matrix-sdk/tests/integration/room/thread.rs +++ b/crates/matrix-sdk/tests/integration/room/thread.rs @@ -1,4 +1,5 @@ -use matrix_sdk::test_utils::mocks::MatrixMockServer; +use assert_matches2::assert_matches; +use matrix_sdk::{room::ThreadStatus, test_utils::mocks::MatrixMockServer}; use matrix_sdk_test::async_test; use ruma::{owned_event_id, room_id}; @@ -35,7 +36,7 @@ async fn test_subscribe_thread() { // I can get the subscription status for that same thread. let subscription = room.fetch_thread_subscription(root_id.clone()).await.unwrap().unwrap(); - assert!(subscription.automatic); + assert_matches!(subscription, ThreadStatus::Subscribed { automatic: true }); // If I try to get a subscription for a thread event that's unknown, I get no // `ThreadSubscription`, not an error. @@ -53,5 +54,10 @@ async fn test_subscribe_thread() { .mount() .await; - room.unsubscribe_thread(root_id).await.unwrap(); + room.unsubscribe_thread(root_id.clone()).await.unwrap(); + + // Now, if I retry to get the subscription status for this thread, it's + // unsubscribed. + let subscription = room.fetch_thread_subscription(root_id).await.unwrap(); + assert_matches!(subscription, Some(ThreadStatus::Unsubscribed)); } diff --git a/labs/multiverse/src/widgets/room_view/mod.rs b/labs/multiverse/src/widgets/room_view/mod.rs index bacb16d5b..8366f58d6 100644 --- a/labs/multiverse/src/widgets/room_view/mod.rs +++ b/labs/multiverse/src/widgets/room_view/mod.rs @@ -13,6 +13,7 @@ use matrix_sdk::{ api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::RoomMessageEventContent, }, + store::ThreadStatus, }; use matrix_sdk_ui::{ Timeline, @@ -525,7 +526,16 @@ impl RoomView { Ok(Some(subscription)) => { status_handle.set_message(format!( "Thread subscription status: {}", - if subscription.automatic { "automatic" } else { "manual" } + match subscription { + ThreadStatus::Subscribed { automatic } => { + if automatic { + "subscribed (automatic)" + } else { + "subscribed (manual)" + } + } + ThreadStatus::Unsubscribed => "unsubscribed", + } )); } Ok(None) => {