diff --git a/src/components/dialogs/join_room.rs b/src/components/dialogs/join_room.rs index ba9f4e9a..cbd79a5f 100644 --- a/src/components/dialogs/join_room.rs +++ b/src/components/dialogs/join_room.rs @@ -199,7 +199,11 @@ mod imp { let id = uri.id.clone(); self.uri.replace(Some(uri)); - if session.room_list().joined_room(&id).is_some() { + if session + .room_list() + .get_by_identifier(&id) + .is_some_and(|room| room.is_joined()) + { // Translators: This is a verb, as in 'View Room'. self.look_up_btn.set_content_label(gettext("View")); } else { diff --git a/src/session/model/room/join_rule.rs b/src/session/model/room/join_rule.rs index f4d570f7..e3bf4760 100644 --- a/src/session/model/room/join_rule.rs +++ b/src/session/model/room/join_rule.rs @@ -424,8 +424,8 @@ fn we_pass_restricted_allow_rule(room: &Room, rule: AllowRule) -> bool { match rule { AllowRule::RoomMembership(room_membership) => room.session().is_some_and(|s| { s.room_list() - .joined_room((&*room_membership.room_id).into()) - .is_some() + .get_by_identifier((&*room_membership.room_id).into()) + .is_some_and(|room| room.is_joined()) }), _ => false, } diff --git a/src/session/model/room_list/mod.rs b/src/session/model/room_list/mod.rs index 1dd03168..041f0cad 100644 --- a/src/session/model/room_list/mod.rs +++ b/src/session/model/room_list/mod.rs @@ -36,21 +36,18 @@ mod imp { #[properties(wrapper_type = super::RoomList)] pub struct RoomList { /// The list of rooms. - pub list: RefCell>, + pub(super) list: RefCell>, /// The list of rooms we are currently joining. - pub pending_rooms: RefCell>, + pub(super) pending_rooms: RefCell>, /// The list of rooms that were upgraded and for which we haven't joined /// the successor yet. - pub tombstoned_rooms: RefCell>, + tombstoned_rooms: RefCell>, /// The current session. #[property(get, construct_only)] - pub session: glib::WeakRef, + session: glib::WeakRef, /// The rooms metainfo that allow to restore this `RoomList` from its /// previous state. - /// - /// This is in a Mutex because updating the data in the store is async - /// and we don't want to overwrite newer data with older data. - pub metainfo: RoomListMetainfo, + metainfo: RoomListMetainfo, } #[glib::object_subclass] @@ -91,349 +88,348 @@ mod imp { .cloned() } } -} - -glib::wrapper! { - /// List of all joined rooms of the user. - /// - /// This is the parent ListModel of the sidebar from which all other models - /// are derived. - /// - /// The `RoomList` also takes care of all so called *pending rooms*, i.e. - /// rooms the user requested to join, but received no response from the - /// server yet. - pub struct RoomList(ObjectSubclass) - @implements gio::ListModel; -} -impl RoomList { - pub fn new(session: &Session) -> Self { - glib::Object::builder().property("session", session).build() - } + impl RoomList { + /// Get the room with the given room ID, if any. + pub(super) fn get(&self, room_id: &RoomId) -> Option { + self.list.borrow().get(room_id).cloned() + } - /// Get a snapshot of the rooms list. - pub fn snapshot(&self) -> Vec { - self.imp().list.borrow().values().cloned().collect() - } + /// Whether this list contains the room with the given ID. + fn contains(&self, room_id: &RoomId) -> bool { + self.list.borrow().contains_key(room_id) + } - /// Whether the room with the given identifier is pending. - pub fn is_pending_room(&self, identifier: &RoomOrAliasId) -> bool { - self.imp().pending_rooms.borrow().contains(identifier) - } + /// Remove the given room identifier from the pending rooms. + fn remove_pending_room(&self, identifier: &RoomOrAliasId) { + self.pending_rooms.borrow_mut().remove(identifier); + self.obj().emit_by_name::<()>("pending-rooms-changed", &[]); + } - fn pending_rooms_remove(&self, identifier: &RoomOrAliasId) { - self.imp().pending_rooms.borrow_mut().remove(identifier); - self.emit_by_name::<()>("pending-rooms-changed", &[]); - } + /// Add the given room identified to the pending rooms. + fn add_pending_room(&self, identifier: OwnedRoomOrAliasId) { + self.pending_rooms.borrow_mut().insert(identifier); + self.obj().emit_by_name::<()>("pending-rooms-changed", &[]); + } - fn pending_rooms_insert(&self, identifier: OwnedRoomOrAliasId) { - self.imp().pending_rooms.borrow_mut().insert(identifier); - self.emit_by_name::<()>("pending-rooms-changed", &[]); - } + /// Add a room that was tombstoned but for which we haven't joined the + /// successor yet. + pub(super) fn add_tombstoned_room(&self, room_id: OwnedRoomId) { + self.tombstoned_rooms.borrow_mut().insert(room_id); + } - fn pending_rooms_replace_or_remove(&self, identifier: &RoomOrAliasId, room_id: &RoomId) { - { - let mut pending_rooms = self.imp().pending_rooms.borrow_mut(); - pending_rooms.remove(identifier); - if !self.contains(room_id) { - pending_rooms.insert(room_id.to_owned().into()); + /// Remove the given room identifier from the pending rooms and replace + /// it with the given room ID if the room is not in the list yet. + fn remove_or_replace_pending_room(&self, identifier: &RoomOrAliasId, room_id: &RoomId) { + { + let mut pending_rooms = self.pending_rooms.borrow_mut(); + pending_rooms.remove(identifier); + if !self.contains(room_id) { + pending_rooms.insert(room_id.to_owned().into()); + } } + self.obj().emit_by_name::<()>("pending-rooms-changed", &[]); } - self.emit_by_name::<()>("pending-rooms-changed", &[]); - } - /// Get the room with the given room ID, if any. - pub fn get(&self, room_id: &RoomId) -> Option { - self.imp().list.borrow().get(room_id).cloned() - } + /// Handle when items were added to the list. + fn items_added(&self, added: usize) { + let position = { + let list = self.list.borrow(); - /// Get the room with the given identifier, if any. - pub fn get_by_identifier(&self, identifier: &RoomOrAliasId) -> Option { - match <&RoomId>::try_from(identifier) { - Ok(room_id) => self.get(room_id), - Err(room_alias) => { - let mut matches = self - .imp() - .list - .borrow() - .iter() - .filter(|(_, room)| { - let matrix_room = room.matrix_room(); - matrix_room.canonical_alias().as_deref() == Some(room_alias) - || matrix_room.alt_aliases().iter().any(|a| a == room_alias) - }) - .map(|(room_id, room)| (room_id.clone(), room.clone())) - .collect::>(); - - if matches.len() <= 1 { - return matches.into_values().next(); - } + let position = list.len().saturating_sub(added); - // The alias is shared between upgraded rooms. We want the latest room, so - // filter out those that are predecessors. - let predecessors = matches - .iter() - .filter_map(|(_, room)| room.predecessor_id().cloned()) - .collect::>(); - for room_id in predecessors { - matches.remove(&room_id); + let mut tombstoned_rooms_to_remove = Vec::new(); + for (_room_id, room) in list.iter().skip(position) { + room.connect_room_forgotten(clone!( + #[weak(rename_to = imp)] + self, + move |room| { + imp.remove(room.room_id()); + } + )); + + // Check if the new room is the successor to a tombstoned room. + if let Some(predecessor_id) = room.predecessor_id() { + if self.tombstoned_rooms.borrow().contains(predecessor_id) { + if let Some(room) = self.get(predecessor_id) { + room.update_successor(); + tombstoned_rooms_to_remove.push(predecessor_id.clone()); + } + } + } } - if matches.len() <= 1 { - return matches.into_values().next(); + if !tombstoned_rooms_to_remove.is_empty() { + let mut tombstoned_rooms = self.tombstoned_rooms.borrow_mut(); + for room_id in tombstoned_rooms_to_remove { + tombstoned_rooms.remove(&room_id); + } } - // Ideally this should not happen, return the one with the latest activity. - matches - .into_values() - .fold(None::, |latest_room, room| { - latest_room - .filter(|r| r.latest_activity() >= room.latest_activity()) - .or(Some(room)) - }) - } + position + }; + + self.obj().items_changed(position as u32, 0, added as u32); } - } - /// Wait till the room with the given ID becomes available. - pub async fn get_wait(&self, room_id: &RoomId) -> Option { - if let Some(room) = self.get(room_id) { - Some(room) - } else { - let (sender, receiver) = futures_channel::oneshot::channel(); - - let room_id = room_id.to_owned(); - let sender = Cell::new(Some(sender)); - // FIXME: add a timeout - let handler_id = self.connect_items_changed(move |obj, _, _, _| { - if let Some(room) = obj.get(&room_id) { - if let Some(sender) = sender.take() { - sender.send(Some(room)).unwrap(); - } - } - }); + /// Remove the room with the given ID. + fn remove(&self, room_id: &RoomId) { + let removed = { + let mut list = self.list.borrow_mut(); - let room = receiver.await.unwrap(); - self.disconnect(handler_id); - room + list.shift_remove_full(room_id) + }; + + self.tombstoned_rooms.borrow_mut().remove(room_id); + + if let Some((position, ..)) = removed { + self.obj().items_changed(position as u32, 1, 0); + } } - } - /// Whether this list contains the room with the given ID. - pub fn contains(&self, room_id: &RoomId) -> bool { - self.imp().list.borrow().contains_key(room_id) - } + /// Load the list of rooms from the `Store`. + pub(super) async fn load(&self) { + let rooms = self.metainfo.load_rooms().await; + let added = rooms.len(); + self.list.borrow_mut().extend(rooms); - /// Remove the room with the given ID. - pub fn remove(&self, room_id: &RoomId) { - let imp = self.imp(); + self.items_added(added); + } - let removed = { - let mut list = imp.list.borrow_mut(); + /// Handle room updates received via sync. + pub(super) fn handle_room_updates(&self, rooms: RoomUpdates) { + let Some(session) = self.session.upgrade() else { + return; + }; + let client = session.client(); + + let mut new_rooms = HashMap::new(); + + for (room_id, left_room) in rooms.leave { + let room = if let Some(room) = self.get(&room_id) { + room + } else if let Some(matrix_room) = client.get_room(&room_id) { + new_rooms + .entry(room_id.clone()) + .or_insert_with(|| Room::new(&session, matrix_room, None)) + .clone() + } else { + warn!("Could not find left room {room_id}"); + continue; + }; + + self.remove_pending_room((*room_id).into()); + room.handle_ambiguity_changes(left_room.ambiguity_changes.values()); + } - list.shift_remove_full(room_id) - }; + for (room_id, joined_room) in rooms.join { + let room = if let Some(room) = self.get(&room_id) { + room + } else if let Some(matrix_room) = client.get_room(&room_id) { + new_rooms + .entry(room_id.clone()) + .or_insert_with(|| Room::new(&session, matrix_room, None)) + .clone() + } else { + warn!("Could not find joined room {room_id}"); + continue; + }; + + self.remove_pending_room((*room_id).into()); + self.metainfo.watch_room(&room); + room.handle_ambiguity_changes(joined_room.ambiguity_changes.values()); + } - imp.tombstoned_rooms.borrow_mut().remove(room_id); + for (room_id, _invited_room) in rooms.invite { + let room = if let Some(room) = self.get(&room_id) { + room + } else if let Some(matrix_room) = client.get_room(&room_id) { + new_rooms + .entry(room_id.clone()) + .or_insert_with(|| Room::new(&session, matrix_room, None)) + .clone() + } else { + warn!("Could not find invited room {room_id}"); + continue; + }; + + self.remove_pending_room((*room_id).into()); + self.metainfo.watch_room(&room); + } - if let Some((position, ..)) = removed { - self.items_changed(position as u32, 1, 0); + if !new_rooms.is_empty() { + let added = new_rooms.len(); + self.list.borrow_mut().extend(new_rooms); + self.items_added(added); + } } - } - fn items_added(&self, added: usize) { - let position = { - let imp = self.imp(); - let list = imp.list.borrow(); + /// Join the room with the given identifier. + pub(super) async fn join_by_id_or_alias( + &self, + identifier: OwnedRoomOrAliasId, + via: Vec, + ) -> Result { + let Some(session) = self.session.upgrade() else { + return Err("Could not upgrade Session".to_owned()); + }; + let client = session.client(); + let identifier_clone = identifier.clone(); - let position = list.len().saturating_sub(added); + self.add_pending_room(identifier.clone()); - let mut tombstoned_rooms_to_remove = Vec::new(); - for (_room_id, room) in list.iter().skip(position) { - room.connect_room_forgotten(clone!( - #[weak(rename_to = obj)] - self, - move |room| { - obj.remove(room.room_id()); - } - )); - - // Check if the new room is the successor to a tombstoned room. - if let Some(predecessor_id) = room.predecessor_id() { - if imp.tombstoned_rooms.borrow().contains(predecessor_id) { - if let Some(room) = self.get(predecessor_id) { - room.update_successor(); - tombstoned_rooms_to_remove.push(predecessor_id.clone()); - } - } - } - } + let handle = spawn_tokio!(async move { + client + .join_room_by_id_or_alias(&identifier_clone, &via) + .await + }); - if !tombstoned_rooms_to_remove.is_empty() { - let mut tombstoned_rooms = imp.tombstoned_rooms.borrow_mut(); - for room_id in tombstoned_rooms_to_remove { - tombstoned_rooms.remove(&room_id); + match handle.await.expect("task was not aborted") { + Ok(matrix_room) => { + self.remove_or_replace_pending_room(&identifier, matrix_room.room_id()); + Ok(matrix_room.room_id().to_owned()) + } + Err(error) => { + self.remove_pending_room(&identifier); + error!("Joining room {identifier} failed: {error}"); + + let error = gettext_f( + // Translators: Do NOT translate the content between '{' and '}', this is a + // variable name. + "Could not join room {room_name}", + &[("room_name", identifier.as_str())], + ); + + Err(error) } } + } + } +} - position - }; +glib::wrapper! { + /// List of all rooms known by the user. + /// + /// This is the parent `GListModel` of the sidebar from which all other models + /// are derived. + /// + /// The `RoomList` also takes care of, so called *pending rooms*, i.e. + /// rooms the user requested to join, but received no response from the + /// server yet. + pub struct RoomList(ObjectSubclass) + @implements gio::ListModel; +} - self.items_changed(position as u32, 0, added as u32); +impl RoomList { + pub fn new(session: &Session) -> Self { + glib::Object::builder().property("session", session).build() } - /// Loads the state from the `Store`. - /// - /// Note that the `Store` currently doesn't store all events, therefore, we - /// aren't really loading much via this function. - pub async fn load(&self) { - let imp = self.imp(); + /// Load the list of rooms from the `Store`. + pub(crate) async fn load(&self) { + self.imp().load().await; + } - let rooms = imp.metainfo.load_rooms().await; - let added = rooms.len(); - imp.list.borrow_mut().extend(rooms); + /// Get a snapshot of the rooms list. + pub(crate) fn snapshot(&self) -> Vec { + self.imp().list.borrow().values().cloned().collect() + } - self.items_added(added); + /// Whether the room with the given identifier is pending. + pub(crate) fn is_pending_room(&self, identifier: &RoomOrAliasId) -> bool { + self.imp().pending_rooms.borrow().contains(identifier) } - pub fn handle_room_updates(&self, rooms: RoomUpdates) { - let Some(session) = self.session() else { - return; + /// Get the room with the given room ID, if any. + pub(crate) fn get(&self, room_id: &RoomId) -> Option { + self.imp().get(room_id) + } + + /// Get the room with the given identifier, if any. + pub(crate) fn get_by_identifier(&self, identifier: &RoomOrAliasId) -> Option { + let room_alias = match <&RoomId>::try_from(identifier) { + Ok(room_id) => return self.get(room_id), + Err(room_alias) => room_alias, }; - let imp = self.imp(); - let client = session.client(); - - let mut new_rooms = HashMap::new(); - - for (room_id, left_room) in rooms.leave { - let room = if let Some(room) = self.get(&room_id) { - room - } else if let Some(matrix_room) = client.get_room(&room_id) { - new_rooms - .entry(room_id.clone()) - .or_insert_with(|| Room::new(&session, matrix_room, None)) - .clone() - } else { - warn!("Could not find left room {room_id}"); - continue; - }; - self.pending_rooms_remove((*room_id).into()); - room.handle_ambiguity_changes(left_room.ambiguity_changes.values()); - } + let mut matches = self + .imp() + .list + .borrow() + .iter() + .filter(|(_, room)| { + // We don't want a room that is not joined, it might not be the proper room for + // the given alias anymore. + if !room.is_joined() { + return false; + } - for (room_id, joined_room) in rooms.join { - let room = if let Some(room) = self.get(&room_id) { - room - } else if let Some(matrix_room) = client.get_room(&room_id) { - new_rooms - .entry(room_id.clone()) - .or_insert_with(|| Room::new(&session, matrix_room, None)) - .clone() - } else { - warn!("Could not find joined room {room_id}"); - continue; - }; + let matrix_room = room.matrix_room(); + matrix_room.canonical_alias().as_deref() == Some(room_alias) + || matrix_room.alt_aliases().iter().any(|a| a == room_alias) + }) + .map(|(room_id, room)| (room_id.clone(), room.clone())) + .collect::>(); - self.pending_rooms_remove((*room_id).into()); - imp.metainfo.watch_room(&room); - room.handle_ambiguity_changes(joined_room.ambiguity_changes.values()); + if matches.len() <= 1 { + return matches.into_values().next(); } - for (room_id, _invited_room) in rooms.invite { - let room = if let Some(room) = self.get(&room_id) { - room - } else if let Some(matrix_room) = client.get_room(&room_id) { - new_rooms - .entry(room_id.clone()) - .or_insert_with(|| Room::new(&session, matrix_room, None)) - .clone() - } else { - warn!("Could not find invited room {room_id}"); - continue; - }; - - self.pending_rooms_remove((*room_id).into()); - imp.metainfo.watch_room(&room); + // The alias is shared between upgraded rooms. We want the latest room, so + // filter out those that are predecessors. + let predecessors = matches + .iter() + .filter_map(|(_, room)| room.predecessor_id().cloned()) + .collect::>(); + for room_id in predecessors { + matches.remove(&room_id); } - if !new_rooms.is_empty() { - let added = new_rooms.len(); - imp.list.borrow_mut().extend(new_rooms); - self.items_added(added); + if matches.len() <= 1 { + return matches.into_values().next(); } + + // Ideally this should not happen, return the one with the latest activity. + matches + .into_values() + .fold(None::, |latest_room, room| { + latest_room + .filter(|r| r.latest_activity() >= room.latest_activity()) + .or(Some(room)) + }) } - /// Join the room with the given identifier. - pub async fn join_by_id_or_alias( - &self, - identifier: OwnedRoomOrAliasId, - via: Vec, - ) -> Result { - let Some(session) = self.session() else { - return Err("Could not upgrade Session".to_owned()); - }; - let client = session.client(); - let identifier_clone = identifier.clone(); + /// Wait till the room with the given ID becomes available. + pub(crate) async fn get_wait(&self, room_id: &RoomId) -> Option { + if let Some(room) = self.get(room_id) { + return Some(room); + } - self.pending_rooms_insert(identifier.clone()); + let (sender, receiver) = futures_channel::oneshot::channel(); - let handle = spawn_tokio!(async move { - client - .join_room_by_id_or_alias(&identifier_clone, &via) - .await + let room_id = room_id.to_owned(); + let sender = Cell::new(Some(sender)); + // FIXME: add a timeout + let handler_id = self.connect_items_changed(move |obj, _, _, _| { + if let Some(room) = obj.get(&room_id) { + if let Some(sender) = sender.take() { + let _ = sender.send(Some(room)); + } + } }); - match handle.await.unwrap() { - Ok(matrix_room) => { - self.pending_rooms_replace_or_remove(&identifier, matrix_room.room_id()); - Ok(matrix_room.room_id().to_owned()) - } - Err(error) => { - self.pending_rooms_remove(&identifier); - error!("Joining room {identifier} failed: {error}"); - - let error = gettext_f( - // Translators: Do NOT translate the content between '{' and '}', this is a - // variable name. - "Could not join room {room_name}", - &[("room_name", identifier.as_str())], - ); - - Err(error) - } - } - } + let room = receiver.await.ok().flatten(); - pub fn connect_pending_rooms_changed( - &self, - f: F, - ) -> glib::SignalHandlerId { - self.connect_closure( - "pending-rooms-changed", - true, - closure_local!(move |obj: Self| { - f(&obj); - }), - ) - } + self.disconnect(handler_id); - /// Get the room with the given identifier, if it is joined. - pub fn joined_room(&self, identifier: &RoomOrAliasId) -> Option { - self.get_by_identifier(identifier).filter(Room::is_joined) - } - - /// Add a room that was tombstoned but for which we haven't joined the - /// successor yet. - pub fn add_tombstoned_room(&self, room_id: OwnedRoomId) { - self.imp().tombstoned_rooms.borrow_mut().insert(room_id); + room } /// Get the joined room that is a direct chat with the user with the given /// ID. /// /// If several rooms are found, returns the room with the latest activity. - pub fn direct_chat(&self, user_id: &UserId) -> Option { + pub(crate) fn direct_chat(&self, user_id: &UserId) -> Option { self.imp() .list .borrow() @@ -446,4 +442,38 @@ impl RoomList { .max_by(|x, y| x.latest_activity().cmp(&y.latest_activity())) .cloned() } + + /// Add a room that was tombstoned but for which we haven't joined the + /// successor yet. + pub(crate) fn add_tombstoned_room(&self, room_id: OwnedRoomId) { + self.imp().add_tombstoned_room(room_id); + } + + /// Handle room updates received via sync. + pub(crate) fn handle_room_updates(&self, rooms: RoomUpdates) { + self.imp().handle_room_updates(rooms); + } + + /// Join the room with the given identifier. + pub(crate) async fn join_by_id_or_alias( + &self, + identifier: OwnedRoomOrAliasId, + via: Vec, + ) -> Result { + self.imp().join_by_id_or_alias(identifier, via).await + } + + /// Connect to the signal emitted when the pending rooms changed. + pub fn connect_pending_rooms_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "pending-rooms-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } } diff --git a/src/session/view/session_view.rs b/src/session/view/session_view.rs index 4924be86..f627d26a 100644 --- a/src/session/view/session_view.rs +++ b/src/session/view/session_view.rs @@ -312,7 +312,7 @@ mod imp { pub(super) fn select_room_if_exists(&self, identifier: &RoomOrAliasId) -> bool { if let Some(room) = self .room_list() - .and_then(|room_list| room_list.joined_room(identifier)) + .and_then(|room_list| room_list.get_by_identifier(identifier)) { self.select_room(room); true