diff --git a/Cargo.lock b/Cargo.lock index ede4f63de92..f1f201ef2d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4483,7 +4483,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.12.5" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "assign", "js_int", @@ -4499,7 +4499,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.20.4" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "as_variant", "assign", @@ -4522,7 +4522,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.15.4" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "as_variant", "base64", @@ -4555,7 +4555,7 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.30.4" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "as_variant", "indexmap", @@ -4581,7 +4581,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" version = "0.11.2" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "headers", "http", @@ -4599,7 +4599,7 @@ dependencies = [ [[package]] name = "ruma-html" version = "0.4.1" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "as_variant", "html5ever", @@ -4610,7 +4610,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.10.1" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "js_int", "thiserror 2.0.11", @@ -4619,7 +4619,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.15.2" -source = "git+https://github.com/ruma/ruma?rev=de19ebaf71af620eb17abaefd92e43153f9d041d#de19ebaf71af620eb17abaefd92e43153f9d041d" +source = "git+https://github.com/ruma/ruma?rev=184d4f85b201bc1e932a8344d78575e826f31efa#184d4f85b201bc1e932a8344d78575e826f31efa" dependencies = [ "cfg-if", "proc-macro-crate", diff --git a/Cargo.toml b/Cargo.toml index 5435d5d427e..4e99c8d19ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,7 @@ proptest = { version = "1.6.0", default-features = false, features = ["std"] } rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false } rmp-serde = "1.3.0" -ruma = { git = "https://github.com/ruma/ruma", rev = "de19ebaf71af620eb17abaefd92e43153f9d041d", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "184d4f85b201bc1e932a8344d78575e826f31efa", features = [ "client-api-c", "compat-upload-signatures", "compat-arbitrary-length-ids", @@ -76,8 +76,9 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "de19ebaf71af620eb17abaefd9 "unstable-msc4171", "unstable-msc4278", "unstable-msc4286", + "unstable-msc4306" ] } -ruma-common = { git = "https://github.com/ruma/ruma", rev = "de19ebaf71af620eb17abaefd92e43153f9d041d" } +ruma-common = { git = "https://github.com/ruma/ruma", rev = "184d4f85b201bc1e932a8344d78575e826f31efa" } sentry = "0.36.0" sentry-tracing = "0.36.0" serde = { version = "1.0.217", features = ["rc"] } diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 3780de83476..1bd59bd3a5b 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. ### Features: +- Add experimental support for + [MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the + `Room::fetch_thread_subscription()` and `Room::set_thread_subscription()` methods. + ([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442)) - [**breaking**] [`GalleryUploadParameters::reply`] and [`UploadParameters::reply`] have been both replaced with a new optional `in_reply_to` field, that's a string which will be parsed into an `OwnedEventId` when sending the event. The thread relationship will be automatically filled in, diff --git a/bindings/matrix-sdk-ffi/src/room/mod.rs b/bindings/matrix-sdk-ffi/src/room/mod.rs index 1b10853619d..3f26f1a8d5a 100644 --- a/bindings/matrix-sdk-ffi/src/room/mod.rs +++ b/bindings/matrix-sdk-ffi/src/room/mod.rs @@ -1139,6 +1139,59 @@ impl Room { Ok(Arc::new(RoomPreview::new(AsyncRuntimeDropped::new(client), room_preview))) } + + /// Toggle a MSC4306 subscription to a thread in this room, based on the + /// thread root event id. + /// + /// If `subscribed` is `true`, it will subscribe to the thread, with a + /// precision that the subscription was manually requested by the user + /// (i.e. not automatic). + /// + /// If the thread was already subscribed to (resp. unsubscribed from), while + /// trying to subscribe to it (resp. unsubscribe from it), it will do + /// nothing, i.e. subscribing (resp. unsubscribing) to a thread is an + /// idempotent operation. + pub async fn set_thread_subscription( + &self, + thread_root_event_id: String, + subscribed: bool, + ) -> Result<(), ClientError> { + let thread_root = EventId::parse(thread_root_event_id)?; + if subscribed { + // This is a manual subscription. + let automatic = false; + self.inner.subscribe_thread(thread_root, automatic).await?; + } else { + self.inner.unsubscribe_thread(thread_root).await?; + } + Ok(()) + } + + /// Return the current MSC4306 thread subscription for the given thread root + /// in this room. + /// + /// Returns `None` if the thread doesn't exist, or isn't subscribed to, or + /// the server can't handle MSC4306; otherwise, returns the thread + /// subscription status. + pub async fn fetch_thread_subscription( + &self, + thread_root_event_id: String, + ) -> 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 })) + } +} + +/// 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, } /// A listener for receiving new live location shares in a room. diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 41cd9117c31..478d76611ee 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -8,6 +8,11 @@ All notable changes to this project will be documented in this file. ### Features +- Add experimental support for + [MSC4306](https://github.com/matrix-org/matrix-spec-proposals/pull/4306), with the + `Room::fetch_thread_subscription()`, `Room::subscribe_thread()` and `Room::unsubscribe_thread()` + methods. + ([#5442](https://github.com/matrix-org/matrix-rust-sdk/pull/5442)) - [**breaking**] `RoomMemberRole` has a new `Creator` variant, that differentiates room creators with infinite power levels, as introduced in room version 12. diff --git a/crates/matrix-sdk/src/room/mod.rs b/crates/matrix-sdk/src/room/mod.rs index a6d5e0ed9c2..027e08cd2f1 100644 --- a/crates/matrix-sdk/src/room/mod.rs +++ b/crates/matrix-sdk/src/room/mod.rs @@ -78,8 +78,9 @@ use ruma::{ receipt::create_receipt, redact::redact_event, room::{get_room_event, report_content, report_room}, - state::{get_state_events_for_key, send_state_event}, + state::{get_state_event_for_key, send_state_event}, tag::{create_tag, delete_tag}, + threads::{get_thread_subscription, subscribe_thread, unsubscribe_thread}, typing::create_typing_event::{self, v3::Typing}, }, assign, @@ -814,7 +815,7 @@ impl Room { .encryption_state_deduplicated_handler .run(self.room_id().to_owned(), async move { // Request the event from the server. - let request = get_state_events_for_key::v3::Request::new( + let request = get_state_event_for_key::v3::Request::new( self.room_id().to_owned(), StateEventType::RoomEncryption, "".to_owned(), @@ -822,7 +823,7 @@ impl Room { let response = match self.client.send(request).await { Ok(response) => Some( response - .content + .into_content() .deserialize_as_unchecked::()?, ), Err(err) if err.client_api_error_kind() == Some(&ErrorKind::NotFound) => None, @@ -3490,7 +3491,7 @@ impl Room { pub async fn load_pinned_events(&self) -> Result>> { let response = self .client - .send(get_state_events_for_key::v3::Request::new( + .send(get_state_event_for_key::v3::Request::new( self.room_id().to_owned(), StateEventType::RoomPinnedEvents, "".to_owned(), @@ -3499,7 +3500,10 @@ impl Room { match response { Ok(response) => Ok(Some( - response.content.deserialize_as_unchecked::()?.pinned, + response + .into_content() + .deserialize_as_unchecked::()? + .pinned, )), Err(http_error) => match http_error.as_client_api_error() { Some(error) if error.status_code == StatusCode::NOT_FOUND => Ok(None), @@ -3708,6 +3712,101 @@ impl Room { ) -> Result { opts.send(self, event_id).await } + + /// Subscribe to a given thread in this room. + /// + /// This will subscribe the user to the thread, so that they will receive + /// notifications for that thread specifically. + /// + /// # Arguments + /// + /// - `thread_root`: The ID of the thread root event to subscribe to. + /// - `automatic`: Whether the subscription was made automatically by a + /// client, not by manual user choice. If there was a previous automatic + /// subscription, and that's set to `true` (i.e. we're now subscribing + /// manually), the subscription will be overridden to a manual one + /// instead. + /// + /// # Returns + /// + /// - A 404 error if the event isn't known, or isn't a thread root. + /// - An `Ok` result if the subscription was successful. + pub async fn subscribe_thread(&self, thread_root: OwnedEventId, automatic: bool) -> Result<()> { + self.client + .send(subscribe_thread::unstable::Request::new( + self.room_id().to_owned(), + thread_root, + automatic, + )) + .await?; + Ok(()) + } + + /// Unsubscribe from a given thread in this room. + /// + /// # Arguments + /// + /// - `thread_root`: The ID of the thread root event to unsubscribe to. + /// + /// # Returns + /// + /// - An `Ok` result if the unsubscription was successful, or the thread was + /// already unsubscribed. + /// - A 404 error if the event isn't known, or isn't a thread root. + pub async fn unsubscribe_thread(&self, thread_root: OwnedEventId) -> Result<()> { + self.client + .send(unsubscribe_thread::unstable::Request::new( + self.room_id().to_owned(), + thread_root, + )) + .await?; + Ok(()) + } + + /// Return the current thread subscription for the given thread root in this + /// room. + /// + /// # Arguments + /// + /// - `thread_root`: The ID of the thread root event to get the subscription + /// for. + /// + /// # Returns + /// + /// - An `Ok` result with `Some(ThreadSubscription)` if the subscription + /// exists. + /// - 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 + /// error. + pub async fn fetch_thread_subscription( + &self, + thread_root: OwnedEventId, + ) -> Result> { + let result = self + .client + .send(get_thread_subscription::unstable::Request::new( + self.room_id().to_owned(), + thread_root, + )) + .await; + + match result { + Ok(response) => Ok(Some(ThreadSubscription { automatic: response.automatic })), + Err(http_error) => match http_error.as_client_api_error() { + Some(error) if error.status_code == StatusCode::NOT_FOUND => 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")] diff --git a/crates/matrix-sdk/src/test_utils/mocks/mod.rs b/crates/matrix-sdk/src/test_utils/mocks/mod.rs index 5ca2554cb35..7da6475acc0 100644 --- a/crates/matrix-sdk/src/test_utils/mocks/mod.rs +++ b/crates/matrix-sdk/src/test_utils/mocks/mod.rs @@ -1335,6 +1335,32 @@ impl MatrixMockServer { .and(query_param("animated", animated.to_string())); self.mock_endpoint(mock, AuthedMediaThumbnailEndpoint).expect_default_access_token() } + + /// Create a prebuilt mock for the endpoint used to get a thread + /// subscription in a given room. + pub fn mock_get_thread_subscription(&self) -> MockEndpoint<'_, GetThreadSubscriptionEndpoint> { + let mock = Mock::given(method("GET")); + self.mock_endpoint(mock, GetThreadSubscriptionEndpoint::default()) + .expect_default_access_token() + } + + /// Create a prebuilt mock for the endpoint used to define a thread + /// subscription in a given room. + pub fn mock_put_thread_subscription(&self) -> MockEndpoint<'_, PutThreadSubscriptionEndpoint> { + let mock = Mock::given(method("PUT")); + self.mock_endpoint(mock, PutThreadSubscriptionEndpoint::default()) + .expect_default_access_token() + } + + /// Create a prebuilt mock for the endpoint used to delete a thread + /// subscription in a given room. + pub fn mock_delete_thread_subscription( + &self, + ) -> MockEndpoint<'_, DeleteThreadSubscriptionEndpoint> { + let mock = Mock::given(method("DELETE")); + self.mock_endpoint(mock, DeleteThreadSubscriptionEndpoint::default()) + .expect_default_access_token() + } } /// Parameter to [`MatrixMockServer::sync_room`]. @@ -3753,3 +3779,121 @@ impl<'a> MockEndpoint<'a, JoinRoomEndpoint> { }))) } } + +#[derive(Default)] +struct ThreadSubscriptionMatchers { + /// Optional room id to match in the query. + room_id: Option, + /// Optional thread root event id to match in the query. + thread_root: Option, +} + +impl ThreadSubscriptionMatchers { + /// Match the request parameter against a specific room id. + fn match_room_id(mut self, room_id: OwnedRoomId) -> Self { + self.room_id = Some(room_id); + self + } + + /// Match the request parameter against a specific thread root event id. + fn match_thread_id(mut self, thread_root: OwnedEventId) -> Self { + self.thread_root = Some(thread_root); + self + } + + /// Compute the final URI for the thread subscription endpoint. + fn endpoint_regexp_uri(&self) -> String { + if self.room_id.is_some() || self.thread_root.is_some() { + format!( + "^/_matrix/client/unstable/io.element.msc4306/rooms/{}/thread/{}/subscription$", + self.room_id.as_deref().map(|s| s.as_str()).unwrap_or(".*"), + self.thread_root.as_deref().map(|s| s.as_str()).unwrap_or(".*").replace("$", "\\$") + ) + } else { + "^/_matrix/client/unstable/io.element.msc4306/rooms/.*/thread/.*/subscription$" + .to_owned() + } + } +} + +/// A prebuilt mock for `GET +/// /client/*/rooms/{room_id}/threads/{thread_root}/subscription` +#[derive(Default)] +pub struct GetThreadSubscriptionEndpoint { + matchers: ThreadSubscriptionMatchers, +} + +impl<'a> MockEndpoint<'a, GetThreadSubscriptionEndpoint> { + /// Returns a successful response for the given thread subscription. + pub fn ok(mut self, automatic: bool) -> MatrixMock<'a> { + self.mock = self.mock.and(path_regex(self.endpoint.matchers.endpoint_regexp_uri())); + self.respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "automatic": automatic + }))) + } + + /// Match the request parameter against a specific room id. + pub fn match_room_id(mut self, room_id: OwnedRoomId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_room_id(room_id); + self + } + /// Match the request parameter against a specific thread root event id. + pub fn match_thread_id(mut self, thread_root: OwnedEventId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_thread_id(thread_root); + self + } +} + +/// A prebuilt mock for `PUT +/// /client/*/rooms/{room_id}/threads/{thread_root}/subscription` +#[derive(Default)] +pub struct PutThreadSubscriptionEndpoint { + matchers: ThreadSubscriptionMatchers, +} + +impl<'a> MockEndpoint<'a, PutThreadSubscriptionEndpoint> { + /// Returns a successful response for the given setting of thread + /// subscription. + pub fn ok(mut self) -> MatrixMock<'a> { + self.mock = self.mock.and(path_regex(self.endpoint.matchers.endpoint_regexp_uri())); + self.respond_with(ResponseTemplate::new(200)) + } + + /// Match the request parameter against a specific room id. + pub fn match_room_id(mut self, room_id: OwnedRoomId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_room_id(room_id); + self + } + /// Match the request parameter against a specific thread root event id. + pub fn match_thread_id(mut self, thread_root: OwnedEventId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_thread_id(thread_root); + self + } +} + +/// A prebuilt mock for `DELETE +/// /client/*/rooms/{room_id}/threads/{thread_root}/subscription` +#[derive(Default)] +pub struct DeleteThreadSubscriptionEndpoint { + matchers: ThreadSubscriptionMatchers, +} + +impl<'a> MockEndpoint<'a, DeleteThreadSubscriptionEndpoint> { + /// Returns a successful response for the deletion of a given thread + /// subscription. + pub fn ok(mut self) -> MatrixMock<'a> { + self.mock = self.mock.and(path_regex(self.endpoint.matchers.endpoint_regexp_uri())); + self.respond_with(ResponseTemplate::new(200)) + } + + /// Match the request parameter against a specific room id. + pub fn match_room_id(mut self, room_id: OwnedRoomId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_room_id(room_id); + self + } + /// Match the request parameter against a specific thread root event id. + pub fn match_thread_id(mut self, thread_root: OwnedEventId) -> Self { + self.endpoint.matchers = self.endpoint.matchers.match_thread_id(thread_root); + self + } +} diff --git a/crates/matrix-sdk/tests/integration/room/mod.rs b/crates/matrix-sdk/tests/integration/room/mod.rs index a5e3813b7fd..7232afabe3c 100644 --- a/crates/matrix-sdk/tests/integration/room/mod.rs +++ b/crates/matrix-sdk/tests/integration/room/mod.rs @@ -7,3 +7,4 @@ mod left; mod notification_mode; mod spaces; mod tags; +mod thread; diff --git a/crates/matrix-sdk/tests/integration/room/thread.rs b/crates/matrix-sdk/tests/integration/room/thread.rs new file mode 100644 index 00000000000..043818cf70c --- /dev/null +++ b/crates/matrix-sdk/tests/integration/room/thread.rs @@ -0,0 +1,57 @@ +use matrix_sdk::test_utils::mocks::MatrixMockServer; +use matrix_sdk_test::async_test; +use ruma::{owned_event_id, room_id}; + +#[async_test] +async fn test_subscribe_thread() { + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + let room_id = room_id!("!test:example.org"); + let room = server.sync_joined_room(&client, room_id).await; + + let root_id = owned_event_id!("$root"); + + server + .mock_put_thread_subscription() + .match_room_id(room_id.to_owned()) + .match_thread_id(root_id.clone()) + .ok() + .mock_once() + .mount() + .await; + + // I can subscribe to a thread. + room.subscribe_thread(root_id.clone(), true).await.unwrap(); + + server + .mock_get_thread_subscription() + .match_room_id(room_id.to_owned()) + .match_thread_id(root_id.clone()) + .ok(true) + .mock_once() + .mount() + .await; + + // 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); + + // If I try to get a subscription for a thread event that's unknown, I get no + // `ThreadSubscription`, not an error. + let subscription = + room.fetch_thread_subscription(owned_event_id!("$another_root")).await.unwrap(); + assert!(subscription.is_none()); + + // I can also unsubscribe from a thread. + server + .mock_delete_thread_subscription() + .match_room_id(room_id.to_owned()) + .match_thread_id(root_id.clone()) + .ok() + .mock_once() + .mount() + .await; + + room.unsubscribe_thread(root_id).await.unwrap(); +} diff --git a/labs/multiverse/src/widgets/room_view/input.rs b/labs/multiverse/src/widgets/room_view/input.rs index f4d4631c9f1..fcb8cf8284f 100644 --- a/labs/multiverse/src/widgets/room_view/input.rs +++ b/labs/multiverse/src/widgets/room_view/input.rs @@ -16,6 +16,8 @@ struct Cli { pub enum Command { Invite { user_id: String }, Leave, + Subscribe, + Unsubscribe, } pub enum MessageOrCommand { diff --git a/labs/multiverse/src/widgets/room_view/mod.rs b/labs/multiverse/src/widgets/room_view/mod.rs index a22b478edf7..bacb16d5bfa 100644 --- a/labs/multiverse/src/widgets/room_view/mod.rs +++ b/labs/multiverse/src/widgets/room_view/mod.rs @@ -9,7 +9,8 @@ use matrix_sdk::{ Client, Room, RoomState, locks::Mutex, ruma::{ - OwnedRoomId, RoomId, UserId, api::client::receipt::create_receipt::v3::ReceiptType, + OwnedEventId, OwnedRoomId, RoomId, UserId, + api::client::receipt::create_receipt::v3::ReceiptType, events::room::message::RoomMessageEventContent, }, }; @@ -52,6 +53,7 @@ enum TimelineKind { Thread { room: OwnedRoomId, + thread_root: OwnedEventId, /// The threaded-focused timeline for this thread. timeline: Arc>>, /// Items in the thread timeline (to avoid recomputing them every single @@ -145,10 +147,11 @@ impl RoomView { let i = items.clone(); let t = thread_timeline.clone(); let root = root_event_id; + let cloned_root = root.clone(); let r = room.clone(); let task = spawn(async move { let timeline = TimelineBuilder::new(&r) - .with_focus(TimelineFocus::Thread { root_event_id: root.clone() }) + .with_focus(TimelineFocus::Thread { root_event_id: cloned_root }) .track_read_marker_and_receipts() .build() .await @@ -171,6 +174,7 @@ impl RoomView { self.timeline_list.unselect(); self.kind = TimelineKind::Thread { + thread_root: root, room: room.room_id().to_owned(), timeline: thread_timeline, items, @@ -225,6 +229,12 @@ impl RoomView { self.switch_to_room_timeline(None); } + // Pressing 'Alt+s' on a threaded timeline will print the current + // subscription status. + (KeyModifiers::ALT, Char('s')) => { + self.print_thread_subscription_status().await; + } + (KeyModifiers::CONTROL, Char('l')) => { self.toggle_reaction_to_latest_msg().await } @@ -477,10 +487,67 @@ impl RoomView { self.input.clear(); } + async fn subscribe_thread(&mut self) { + if let TimelineKind::Thread { thread_root, .. } = &self.kind { + self.call_with_room(async |room, status_handle| { + if let Err(err) = room.subscribe_thread(thread_root.clone(), false).await { + status_handle.set_message(format!("error when subscribing to a thread: {err}")); + } else { + status_handle.set_message("Subscribed to thread!".to_owned()); + } + }) + .await; + + self.input.clear(); + } + } + + async fn unsubscribe_thread(&mut self) { + if let TimelineKind::Thread { thread_root, .. } = &self.kind { + self.call_with_room(async |room, status_handle| { + if let Err(err) = room.unsubscribe_thread(thread_root.clone()).await { + status_handle + .set_message(format!("error when unsubscribing to a thread: {err}")); + } else { + status_handle.set_message("Unsubscribed from thread!".to_owned()); + } + }) + .await; + + self.input.clear(); + } + } + + async fn print_thread_subscription_status(&mut self) { + if let TimelineKind::Thread { thread_root, .. } = &self.kind { + self.call_with_room(async |room, status_handle| { + match room.fetch_thread_subscription(thread_root.clone()).await { + Ok(Some(subscription)) => { + status_handle.set_message(format!( + "Thread subscription status: {}", + if subscription.automatic { "automatic" } else { "manual" } + )); + } + Ok(None) => { + status_handle + .set_message("Thread is not subscribed or does not exist".to_owned()); + } + Err(err) => { + status_handle + .set_message(format!("Error getting thread subscription: {err}")); + } + } + }) + .await; + } + } + async fn handle_command(&mut self, command: input::Command) { match command { input::Command::Invite { user_id } => self.invite_member(&user_id).await, input::Command::Leave => self.leave_room().await, + input::Command::Subscribe => self.subscribe_thread().await, + input::Command::Unsubscribe => self.unsubscribe_thread().await, } } diff --git a/labs/multiverse/src/widgets/status.rs b/labs/multiverse/src/widgets/status.rs index 362650a2c34..d025f90fd49 100644 --- a/labs/multiverse/src/widgets/status.rs +++ b/labs/multiverse/src/widgets/status.rs @@ -89,7 +89,7 @@ impl Status { let status_message = status_message.clone(); async move { - // Clear the status message in 4 seconds. + // Clear the status message after the standard duration. sleep(MESSAGE_DURATION).await; status_message.lock().take(); }