diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 98ffdf288..16454f3ba 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -123,16 +123,16 @@ pub enum RoomsListUpdate { /// The Html-formatted text preview of the latest message. latest_message_text: String, }, - /// Update the number of unread messages for the given room. + /// Update the number of unread messages and mentions for the given room. UpdateNumUnreadMessages { room_id: OwnedRoomId, - count: UnreadMessageCount, + unread_messages: UnreadMessageCount, unread_mentions: u64, }, /// Update the displayable name for the given room. UpdateRoomName { room_id: OwnedRoomId, - new_room_name: String, + new_room_name: Option, }, /// Update the avatar (image) for the given room. UpdateRoomAvatar { @@ -449,9 +449,9 @@ impl RoomsList { error!("Error: couldn't find room {room_id} to update latest event"); } } - RoomsListUpdate::UpdateNumUnreadMessages { room_id, count , unread_mentions} => { + RoomsListUpdate::UpdateNumUnreadMessages { room_id, unread_messages, unread_mentions } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { - (room.num_unread_messages, room.num_unread_mentions) = match count { + (room.num_unread_messages, room.num_unread_mentions) = match unread_messages { UnreadMessageCount::Unknown => (0, 0), UnreadMessageCount::Known(count) => (count, unread_mentions), }; @@ -462,7 +462,7 @@ impl RoomsList { RoomsListUpdate::UpdateRoomName { room_id, new_room_name } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { let was_displayed = (self.display_filter)(room); - room.room_name = Some(new_room_name); + room.room_name = new_room_name; let should_display = (self.display_filter)(room); match (was_displayed, should_display) { // No need to update the displayed rooms list. diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 856782e5f..0e5640a92 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -1,6 +1,6 @@ //! The RoomInputBar widget contains all components related to sending messages/content to a room. //! -//! The RoomInputBar must be capped to a maximum height of 75% of the containing RoomScreen's height. +//! The RoomInputBar is capped to a maximum height of 62.5% of the containing RoomScreen's height. //! //! The widgets included in the RoomInputBar are: //! * a preview of the message the user is replying to. diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 209f137f3..2d1f3fd1a 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -11,14 +11,15 @@ use matrix_sdk::{ api::client::{profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType}, events::{ room::{ message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource - }, FullStateEventContent, MessageLikeEventType, StateEventType + }, MessageLikeEventType, StateEventType }, matrix_uri::MatrixId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomOrAliasId, UserId - }, sliding_sync::VersionBuilder, Client, ClientBuildError, Error, OwnedServerName, Room, RoomMemberships, RoomState, SuccessorRoom + }, sliding_sync::VersionBuilder, Client, ClientBuildError, Error, OwnedServerName, Room, RoomDisplayName, RoomMemberships, RoomState, SuccessorRoom }; use matrix_sdk_ui::{ - room_list_service::{RoomListItem, RoomListLoadingState, SyncIndicator}, sync_service::{self, SyncService}, timeline::{AnyOtherFullStateEventContent, EventTimelineItem, MembershipChange, RoomExt, TimelineEventItemId, TimelineItem, TimelineItemContent}, RoomListService, Timeline + room_list_service::{RoomListItem, RoomListLoadingState, SyncIndicator}, sync_service::{self, SyncService}, timeline::{EventTimelineItem, LatestEventValue, RoomExt, TimelineDetails, TimelineEventItemId, TimelineItem}, RoomListService, Timeline }; use robius_open::Uri; +use ruma::events::tag::Tags; use tokio::{ runtime::Handle, sync::{mpsc::{Receiver, Sender, UnboundedReceiver, UnboundedSender}, watch, Notify}, task::JoinHandle, time::error::Elapsed, @@ -845,8 +846,8 @@ async fn async_worker( } enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { room_id: room_id.clone(), - count: UnreadMessageCount::Known(timeline.room().num_unread_messages()), - unread_mentions:timeline.room().num_unread_mentions(), + unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), + unread_mentions: timeline.room().num_unread_mentions(), }); }); } @@ -1009,7 +1010,7 @@ async fn async_worker( // Update the rooms list with new unread counts enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { room_id: room_id_clone.clone(), - count: UnreadMessageCount::Known(unread_count), + unread_messages: UnreadMessageCount::Known(unread_count), unread_mentions, }); } @@ -1189,7 +1190,7 @@ async fn async_worker( // Also update the number of unread messages in the room. enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { room_id: room_id.clone(), - count: UnreadMessageCount::Known(timeline.room().num_unread_messages()), + unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), unread_mentions: timeline.room().num_unread_mentions() }); }); @@ -1214,7 +1215,7 @@ async fn async_worker( // Also update the number of unread messages in the room. enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { room_id: room_id.clone(), - count: UnreadMessageCount::Known(timeline.room().num_unread_messages()), + unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), unread_mentions: timeline.room().num_unread_mentions() }); }); @@ -1224,7 +1225,7 @@ async fn async_worker( let (timeline, sender) = { let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); let Some(room_info) = all_joined_rooms.get(&room_id) else { - log!("BUG: room info not found for fetch members request {room_id}"); + log!("BUG: room info not found for get room power levels request {room_id}"); continue; }; @@ -1734,23 +1735,43 @@ fn username_to_full_user_id( /// /// This struct is necessary in order for us to track the previous state /// of a room received from the room list service, so that we can -/// determine if the room has changed state. +/// determine what room data has changed since the last update. /// We can't just store the `matrix_sdk::Room` object itself, /// because that is a shallow reference to an inner room object within /// the room list service. #[derive(Clone)] struct RoomListServiceRoomInfo { room_id: OwnedRoomId, - room_state: RoomState, + state: RoomState, is_direct: bool, + is_tombstoned: bool, + tags: Option, + user_power_levels: Option, + // latest_event_timestamp: Option, + num_unread_messages: u64, + num_unread_mentions: u64, + display_name: Option, + room_avatar: Option, room: matrix_sdk::Room, } impl RoomListServiceRoomInfo { async fn from_room(room: matrix_sdk::Room) -> Self { Self { room_id: room.room_id().to_owned(), - room_state: room.state(), + state: room.state(), is_direct: room.is_direct().await.unwrap_or(false), + is_tombstoned: room.is_tombstoned(), + tags: room.tags().await.ok().flatten(), + user_power_levels: if let Some(user_id) = current_user_id() { + UserPowerLevels::from_room(&room, &user_id).await + } else { + None + }, + // latest_event_timestamp: room.new_latest_event_timestamp(), + num_unread_messages: room.num_unread_messages(), + num_unread_mentions: room.num_unread_mentions(), + display_name: room.display_name().await.ok(), + room_avatar: room.avatar_url(), room, } } @@ -1917,8 +1938,9 @@ async fn async_main_loop( let _num_new_rooms = new_rooms.len(); if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Append {_num_new_rooms}"); } for new_room in new_rooms { + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner()).await; add_new_room(&new_room, &room_list_service).await?; - all_known_rooms.push_back(RoomListServiceRoomInfo::from_room(new_room.into_inner()).await); + all_known_rooms.push_back(new_room); } } VectorDiff::Clear => { @@ -1929,13 +1951,15 @@ async fn async_main_loop( } VectorDiff::PushFront { value: new_room } => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushFront"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner()).await; add_new_room(&new_room, &room_list_service).await?; - all_known_rooms.push_front(RoomListServiceRoomInfo::from_room(new_room.into_inner()).await); + all_known_rooms.push_front(new_room); } VectorDiff::PushBack { value: new_room } => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushBack"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner()).await; add_new_room(&new_room, &room_list_service).await?; - all_known_rooms.push_back(RoomListServiceRoomInfo::from_room(new_room.into_inner()).await); + all_known_rooms.push_back(new_room); } remove_diff @ VectorDiff::PopFront => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PopFront"); } @@ -1963,17 +1987,19 @@ async fn async_main_loop( } VectorDiff::Insert { index, value: new_room } => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Insert at {index}"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner()).await; add_new_room(&new_room, &room_list_service).await?; - all_known_rooms.insert(index, RoomListServiceRoomInfo::from_room(new_room.into_inner()).await); + all_known_rooms.insert(index, new_room); } VectorDiff::Set { index, value: changed_room } => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Set at {index}"); } + let changed_room = RoomListServiceRoomInfo::from_room(changed_room.into_inner()).await; if let Some(old_room) = all_known_rooms.get(index) { update_room(old_room, &changed_room, &room_list_service).await?; } else { error!("BUG: room list diff: Set index {index} was out of bounds."); } - all_known_rooms.set(index, RoomListServiceRoomInfo::from_room(changed_room.into_inner()).await); + all_known_rooms.set(index, changed_room); } remove_diff @ VectorDiff::Remove { index: remove_index } => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Remove at {remove_index}"); } @@ -2011,11 +2037,10 @@ async fn async_main_loop( // so this is just a sanity check. ALL_JOINED_ROOMS.lock().unwrap().clear(); enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); - for room in &new_rooms { - add_new_room(room.deref(), &room_list_service).await?; - } for new_room in new_rooms.into_iter() { - all_known_rooms.push_back(RoomListServiceRoomInfo::from_room(new_room.into_inner()).await); + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner()).await; + add_new_room(&new_room, &room_list_service).await?; + all_known_rooms.push_back(new_room); } } } @@ -2050,8 +2075,9 @@ async fn optimize_remove_then_add_into_update( if LOG_ROOM_LIST_DIFFS { log!("Optimizing {remove_diff:?} + Insert({insert_index}) into Update for room {}", room.room_id); } - update_room(room, new_room, room_list_service).await?; - all_known_rooms.insert(*insert_index, RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await); + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await; + update_room(room, &new_room, room_list_service).await?; + all_known_rooms.insert(*insert_index, new_room); next_diff_was_handled = true; } Some(VectorDiff::PushFront { value: new_room }) @@ -2060,8 +2086,9 @@ async fn optimize_remove_then_add_into_update( if LOG_ROOM_LIST_DIFFS { log!("Optimizing {remove_diff:?} + PushFront into Update for room {}", room.room_id); } - update_room(room, new_room, room_list_service).await?; - all_known_rooms.push_front(RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await); + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await; + update_room(room, &new_room, room_list_service).await?; + all_known_rooms.push_front(new_room); next_diff_was_handled = true; } Some(VectorDiff::PushBack { value: new_room }) @@ -2070,8 +2097,9 @@ async fn optimize_remove_then_add_into_update( if LOG_ROOM_LIST_DIFFS { log!("Optimizing {remove_diff:?} + PushBack into Update for room {}", room.room_id); } - update_room(room, new_room, room_list_service).await?; - all_known_rooms.push_back(RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await); + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref()).await; + update_room(room, &new_room, room_list_service).await?; + all_known_rooms.push_back(new_room); next_diff_was_handled = true; } _ => next_diff_was_handled = false, @@ -2088,44 +2116,39 @@ async fn optimize_remove_then_add_into_update( /// Invoked when the room list service has received an update that changes an existing room. async fn update_room( old_room: &RoomListServiceRoomInfo, - new_room: &matrix_sdk::Room, + new_room: &RoomListServiceRoomInfo, room_list_service: &RoomListService, ) -> Result<()> { - let new_room_id = new_room.room_id().to_owned(); + let new_room_id = new_room.room_id.clone(); if old_room.room_id == new_room_id { - let new_room_name = new_room.display_name().await.map(|n| n.to_string()).ok(); - let mut room_avatar_changed = false; - // Handle state transitions for a room. - let old_room_state = old_room.room_state; - let new_room_state = new_room.state(); if LOG_ROOM_LIST_DIFFS { - log!("Room {new_room_name:?} ({new_room_id}) state went from {old_room_state:?} --> {new_room_state:?}"); + log!("Room {:?} ({new_room_id}) state went from {:?} --> {:?}", new_room.display_name, old_room.state, new_room.state); } - if old_room_state != new_room_state { - match new_room_state { + if old_room.state != new_room.state { + match new_room.state { RoomState::Banned => { // TODO: handle rooms that this user has been banned from. - log!("Removing Banned room: {new_room_name:?} ({new_room_id})"); - remove_room(&RoomListServiceRoomInfo::from_room_ref(new_room).await); + log!("Removing Banned room: {:?} ({new_room_id})", new_room.display_name); + remove_room(new_room); return Ok(()); } RoomState::Left => { - log!("Removing Left room: {new_room_name:?} ({new_room_id})"); + log!("Removing Left room: {:?} ({new_room_id})", new_room.display_name); // TODO: instead of removing this, we could optionally add it to // a separate list of left rooms, which would be collapsed by default. // Upon clicking a left room, we could show a splash page // that prompts the user to rejoin the room or forget it permanently. // Currently, we just remove it and do not show left rooms at all. - remove_room(&RoomListServiceRoomInfo::from_room_ref(new_room).await); + remove_room(new_room); return Ok(()); } RoomState::Joined => { - log!("update_room(): adding new Joined room: {new_room_name:?} ({new_room_id})"); + log!("update_room(): adding new Joined room: {:?} ({new_room_id})", new_room.display_name); return add_new_room(new_room, room_list_service).await; } RoomState::Invited => { - log!("update_room(): adding new Invited room: {new_room_name:?} ({new_room_id})"); + log!("update_room(): adding new Invited room: {:?} ({new_room_id})", new_room.display_name); return add_new_room(new_room, room_list_service).await; } RoomState::Knocked => { @@ -2135,75 +2158,113 @@ async fn update_room( } } - - let Some(client) = get_client() else { - return Ok(()); - }; - if let (Some(new_latest_event), Some(old_latest_event)) = - (new_room.latest_event(), old_room.room.latest_event()) - { - if let Some(new_latest_event) = - EventTimelineItem::from_latest_event(client.clone(), &new_room_id, new_latest_event) - .await - { - if let Some(old_latest_event) = EventTimelineItem::from_latest_event( - client.clone(), - &new_room_id, - old_latest_event, - ) - .await - { - if new_latest_event.timestamp() > old_latest_event.timestamp() { - log!("Updating latest event for room {}", new_room_id); - room_avatar_changed = - update_latest_event(new_room, &new_latest_event, None); - } - } - } + // First, we check for changes to room data that is relevant to any room, + // including joined, invited, and other rooms. + // This includes the room name and room avatar. + if old_room.room_avatar != new_room.room_avatar { + log!("Updating room avatar for room {}", new_room_id); + spawn_fetch_room_avatar(new_room); } - - if room_avatar_changed || (old_room.room.avatar_url() != new_room.avatar_url()) { - log!("Updating avatar for room {}", new_room_id); - spawn_fetch_room_avatar(new_room.clone()); + if old_room.display_name != new_room.display_name { + log!("Updating room {} name: {:?} --> {:?}", new_room_id, old_room.display_name, new_room.display_name); + enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName { + room_id: new_room_id.clone(), + new_room_name: new_room.display_name.as_ref().map(|n| n.to_string()), + }); } - if let Some(new_room_name) = new_room_name { - if old_room.room.cached_display_name().map(|room_name| room_name.to_string()).as_ref() != Some(&new_room_name) { - log!("Updating room name for room {} to {}", new_room_id, new_room_name); - enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName { + // Then, we check for changes to room data that is only relevant to joined rooms: + // including the latest event, tags, unread counts, is_direct, tombstoned state, power levels, etc. + // Invited or left rooms don't care about these details. + if matches!(new_room.state, RoomState::Joined) { + // For some reason, the latest event API does not reliably catch *all* changes + // to the latest event in a given room, such as redactions. + // Thus, we have to re-obtain the latest event on *every* update, regardless of timestamp. + // + // let should_update_latest = match (old_room.latest_event_timestamp, new_room.new_latest_event_timestamp()) { + // (Some(old_ts), Some(new_ts)) if new_ts > old_ts => true, + // (None, Some(_)) => true, + // _ => false, + // }; + // if should_update_latest { ... } + update_latest_event(&new_room.room).await; + + if old_room.tags != new_room.tags { + log!("Updating room {} tags from {:?} to {:?}", new_room_id, old_room.tags, new_room.tags); + enqueue_rooms_list_update(RoomsListUpdate::Tags { room_id: new_room_id.clone(), - new_room_name, + new_tags: new_room.tags.clone().unwrap_or_default(), }); } - } - // Below, we update room data that is only relevant to joined rooms: - // tags, unread count, is_direct, etc. - // Invited or left rooms don't care about these details. - if matches!(new_room_state, RoomState::Joined) { - if let Ok(new_tags) = new_room.tags().await { - enqueue_rooms_list_update(RoomsListUpdate::Tags { + if old_room.num_unread_messages != new_room.num_unread_messages + || old_room.num_unread_mentions != new_room.num_unread_mentions + { + log!("Updating room {}, unread messages {} --> {}, unread mentions {} --> {}", + new_room_id, + old_room.num_unread_messages, new_room.num_unread_messages, + old_room.num_unread_mentions, new_room.num_unread_mentions, + ); + enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { room_id: new_room_id.clone(), - new_tags: new_tags.unwrap_or_default(), + unread_messages: UnreadMessageCount::Known(new_room.num_unread_messages), + unread_mentions: new_room.num_unread_mentions, }); } - enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { - room_id: new_room_id.clone(), - count: UnreadMessageCount::Known(new_room.num_unread_messages()), - unread_mentions: new_room.num_unread_mentions() - }); + if old_room.is_direct != new_room.is_direct { + log!("Updating room {} is_direct from {} to {}", + new_room_id, + old_room.is_direct, + new_room.is_direct, + ); + enqueue_rooms_list_update(RoomsListUpdate::UpdateIsDirect { + room_id: new_room_id.clone(), + is_direct: new_room.is_direct, + }); + } - if let Ok(is_new_room_direct) = new_room.is_direct().await { - if old_room.is_direct != is_new_room_direct { - enqueue_rooms_list_update(RoomsListUpdate::UpdateIsDirect { - room_id: new_room_id.clone(), - is_direct: is_new_room_direct, - }); + let mut __timeline_update_sender_opt = None; + let mut get_timeline_update_sender = |room_id| { + if __timeline_update_sender_opt.is_none() { + if let Some(jrd) = ALL_JOINED_ROOMS.lock().unwrap().get(room_id) { + __timeline_update_sender_opt = Some(jrd.timeline_update_sender.clone()); + } + } + __timeline_update_sender_opt.clone() + }; + + if !old_room.is_tombstoned && new_room.is_tombstoned { + enqueue_rooms_list_update(RoomsListUpdate::TombstonedRoom { room_id: new_room_id.clone() }); + if let Some(successor_room) = new_room.room.successor_room() { + if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { + log!("Updating room {new_room_id} to be tombstoned, {successor_room:?}"); + match timeline_update_sender.send(TimelineUpdate::Tombstoned(Some(successor_room))) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => error!("Failed to send the Tombstoned update to room {new_room_id}"), + } + } else { + error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); + } + } else { + log!("BUG: room {} was tombstoned but had no successor room!", new_room_id); } } - } + if let Some(nupl) = new_room.user_power_levels + && old_room.user_power_levels.is_none_or(|oupl| oupl != nupl) + { + if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { + log!("Updating room {new_room_id} user power levels."); + match timeline_update_sender.send(TimelineUpdate::UserPowerLevels(nupl)) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => error!("Failed to send the UserPowerLevels update to room {new_room_id}"), + } + } else { + error!("BUG: could not find JoinedRoomDetails for room {new_room_id} where power levels changed."); + } + } + } Ok(()) } else { @@ -2222,32 +2283,29 @@ fn remove_room(room: &RoomListServiceRoomInfo) { enqueue_rooms_list_update( RoomsListUpdate::RemoveRoom { room_id: room.room_id.clone(), - new_state: room.room_state, + new_state: room.state, } ); } /// Invoked when the room list service has received an update with a brand new room. -async fn add_new_room(room: &matrix_sdk::Room, room_list_service: &RoomListService) -> Result<()> { - let room_id = room.room_id().to_owned(); - // We must call `display_name()` here to calculate and cache the room's name. - let room_name = room.display_name().await.map(|n| n.to_string()).ok(); - - let is_direct = room.is_direct().await.unwrap_or(false); - - match room.state() { +async fn add_new_room( + new_room: &RoomListServiceRoomInfo, + room_list_service: &RoomListService, +) -> Result<()> { + match new_room.state { RoomState::Knocked => { // TODO: handle Knocked rooms (e.g., can you re-knock? or cancel a prior knock?) return Ok(()); } RoomState::Banned => { - log!("Got new Banned room: {room_name:?} ({room_id})"); + log!("Got new Banned room: {:?} ({})", new_room.display_name, new_room.room_id); // TODO: handle rooms that this user has been banned from. return Ok(()); } RoomState::Left => { - log!("Got new Left room: {room_name:?} ({room_id})"); + log!("Got new Left room: {:?} ({:?})", new_room.display_name, new_room.room_id); // TODO: add this to the list of left rooms, // which is collapsed by default. // Upon clicking a left room, we can show a splash page @@ -2258,19 +2316,20 @@ async fn add_new_room(room: &matrix_sdk::Room, room_list_service: &RoomListServi return Ok(()); } RoomState::Invited => { - let invite_details = room.invite_details().await.ok(); + let invite_details = new_room.room.invite_details().await.ok(); let Some(client) = get_client() else { return Ok(()); }; - let latest_event = if let Some(latest_event) = room.latest_event() { - EventTimelineItem::from_latest_event(client, &room_id, latest_event).await + let latest_event = if let Some(latest_event) = new_room.room.latest_event() { + EventTimelineItem::from_latest_event(client, &new_room.room_id, latest_event).await } else { None }; let latest = latest_event.as_ref().map( - |ev| get_latest_event_details(ev, &room_id) + |ev| get_latest_event_details(ev, &new_room.room_id) ); - let room_avatar = room_avatar(room, room_name.as_deref()).await; + let room_name = new_room.display_name.as_ref().map(|n| n.to_string()); + let room_avatar = room_avatar(&new_room.room, room_name.as_deref()).await; let inviter_info = if let Some(inviter) = invite_details.and_then(|d| d.inviter) { Some(InviterInfo { @@ -2287,54 +2346,55 @@ async fn add_new_room(room: &matrix_sdk::Room, room_list_service: &RoomListServi None }; rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(InvitedRoomInfo { - room_id: room_id.clone(), + room_id: new_room.room_id.clone(), room_name, inviter_info, room_avatar, - canonical_alias: room.canonical_alias(), - alt_aliases: room.alt_aliases(), + canonical_alias: new_room.room.canonical_alias(), + alt_aliases: new_room.room.alt_aliases(), latest, invite_state: Default::default(), is_selected: false, - is_direct, + is_direct: new_room.is_direct, })); - Cx::post_action(AppStateAction::RoomLoadedSuccessfully(room_id)); + Cx::post_action(AppStateAction::RoomLoadedSuccessfully(new_room.room_id.clone())); return Ok(()); } RoomState::Joined => { } // Fall through to adding the joined room below. } - // Subscribe to all updates for this room in order to properly receive all of its states. - room_list_service.subscribe_to_rooms(&[&room_id]).await; + // Subscribe to all updates for this room in order to properly receive all of its states, + // as well as its latest event (via `Room::new_latest_event_*()` and the `LatestEvents` API). + room_list_service.subscribe_to_rooms(&[&new_room.room_id]).await; let timeline = Arc::new( - room.timeline_builder() + new_room.room.timeline_builder() .track_read_marker_and_receipts() .build() .await - .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {room_id}: {e}"))?, + .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {}: {e}", new_room.room_id))?, ); let latest_event = timeline.latest_event().await; let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); let (request_sender, request_receiver) = watch::channel(Vec::new()); let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler( - room.clone(), + new_room.room.clone(), timeline.clone(), timeline_update_sender.clone(), request_receiver, )); let latest = latest_event.as_ref().map( - |ev| get_latest_event_details(ev, &room_id) + |ev| get_latest_event_details(ev, &new_room.room_id) ); - log!("Adding new joined room {room_id}."); + log!("Adding new joined room {}, name: {:?}", new_room.room_id, new_room.display_name); ALL_JOINED_ROOMS.lock().unwrap().insert( - room_id.clone(), + new_room.room_id.clone(), JoinedRoomDetails { - room_id: room_id.clone(), + room_id: new_room.room_id.clone(), timeline, timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)), timeline_update_sender, @@ -2346,25 +2406,26 @@ async fn add_new_room(room: &matrix_sdk::Room, room_list_service: &RoomListServi // We need to add the room to the `ALL_JOINED_ROOMS` list before we can // send the `AddJoinedRoom` update to the UI, because the UI might immediately // issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`. + let room_name = new_room.display_name.as_ref().map(|n| n.to_string()); rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddJoinedRoom(JoinedRoomInfo { - room_id: room_id.clone(), + room_id: new_room.room_id.clone(), latest, - tags: room.tags().await.ok().flatten().unwrap_or_default(), - num_unread_messages: room.num_unread_messages(), - num_unread_mentions: room.num_unread_mentions(), + tags: new_room.tags.clone().unwrap_or_default(), + num_unread_messages: new_room.num_unread_messages, + num_unread_mentions: new_room.num_unread_mentions, // start with a basic text avatar; the avatar image will be fetched asynchronously below. avatar: avatar_from_room_name(room_name.as_deref()), room_name, - canonical_alias: room.canonical_alias(), - alt_aliases: room.alt_aliases(), + canonical_alias: new_room.room.canonical_alias(), + alt_aliases: new_room.room.alt_aliases(), has_been_paginated: false, is_selected: false, - is_direct, - is_tombstoned: room.is_tombstoned() || room.tombstone_content().is_some(), + is_direct: new_room.is_direct, + is_tombstoned: new_room.is_tombstoned, })); - Cx::post_action(AppStateAction::RoomLoadedSuccessfully(room_id)); - spawn_fetch_room_avatar(room.clone()); + Cx::post_action(AppStateAction::RoomLoadedSuccessfully(new_room.room_id.clone())); + spawn_fetch_room_avatar(new_room); Ok(()) } @@ -2598,8 +2659,6 @@ async fn timeline_subscriber_handler( |_e| panic!("Error: timeline update sender couldn't send first update ({} items) to room {room_id}!", timeline_items.len()) ); - let mut latest_event = timeline.latest_event().await; - // the event ID to search for while loading previous items into the timeline. let mut target_event_id = None; // the timeline index and event ID of the target event, if it has been found. @@ -2685,8 +2744,6 @@ async fn timeline_subscriber_handler( batch_opt = subscriber.next() => { let Some(batch) = batch_opt else { break }; let mut num_updates = 0; - // For now we always requery the latest event, but this can be better optimized. - let mut reobtain_latest_event = true; let mut index_of_first_change = usize::MAX; let mut index_of_last_change = usize::MIN; // whether to clear the entire cache of drawn items @@ -2702,14 +2759,12 @@ async fn timeline_subscriber_handler( timeline_items.extend(values); index_of_last_change = max(index_of_last_change, timeline_items.len()); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Append {_values_len}. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; is_append = true; } VectorDiff::Clear => { if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Clear"); } clear_cache = true; timeline_items.clear(); - reobtain_latest_event = true; } VectorDiff::PushFront { value } => { if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff PushFront"); } @@ -2721,14 +2776,12 @@ async fn timeline_subscriber_handler( clear_cache = true; timeline_items.push_front(value); - reobtain_latest_event |= latest_event.is_none(); } VectorDiff::PushBack { value } => { index_of_first_change = min(index_of_first_change, timeline_items.len()); timeline_items.push_back(value); index_of_last_change = max(index_of_last_change, timeline_items.len()); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff PushBack. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; is_append = true; } VectorDiff::PopFront => { @@ -2745,7 +2798,6 @@ async fn timeline_subscriber_handler( index_of_first_change = min(index_of_first_change, timeline_items.len()); index_of_last_change = usize::MAX; if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff PopBack. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; } VectorDiff::Insert { index, value } => { if index == 0 { @@ -2770,14 +2822,12 @@ async fn timeline_subscriber_handler( timeline_items.insert(index, value); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Insert at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; } VectorDiff::Set { index, value } => { index_of_first_change = min(index_of_first_change, index); index_of_last_change = max(index_of_last_change, index.saturating_add(1)); timeline_items.set(index, value); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Set at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; } VectorDiff::Remove { index } => { if index == 0 { @@ -2794,7 +2844,6 @@ async fn timeline_subscriber_handler( } timeline_items.remove(index); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Remove at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; } VectorDiff::Truncate { length } => { if length == 0 { @@ -2805,25 +2854,17 @@ async fn timeline_subscriber_handler( } timeline_items.truncate(length); if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Truncate to length {length}. Changes: {index_of_first_change}..{index_of_last_change}"); } - reobtain_latest_event = true; } VectorDiff::Reset { values } => { if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id} diff Reset, new length {}", values.len()); } clear_cache = true; // we must assume all items have changed. timeline_items = values; - reobtain_latest_event = true; } } } if num_updates > 0 { - let new_latest_event = if reobtain_latest_event { - timeline.latest_event().await - } else { - None - }; - // Handle the case where back pagination inserts items at the beginning of the timeline // (meaning the entire timeline needs to be re-drawn), // but there is a virtual event at index 0 (e.g., a day divider). @@ -2859,16 +2900,6 @@ async fn timeline_subscriber_handler( ); } - // Update the latest event for this room. - // We always do this in case a redaction or other event has changed the latest event. - if let Some(new_latest) = new_latest_event { - let room_avatar_changed = update_latest_event(&room, &new_latest, Some(&timeline_update_sender)); - if room_avatar_changed { - spawn_fetch_room_avatar(room.clone()); - } - latest_event = Some(new_latest); - } - // Send a Makepad-level signal to update this room's timeline UI view. SignalToUI::set_ui_signal(); } @@ -2884,111 +2915,61 @@ async fn timeline_subscriber_handler( /// Handles the given updated latest event for the given room. /// -/// This currently includes checking the given event for: -/// * room name changes, in which it sends a `RoomsListUpdate`. -/// * room power level changes to see if the current user's permissions -/// have changed; if so, it sends a [`TimelineUpdate::UserPowerLevels`]. -/// * room avatar changes, which is not handled here. -/// Instead, we return `true` such that other code can fetch the new avatar. -/// * membership changes to see if the current user has joined or left a room. -/// -/// Finally, this function sends a `RoomsListUpdate::UpdateLatestEvent` +/// This function sends a `RoomsListUpdate::UpdateLatestEvent` /// to update the latest event in the RoomsList's room preview for the given room. -/// -/// Returns `true` if room avatar has changed and should be fetched and updated. -fn update_latest_event( - room: &Room, - event_tl_item: &EventTimelineItem, - timeline_update_sender: Option<&crossbeam_channel::Sender> -) -> bool { - let mut room_avatar_changed = false; - - let room_id = room.room_id().to_owned(); - let (timestamp, latest_message_text) = get_latest_event_details(event_tl_item, &room_id); - match event_tl_item.content() { - // Check for relevant state events. - TimelineItemContent::OtherState(other) => { - match other.content() { - // Check for room name changes. - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => { - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName { - room_id: room_id.clone(), - new_room_name: content.name.clone(), - }); - } - // Check for room avatar changes. - AnyOtherFullStateEventContent::RoomAvatar(_avatar_event) => { - room_avatar_changed = true; - } - // Check for an update to the current user's power levels in this room. - AnyOtherFullStateEventContent::RoomPowerLevels(FullStateEventContent::Original { content, prev_content: _ }) => { - if let (Some(sender), Some(user_id)) = (timeline_update_sender, current_user_id()) { - if let Some(authorization_rules) = room.version().and_then(|v| v.rules().map(|r| r.authorization)) { - let user_power_levels = UserPowerLevels::from( - &RoomPowerLevels::new( - content.clone().into(), - &authorization_rules, - room.creators().unwrap_or_default(), - ), - &user_id, - ); - match sender.send(TimelineUpdate::UserPowerLevels(user_power_levels)) { - Ok(_) => SignalToUI::set_ui_signal(), - Err(e) => error!("Failed to send the new RoomPowerLevels from an updated latest event: {e}"), - } - } - } - } - // Check for room tombstone status changes. - AnyOtherFullStateEventContent::RoomTombstone(FullStateEventContent::Original { content: _, prev_content: _ }) => { - enqueue_rooms_list_update(RoomsListUpdate::TombstonedRoom { room_id: room_id.clone()}); - if let (Some(sender), Some(room)) = ( - timeline_update_sender, - get_client() - .unwrap() - .get_room(&room_id) - .and_then(|room| room.successor_room()), - ) { - match sender.send(TimelineUpdate::Tombstoned(Some(room))) { - Ok(_) => { - SignalToUI::set_ui_signal(); - } - Err(e) => { - error!("Failed to send the new Tombstone event: {e}"); - } - } - } - } - _ => { } - } +async fn update_latest_event(room: &Room) { + let Some(client) = get_client() else { return }; + let (sender_username, sender_id, timestamp, content) = match room.new_latest_event().await { + LatestEventValue::Remote { timestamp, sender, is_own, profile, content } => { + let sender_username = if let TimelineDetails::Ready(profile) = profile { + profile.display_name + } else if is_own { + client.account().get_display_name().await.ok().flatten() + } else { + None + }; + ( + sender_username.unwrap_or_else(|| sender.to_string()), + sender, + timestamp, + content + ) } - TimelineItemContent::MembershipChange(room_membership_change) => { - if matches!( - room_membership_change.change(), - Some(MembershipChange::InvitationAccepted | MembershipChange::Joined) - ) { - if current_user_id().as_deref() == Some(room_membership_change.user_id()) { - submit_async_request(MatrixRequest::GetRoomPowerLevels { room_id: room_id.clone() }); - } - } + LatestEventValue::Local { timestamp, content, is_sending: _ } => { + // TODO: use the `is_sending` flag to augment the preview text + // (e.g., "Sending... " or "Failed to send "). + let our_name = client.account().get_display_name().await.ok().flatten(); + let Some(our_user_id) = current_user_id() else { return }; + ( + our_name.unwrap_or_else(|| String::from("You")), + our_user_id, + timestamp, + content, + ) } - _ => { } - } + LatestEventValue::None => return, + }; + + let latest_message_text = text_preview_of_timeline_item( + &content, + &sender_id, + &sender_username, + ).format_with(&sender_username, true); enqueue_rooms_list_update(RoomsListUpdate::UpdateLatestEvent { - room_id, + room_id: room.room_id().to_owned(), timestamp, latest_message_text, }); - room_avatar_changed } /// Spawn a new async task to fetch the room's new avatar. -fn spawn_fetch_room_avatar(room: Room) { +fn spawn_fetch_room_avatar(room: &RoomListServiceRoomInfo) { + let room_id = room.room_id.clone(); + let room_name = room.display_name.as_ref().map(|n| n.to_string()); + let inner_room = room.room.clone(); Handle::current().spawn(async move { - let room_id = room.room_id().to_owned(); - let room_name_str = room.cached_display_name().map(|dn| dn.to_string()); - let avatar = room_avatar(&room, room_name_str.as_deref()).await; + let avatar = room_avatar(&inner_room, room_name.as_deref()).await; rooms_list::enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomAvatar { room_id, avatar, @@ -3244,6 +3225,11 @@ impl UserPowerLevels { retval } + pub async fn from_room(room: &Room, user_id: &UserId) -> Option { + let room_power_levels = room.power_levels().await.ok()?; + Some(UserPowerLevels::from(&room_power_levels, user_id)) + } + pub fn can_ban(self) -> bool { self.contains(UserPowerLevels::Ban) }