Browse Source

room-list: Refactor and clean up

af/unable-to-decryt-styling
Kévin Commaille 12 months ago
parent
commit
a0fb433fd2
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 6
      src/components/dialogs/join_room.rs
  2. 4
      src/session/model/room/join_rule.rs
  3. 606
      src/session/model/room_list/mod.rs
  4. 2
      src/session/view/session_view.rs

6
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 {

4
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,
}

606
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<IndexMap<OwnedRoomId, Room>>,
pub(super) list: RefCell<IndexMap<OwnedRoomId, Room>>,
/// The list of rooms we are currently joining.
pub pending_rooms: RefCell<HashSet<OwnedRoomOrAliasId>>,
pub(super) pending_rooms: RefCell<HashSet<OwnedRoomOrAliasId>>,
/// The list of rooms that were upgraded and for which we haven't joined
/// the successor yet.
pub tombstoned_rooms: RefCell<HashSet<OwnedRoomId>>,
tombstoned_rooms: RefCell<HashSet<OwnedRoomId>>,
/// The current session.
#[property(get, construct_only)]
pub session: glib::WeakRef<Session>,
session: glib::WeakRef<Session>,
/// 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<imp::RoomList>)
@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<Room> {
self.list.borrow().get(room_id).cloned()
}
/// Get a snapshot of the rooms list.
pub fn snapshot(&self) -> Vec<Room> {
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<Room> {
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<Room> {
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::<HashMap<_, _>>();
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::<Vec<_>>();
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::<Room>, |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<Room> {
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<OwnedServerName>,
) -> Result<OwnedRoomId, String> {
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<imp::RoomList>)
@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<Room> {
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<Room> {
self.imp().get(room_id)
}
/// Get the room with the given identifier, if any.
pub(crate) fn get_by_identifier(&self, identifier: &RoomOrAliasId) -> Option<Room> {
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::<HashMap<_, _>>();
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::<Vec<_>>();
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::<Room>, |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<OwnedServerName>,
) -> Result<OwnedRoomId, String> {
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<Room> {
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<F: Fn(&Self) + 'static>(
&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<Room> {
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<Room> {
pub(crate) fn direct_chat(&self, user_id: &UserId) -> Option<Room> {
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<OwnedServerName>,
) -> Result<OwnedRoomId, String> {
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<F: Fn(&Self) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"pending-rooms-changed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}

2
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

Loading…
Cancel
Save