feat(sdk): save the unsubscribed status in the store, and use it to return something more precise than unknown when fetching a subscription

This commit is contained in:
Benjamin Bouvier
2025-07-28 16:43:57 +02:00
parent 1a5cb2beb8
commit d57d3c4124
4 changed files with 88 additions and 31 deletions
+18 -11
View File
@@ -1129,22 +1129,29 @@ impl Room {
pub async fn fetch_thread_subscription(
&self,
thread_root_event_id: String,
) -> Result<Option<ThreadSubscription>, ClientError> {
) -> Result<Option<ThreadStatus>, 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.
+50 -16
View File
@@ -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<Option<ThreadSubscription>> {
) -> Result<Option<ThreadStatus>> {
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> {
@@ -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));
}
+11 -1
View File
@@ -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) => {