diff --git a/Cargo.lock b/Cargo.lock index a67ea70..b333862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1522,6 +1522,7 @@ dependencies = [ "css-color-parser", "dirs", "emojis", + "futures", "gethostname 0.4.1", "html5ever", "image", diff --git a/Cargo.toml b/Cargo.toml index 802737b..fc941da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ comrak = {version = "0.18.0", features = ["shortcodes"]} css-color-parser = "0.1.2" dirs = "4.0.0" emojis = "~0.5.2" +futures = "0.3" gethostname = "0.4.1" html5ever = "0.26.0" image = "0.24.5" diff --git a/src/base.rs b/src/base.rs index 25ecd7f..3266f44 100644 --- a/src/base.rs +++ b/src/base.rs @@ -21,7 +21,7 @@ use url::Url; use matrix_sdk::{ encryption::verification::SasVerification, - room::Joined, + room::{Joined, Room as MatrixRoom}, ruma::{ events::{ reaction::ReactionEvent, @@ -644,6 +644,13 @@ fn emoji_map() -> CompletionMap { return emojis; } +#[derive(Default)] +pub struct SyncInfo { + pub spaces: Vec, + pub rooms: Vec)>>, + pub dms: Vec)>>, +} + pub struct ChatStore { pub cmds: ProgramCommands, pub worker: Requester, @@ -654,6 +661,7 @@ pub struct ChatStore { pub settings: ApplicationSettings, pub need_load: HashSet, pub emojis: CompletionMap, + pub sync_info: SyncInfo, } impl ChatStore { @@ -663,12 +671,14 @@ impl ChatStore { settings, cmds: crate::commands::setup_commands(), + emojis: emoji_map(), + names: Default::default(), rooms: Default::default(), presences: Default::default(), verifications: Default::default(), need_load: Default::default(), - emojis: emoji_map(), + sync_info: Default::default(), } } diff --git a/src/main.rs b/src/main.rs index d18cbad..6bb97fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -712,6 +712,7 @@ fn main() -> IambResult<()> { let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() + .worker_threads(2) .thread_name_fn(|| { static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); diff --git a/src/windows/mod.rs b/src/windows/mod.rs index 872d464..0e4fbb9 100644 --- a/src/windows/mod.rs +++ b/src/windows/mod.rs @@ -1,4 +1,6 @@ use std::cmp::{Ord, Ordering, PartialOrd}; +use std::ops::Deref; +use std::sync::Arc; use std::time::{Duration, Instant}; use matrix_sdk::{ @@ -10,7 +12,6 @@ use matrix_sdk::{ OwnedRoomId, RoomId, }, - DisplayName, }; use modalkit::tui::{ @@ -78,6 +79,8 @@ use self::{room::RoomState, welcome::WelcomeState}; pub mod room; pub mod welcome; +type MatrixRoomInfo = Arc<(MatrixRoom, Option)>; + const MEMBER_FETCH_DEBOUNCE: Duration = Duration::from_secs(5); #[inline] @@ -380,10 +383,13 @@ impl WindowOps for IambWindow { match self { IambWindow::Room(state) => state.draw(area, buf, focused, store), IambWindow::DirectList(state) => { - let dms = store.application.worker.direct_messages(); - let mut items = dms + let mut items = store + .application + .sync_info + .dms + .clone() .into_iter() - .map(|(id, name, tags)| DirectItem::new(id, name, tags, store)) + .map(|room_info| DirectItem::new(room_info, store)) .collect::>(); items.sort(); @@ -416,10 +422,13 @@ impl WindowOps for IambWindow { .render(area, buf, state); }, IambWindow::RoomList(state) => { - let joined = store.application.worker.active_rooms(); - let mut items = joined + let mut items = store + .application + .sync_info + .rooms + .clone() .into_iter() - .map(|(room, name, tags)| RoomItem::new(room, name, tags, store)) + .map(|room_info| RoomItem::new(room_info, store)) .collect::>(); items.sort(); @@ -432,9 +441,13 @@ impl WindowOps for IambWindow { .render(area, buf, state); }, IambWindow::SpaceList(state) => { - let spaces = store.application.worker.spaces(); - let items = - spaces.into_iter().map(|(room, name)| SpaceItem::new(room, name, store)); + let items = store + .application + .sync_info + .spaces + .clone() + .into_iter() + .map(|room| SpaceItem::new(room, store)); state.set(items.collect()); state.draw(area, buf, focused, store); @@ -639,36 +652,45 @@ impl Window for IambWindow { #[derive(Clone)] pub struct RoomItem { - room: MatrixRoom, - tags: Option, + room_info: MatrixRoomInfo, name: String, } impl RoomItem { - fn new( - room: MatrixRoom, - name: DisplayName, - tags: Option, - store: &mut ProgramStore, - ) -> Self { - let name = name.to_string(); + fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self { + let room = &room_info.deref().0; let room_id = room.room_id(); let info = store.application.get_room_info(room_id.to_owned()); - info.name = name.clone().into(); - info.tags = tags.clone(); + let name = info.name.clone().unwrap_or_default(); + info.tags = room_info.deref().1.clone(); if let Some(alias) = room.canonical_alias() { store.application.names.insert(alias.to_string(), room_id.to_owned()); } - RoomItem { room, tags, name } + RoomItem { room_info, name } + } + + #[inline] + fn room(&self) -> &MatrixRoom { + &self.room_info.deref().0 + } + + #[inline] + fn room_id(&self) -> &RoomId { + self.room().room_id() + } + + #[inline] + fn tags(&self) -> &Option { + &self.room_info.deref().1 } } impl PartialEq for RoomItem { fn eq(&self, other: &Self) -> bool { - self.room.room_id() == other.room.room_id() + self.room_id() == other.room_id() } } @@ -676,7 +698,7 @@ impl Eq for RoomItem {} impl Ord for RoomItem { fn cmp(&self, other: &Self) -> Ordering { - tag_cmp(&self.tags, &other.tags).then_with(|| room_cmp(&self.room, &other.room)) + tag_cmp(self.tags(), other.tags()).then_with(|| room_cmp(self.room(), other.room())) } } @@ -694,7 +716,7 @@ impl ToString for RoomItem { impl ListItem for RoomItem { fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { - if let Some(tags) = &self.tags { + if let Some(tags) = &self.tags() { let style = selected_style(selected); let mut spans = vec![Span::styled(self.name.as_str(), style)]; @@ -707,7 +729,7 @@ impl ListItem for RoomItem { } fn get_word(&self) -> Option { - self.room.room_id().to_string().into() + self.room_id().to_string().into() } } @@ -718,29 +740,37 @@ impl Promptable for RoomItem { ctx: &ProgramContext, _: &mut ProgramStore, ) -> EditResult, IambInfo> { - room_prompt(self.room.room_id(), act, ctx) + room_prompt(self.room_id(), act, ctx) } } #[derive(Clone)] pub struct DirectItem { - room: MatrixRoom, - tags: Option, + room_info: MatrixRoomInfo, name: String, } impl DirectItem { - fn new( - room: MatrixRoom, - name: DisplayName, - tags: Option, - store: &mut ProgramStore, - ) -> Self { - let name = name.to_string(); + fn new(room_info: MatrixRoomInfo, store: &mut ProgramStore) -> Self { + let room_id = room_info.deref().0.room_id().to_owned(); + let name = store.application.get_room_info(room_id).name.clone().unwrap_or_default(); - store.application.set_room_name(room.room_id(), name.as_str()); + DirectItem { room_info, name } + } - DirectItem { room, tags, name } + #[inline] + fn room(&self) -> &MatrixRoom { + &self.room_info.deref().0 + } + + #[inline] + fn room_id(&self) -> &RoomId { + self.room().room_id() + } + + #[inline] + fn tags(&self) -> &Option { + &self.room_info.deref().1 } } @@ -752,7 +782,7 @@ impl ToString for DirectItem { impl ListItem for DirectItem { fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { - if let Some(tags) = &self.tags { + if let Some(tags) = &self.tags() { let style = selected_style(selected); let mut spans = vec![Span::styled(self.name.as_str(), style)]; @@ -765,13 +795,13 @@ impl ListItem for DirectItem { } fn get_word(&self) -> Option { - self.room.room_id().to_string().into() + self.room_id().to_string().into() } } impl PartialEq for DirectItem { fn eq(&self, other: &Self) -> bool { - self.room.room_id() == other.room.room_id() + self.room_id() == other.room_id() } } @@ -779,7 +809,7 @@ impl Eq for DirectItem {} impl Ord for DirectItem { fn cmp(&self, other: &Self) -> Ordering { - tag_cmp(&self.tags, &other.tags).then_with(|| room_cmp(&self.room, &other.room)) + tag_cmp(self.tags(), other.tags()).then_with(|| room_cmp(self.room(), other.room())) } } @@ -796,7 +826,7 @@ impl Promptable for DirectItem { ctx: &ProgramContext, _: &mut ProgramStore, ) -> EditResult, IambInfo> { - room_prompt(self.room.room_id(), act, ctx) + room_prompt(self.room_id(), act, ctx) } } @@ -807,11 +837,14 @@ pub struct SpaceItem { } impl SpaceItem { - fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { - let name = name.to_string(); + fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self { let room_id = room.room_id(); - - store.application.set_room_name(room_id, name.as_str()); + let name = store + .application + .get_room_info(room_id.to_owned()) + .name + .clone() + .unwrap_or_default(); if let Some(alias) = room.canonical_alias() { store.application.names.insert(alias.to_string(), room_id.to_owned()); diff --git a/src/windows/room/space.rs b/src/windows/room/space.rs index 39278f1..4ea4f93 100644 --- a/src/windows/room/space.rs +++ b/src/windows/room/space.rs @@ -120,11 +120,12 @@ impl<'a> StatefulWidget for Space<'a> { let items = members .into_iter() .filter_map(|id| { - let (room, name, tags) = + let (room, _, tags) = self.store.application.worker.get_room(id.clone()).ok()?; + let room_info = std::sync::Arc::new((room, tags)); if id != state.room_id { - Some(RoomItem::new(room, name, tags, self.store)) + Some(RoomItem::new(room_info, self.store)) } else { None } diff --git a/src/worker.rs b/src/worker.rs index 7be7088..85e2441 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -8,6 +8,7 @@ use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::Arc; use std::time::{Duration, Instant}; +use futures::{stream::FuturesUnordered, StreamExt}; use gethostname::gethostname; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::task::JoinHandle; @@ -260,19 +261,122 @@ async fn load_insert(room_id: OwnedRoomId, res: MessageFetchResult, store: Async } } -async fn load_older(client: &Client, store: &AsyncProgramStore) { +async fn load_older(client: &Client, store: &AsyncProgramStore) -> usize { let limit = MIN_MSG_LOAD; - let plan = load_plan(store).await; // Fetch each room separately, so they don't block each other. - for (room_id, fetch_id) in plan.into_iter() { - let client = client.clone(); - let store = store.clone(); + load_plan(store) + .await + .into_iter() + .map(|(room_id, fetch_id)| { + let client = client.clone(); + let store = store.clone(); - tokio::spawn(async move { - let res = load_older_one(client, room_id.as_ref(), fetch_id, limit).await; - load_insert(room_id, res, store).await; - }); + async move { + let res = load_older_one(client, room_id.as_ref(), fetch_id, limit).await; + load_insert(room_id, res, store).await; + } + }) + .collect::>() + .count() + .await +} + +async fn load_older_forever(client: &Client, store: &AsyncProgramStore) { + // Load older messages every 2 seconds. + let mut interval = tokio::time::interval(Duration::from_secs(2)); + + loop { + interval.tick().await; + load_older(client, store).await; + } +} + +async fn refresh_rooms(client: &Client, store: &AsyncProgramStore) { + let mut names = vec![]; + + let mut spaces = vec![]; + let mut rooms = vec![]; + let mut dms = vec![]; + + for room in client.invited_rooms().into_iter() { + let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string(); + names.push((room.room_id().to_owned(), name)); + + if room.is_direct() { + let tags = room.tags().await.unwrap_or_default(); + + dms.push(Arc::new((room.into(), tags))); + } else if room.is_space() { + spaces.push(room.into()); + } else { + let tags = room.tags().await.unwrap_or_default(); + + rooms.push(Arc::new((room.into(), tags))); + } + } + + for room in client.joined_rooms().into_iter() { + let name = room.display_name().await.unwrap_or(DisplayName::Empty).to_string(); + names.push((room.room_id().to_owned(), name)); + + if room.is_direct() { + let tags = room.tags().await.unwrap_or_default(); + + dms.push(Arc::new((room.into(), tags))); + } else if room.is_space() { + spaces.push(room.into()); + } else { + let tags = room.tags().await.unwrap_or_default(); + + rooms.push(Arc::new((room.into(), tags))); + } + } + + let mut locked = store.lock().await; + locked.application.sync_info.spaces = spaces; + locked.application.sync_info.rooms = rooms; + locked.application.sync_info.dms = dms; + + for (room_id, name) in names { + locked.application.set_room_name(&room_id, &name); + } +} + +async fn refresh_rooms_forever(client: &Client, store: &AsyncProgramStore) { + let mut interval = tokio::time::interval(Duration::from_secs(5)); + + loop { + interval.tick().await; + + refresh_rooms(client, store).await; + } +} + +async fn refresh_receipts_forever(client: &Client, store: &AsyncProgramStore) { + // Update the displayed read receipts every 5 seconds. + let mut interval = tokio::time::interval(Duration::from_secs(5)); + let mut sent = HashMap::::default(); + + loop { + interval.tick().await; + let receipts = update_receipts(client).await; + let read = store.lock().await.application.set_receipts(receipts).await; + + for (room_id, read_till) in read.into_iter() { + if let Some(read_sent) = sent.get(&room_id) { + if read_sent == &read_till { + // Skip unchanged receipts. + continue; + } + } + + if let Some(room) = client.get_joined_room(&room_id) { + if room.read_receipt(&read_till).await.is_ok() { + sent.insert(room_id, read_till); + } + } + } } } @@ -331,8 +435,6 @@ async fn update_receipts(client: &Client) -> Vec<(OwnedRoomId, Receipts)> { pub type FetchedRoom = (MatrixRoom, DisplayName, Option); pub enum WorkerTask { - ActiveRooms(ClientReply>), - DirectMessages(ClientReply>), Init(AsyncProgramStore, ClientReply<()>), Login(LoginStyle, ClientReply>), GetInviter(Invited, ClientReply>>), @@ -340,7 +442,6 @@ pub enum WorkerTask { JoinRoom(String, ClientReply>), Members(OwnedRoomId, ClientReply>>), SpaceMembers(OwnedRoomId, ClientReply>>), - Spaces(ClientReply>), TypingNotice(OwnedRoomId), Verify(VerifyAction, SasVerification, ClientReply>), VerifyRequest(OwnedUserId, ClientReply>), @@ -349,14 +450,6 @@ pub enum WorkerTask { impl Debug for WorkerTask { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { match self { - WorkerTask::ActiveRooms(_) => { - f.debug_tuple("WorkerTask::ActiveRooms").field(&format_args!("_")).finish() - }, - WorkerTask::DirectMessages(_) => { - f.debug_tuple("WorkerTask::DirectMessages") - .field(&format_args!("_")) - .finish() - }, WorkerTask::Init(_, _) => { f.debug_tuple("WorkerTask::Init") .field(&format_args!("_")) @@ -396,9 +489,6 @@ impl Debug for WorkerTask { .field(&format_args!("_")) .finish() }, - WorkerTask::Spaces(_) => { - f.debug_tuple("WorkerTask::Spaces").field(&format_args!("_")).finish() - }, WorkerTask::TypingNotice(room_id) => { f.debug_tuple("WorkerTask::TypingNotice").field(room_id).finish() }, @@ -442,14 +532,6 @@ impl Requester { return response.recv(); } - pub fn direct_messages(&self) -> Vec { - let (reply, response) = oneshot(); - - self.tx.send(WorkerTask::DirectMessages(reply)).unwrap(); - - return response.recv(); - } - pub fn get_inviter(&self, invite: Invited) -> IambResult> { let (reply, response) = oneshot(); @@ -474,14 +556,6 @@ impl Requester { return response.recv(); } - pub fn active_rooms(&self) -> Vec { - let (reply, response) = oneshot(); - - self.tx.send(WorkerTask::ActiveRooms(reply)).unwrap(); - - return response.recv(); - } - pub fn members(&self, room_id: OwnedRoomId) -> IambResult> { let (reply, response) = oneshot(); @@ -498,14 +572,6 @@ impl Requester { return response.recv(); } - pub fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> { - let (reply, response) = oneshot(); - - self.tx.send(WorkerTask::Spaces(reply)).unwrap(); - - return response.recv(); - } - pub fn typing_notice(&self, room_id: OwnedRoomId) { self.tx.send(WorkerTask::TypingNotice(room_id)).unwrap(); } @@ -532,7 +598,6 @@ pub struct ClientWorker { settings: ApplicationSettings, client: Client, load_handle: Option>, - rcpt_handle: Option>, sync_handle: Option>, } @@ -571,7 +636,6 @@ impl ClientWorker { settings, client: client.clone(), load_handle: None, - rcpt_handle: None, sync_handle: None, }; @@ -597,18 +661,10 @@ impl ClientWorker { if let Some(handle) = self.sync_handle.take() { handle.abort(); } - - if let Some(handle) = self.rcpt_handle.take() { - handle.abort(); - } } async fn run(&mut self, task: WorkerTask) { match task { - WorkerTask::DirectMessages(reply) => { - assert!(self.initialized); - reply.send(self.direct_messages().await); - }, WorkerTask::Init(store, reply) => { assert_eq!(self.initialized, false); self.init(store).await; @@ -626,10 +682,6 @@ impl ClientWorker { assert!(self.initialized); reply.send(self.get_room(room_id).await); }, - WorkerTask::ActiveRooms(reply) => { - assert!(self.initialized); - reply.send(self.active_rooms().await); - }, WorkerTask::Login(style, reply) => { assert!(self.initialized); reply.send(self.login_and_sync(style).await); @@ -642,10 +694,6 @@ impl ClientWorker { assert!(self.initialized); reply.send(self.space_members(space).await); }, - WorkerTask::Spaces(reply) => { - assert!(self.initialized); - reply.send(self.spaces().await); - }, WorkerTask::TypingNotice(room_id) => { assert!(self.initialized); self.typing_notice(room_id).await; @@ -903,50 +951,14 @@ impl ClientWorker { }, ); - self.rcpt_handle = tokio::spawn({ - let store = store.clone(); - let client = self.client.clone(); - let mut sent = HashMap::::default(); - - async move { - // Update the displayed read receipts every 5 seconds. - let mut interval = tokio::time::interval(Duration::from_secs(5)); - - loop { - interval.tick().await; - - let receipts = update_receipts(&client).await; - let read = store.lock().await.application.set_receipts(receipts).await; - - for (room_id, read_till) in read.into_iter() { - if let Some(read_sent) = sent.get(&room_id) { - if read_sent == &read_till { - // Skip unchanged receipts. - continue; - } - } - - if let Some(room) = client.get_joined_room(&room_id) { - if room.read_receipt(&read_till).await.is_ok() { - sent.insert(room_id, read_till); - } - } - } - } - } - }) - .into(); - self.load_handle = tokio::spawn({ let client = self.client.clone(); async move { - // Load older messages every 2 seconds. - let mut interval = tokio::time::interval(Duration::from_secs(2)); - loop { - interval.tick().await; - load_older(&client, &store).await; - } + let load = load_older_forever(&client, &store); + let rcpt = refresh_receipts_forever(&client, &store); + let room = refresh_rooms_forever(&client, &store); + let ((), (), ()) = tokio::join!(load, rcpt, room); } }) .into(); @@ -1002,31 +1014,30 @@ impl ClientWorker { Ok(Some(InfoMessage::from("Successfully logged in!"))) } - async fn direct_message(&mut self, user: OwnedUserId) -> IambResult { - for (room, name, tags) in self.direct_messages().await { + async fn direct_message(&mut self, user: OwnedUserId) -> IambResult { + for room in self.client.rooms() { + if !room.is_direct() { + continue; + } + if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() { - return Ok((room, name, tags)); + return Ok(room.room_id().to_owned()); } } let rt = CreateRoomType::Direct(user.clone()); let flags = CreateRoomFlags::ENCRYPTED; - match create_room(&self.client, None, rt, flags).await { - Ok(room_id) => self.get_room(room_id).await, - Err(e) => { - error!( - user_id = user.as_str(), - err = e.to_string(), - "Failed to create direct message room" - ); + create_room(&self.client, None, rt, flags).await.map_err(|e| { + error!( + user_id = user.as_str(), + err = e.to_string(), + "Failed to create direct message room" + ); - let msg = format!("Could not open a room with {user}"); - let err = UIError::Failure(msg); - - Err(err) - }, - } + let msg = format!("Could not open a room with {user}"); + UIError::Failure(msg) + }) } async fn get_inviter(&mut self, invited: Invited) -> IambResult> { @@ -1058,9 +1069,7 @@ impl ClientWorker { }, } } else if let Ok(user) = OwnedUserId::try_from(name.as_str()) { - let room = self.direct_message(user).await?.0; - - return Ok(room.room_id().to_owned()); + self.direct_message(user).await } else { let msg = format!("{:?} is not a valid room or user name", name.as_str()); let err = UIError::Failure(msg); @@ -1069,62 +1078,6 @@ impl ClientWorker { } } - async fn direct_messages(&self) -> Vec { - let mut rooms = vec![]; - - for room in self.client.invited_rooms().into_iter() { - if !room.is_direct() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - let tags = room.tags().await.unwrap_or_default(); - - rooms.push((room.into(), name, tags)); - } - - for room in self.client.joined_rooms().into_iter() { - if !room.is_direct() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - let tags = room.tags().await.unwrap_or_default(); - - rooms.push((room.into(), name, tags)); - } - - return rooms; - } - - async fn active_rooms(&self) -> Vec { - let mut rooms = vec![]; - - for room in self.client.invited_rooms().into_iter() { - if room.is_space() || room.is_direct() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - let tags = room.tags().await.unwrap_or_default(); - - rooms.push((room.into(), name, tags)); - } - - for room in self.client.joined_rooms().into_iter() { - if room.is_space() || room.is_direct() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - let tags = room.tags().await.unwrap_or_default(); - - rooms.push((room.into(), name, tags)); - } - - return rooms; - } - async fn members(&mut self, room_id: OwnedRoomId) -> IambResult> { if let Some(room) = self.client.get_room(room_id.as_ref()) { Ok(room.active_members().await.map_err(IambError::from)?) @@ -1145,32 +1098,6 @@ impl ClientWorker { Ok(rooms) } - async fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> { - let mut spaces = vec![]; - - for room in self.client.invited_rooms().into_iter() { - if !room.is_space() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - - spaces.push((room.into(), name)); - } - - for room in self.client.joined_rooms().into_iter() { - if !room.is_space() { - continue; - } - - let name = room.display_name().await.unwrap_or(DisplayName::Empty); - - spaces.push((room.into(), name)); - } - - return spaces; - } - async fn typing_notice(&mut self, room_id: OwnedRoomId) { if let Some(room) = self.client.get_joined_room(room_id.as_ref()) { let _ = room.typing_notice(true).await;