diff --git a/src/home/invite_screen.rs b/src/home/invite_screen.rs index dc412f35..d87e9162 100644 --- a/src/home/invite_screen.rs +++ b/src/home/invite_screen.rs @@ -387,6 +387,10 @@ impl Widget for InviteScreen { } if self.invite_state != orig_state { + if let (Some(room_name_id), true) = (self.room_name_id.as_ref(), cx.has_global::()) { + let rooms_list_ref = cx.get_global::(); + rooms_list_ref.set_invite_state(room_name_id.room_id().clone(), self.invite_state); + } self.redraw(cx); } } @@ -396,7 +400,7 @@ impl Widget for InviteScreen { if !self.is_loaded { let mut restore_status_view = self.view.restore_status_view(ids!(restore_status_view)); if let Some(room_name) = &self.room_name_id { - restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); + restore_status_view.set_content(cx, self.all_rooms_loaded, room_name, None); } return restore_status_view.draw(cx, scope); } @@ -536,11 +540,7 @@ impl InviteScreen { let restore_status_view = self.view.restore_status_view(ids!(restore_status_view)); if !self.is_loaded { - restore_status_view.set_content( - cx, - self.all_rooms_loaded, - room_name_id, - ); + restore_status_view.set_content(cx, self.all_rooms_loaded, room_name_id, None); restore_status_view.set_visible(cx, true); } else { restore_status_view.set_visible(cx, false); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 5a0799c2..1d4c25c5 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -7,7 +7,7 @@ use bytesize::ByteSize; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ + OwnedServerName, RoomDisplayName, RoomState, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ receipt::Receipt, room::{ @@ -16,7 +16,7 @@ use matrix_sdk::{ } }, sticker::{StickerEventContent, StickerMediaSource}, - }, matrix_uri::MatrixId, uint + }, matrix_uri::MatrixId, room::JoinRuleSummary, uint } }; use matrix_sdk_ui::timeline::{ @@ -25,11 +25,11 @@ use matrix_sdk_ui::timeline::{ use ruma::{OwnedUserId, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}}; use crate::{ - app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::RoomsListRef, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{enqueue_rooms_list_update, InviteState, InvitedRoomInfo, RoomsListRef, RoomsListUpdate}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{BasicRoomDetails, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, + room::{BasicRoomDetails, RoomPreviewAction, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, shared::{ avatar::{AvatarState, AvatarWidgetRefExt}, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, @@ -588,6 +588,16 @@ pub struct RoomScreen { #[rust] is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). #[rust] all_rooms_loaded: bool, + /// Tracks whether the current user is no longer a member of this room. + #[rust] removed_state: Option, + /// Tracks whether the membership footer was shown for an invite state. + #[rust] was_invited: bool, + /// Cached join rule for the removed room, used to determine re-join options. + #[rust] removed_join_rule: Option, + /// Whether a room preview request is in flight for removed-room status. + #[rust] removed_preview_requested: bool, + /// Whether a room preview request has already been attempted for this removal. + #[rust] removed_preview_attempted: bool, } impl Drop for RoomScreen { fn drop(&mut self) { @@ -747,6 +757,110 @@ impl Widget for RoomScreen { } } + if let Some(RoomPreviewAction::Fetched(result)) = action.downcast_ref() { + if self.removed_state.is_none() || !self.removed_preview_requested { + continue; + } + match result { + Ok(frp) => { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == frp.room_name_id.room_id()) { + self.removed_join_rule = frp.join_rule.clone(); + self.removed_preview_requested = false; + self.removed_preview_attempted = true; + self.update_membership_footer(cx); + } + } + Err(_) => { + self.removed_preview_requested = false; + self.removed_preview_attempted = true; + self.update_membership_footer(cx); + } + } + } + + if let Some(room_name_id) = self.room_name_id.as_ref() { + let room_id = room_name_id.room_id(); + let rooms_list_ref = cx.has_global::().then(|| cx.get_global::()); + let is_invited = rooms_list_ref + .as_ref() + .and_then(|rooms_list_ref| rooms_list_ref.get_invite_state(room_id)) + .is_some(); + let should_notify = self.removed_state.is_some() || is_invited; + + if let Some(JoinRoomResultAction::Joined { room_id: action_room_id }) = action.downcast_ref() { + if action_room_id == room_id { + if is_invited { + if let Some(rooms_list_ref) = rooms_list_ref.as_ref() { + rooms_list_ref.set_invite_state(room_id.clone(), InviteState::WaitingForJoinedRoom); + } + } + if should_notify { + enqueue_popup_notification(PopupItem { + message: "Successfully joined room.".into(), + kind: PopupKind::Success, + auto_dismissal_duration: Some(5.0), + }); + } + self.update_membership_footer(cx); + continue; + } + } + + if let Some(JoinRoomResultAction::Failed { room_id: action_room_id, error }) = action.downcast_ref() { + if action_room_id == room_id { + if is_invited { + if let Some(rooms_list_ref) = rooms_list_ref.as_ref() { + rooms_list_ref.set_invite_state(room_id.clone(), InviteState::WaitingOnUserInput); + } + } + if should_notify { + let msg = utils::stringify_join_leave_error(error, room_name_id, true, is_invited); + enqueue_popup_notification(PopupItem { + message: msg, + kind: PopupKind::Error, + auto_dismissal_duration: None, + }); + } + self.update_membership_footer(cx); + continue; + } + } + + if let Some(LeaveRoomResultAction::Left { room_id: action_room_id }) = action.downcast_ref() { + if action_room_id == room_id && is_invited { + if let Some(rooms_list_ref) = rooms_list_ref.as_ref() { + rooms_list_ref.set_invite_state(room_id.clone(), InviteState::RoomLeft); + } + if should_notify { + enqueue_popup_notification(PopupItem { + message: "Successfully rejected invite.".into(), + kind: PopupKind::Success, + auto_dismissal_duration: Some(5.0), + }); + } + self.update_membership_footer(cx); + continue; + } + } + + if let Some(LeaveRoomResultAction::Failed { room_id: action_room_id, error }) = action.downcast_ref() { + if action_room_id == room_id && is_invited { + if let Some(rooms_list_ref) = rooms_list_ref.as_ref() { + rooms_list_ref.set_invite_state(room_id.clone(), InviteState::WaitingOnUserInput); + } + if should_notify { + enqueue_popup_notification(PopupItem { + message: format!("Failed to reject invite: {error}"), + kind: PopupKind::Error, + auto_dismissal_duration: None, + }); + } + self.update_membership_footer(cx); + continue; + } + } + } + // Handle the highlight animation for a message. let Some(tl) = self.tl_state.as_mut() else { continue }; if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { @@ -819,12 +933,36 @@ impl Widget for RoomScreen { self.room_name_id = None; self.set_displayed_room(cx, &room_name_clone); } else { + let mut removed_state = rooms_list_ref.get_removed_room_state(room_name_id.room_id()); + if let Some(client) = get_client() + && let Some(room) = client.get_room(room_name_id.room_id()) + { + let actual_state = room.state(); + let desired_removed_state = match actual_state { + RoomState::Left | RoomState::Banned => Some(actual_state), + _ => None, + }; + if desired_removed_state != removed_state { + rooms_list_ref.set_removed_room_state( + room_name_id.room_id().clone(), + desired_removed_state, + ); + removed_state = desired_removed_state; + } + } + if self.removed_state != removed_state { + self.removed_state = removed_state; + self.removed_join_rule = None; + self.removed_preview_requested = false; + self.removed_preview_attempted = false; + } self.all_rooms_loaded = rooms_list_ref.all_rooms_loaded(); return; } } self.process_timeline_updates(cx, &portal_list); + self.update_membership_footer(cx); // Ideally we would do this elsewhere on the main thread, because it's not room-specific, // but it doesn't hurt to do it here. @@ -983,7 +1121,12 @@ impl Widget for RoomScreen { return DrawStep::done(); }; let mut restore_status_view = self.view.restore_status_view(ids!(restore_status_view)); - restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); + restore_status_view.set_content( + cx, + self.all_rooms_loaded, + room_name, + self.removed_state, + ); return restore_status_view.draw(cx, scope); } if self.tl_state.is_none() { @@ -1166,6 +1309,138 @@ impl RoomScreen { self.room_name_id.as_ref().map(|r| r.room_id()) } + fn update_membership_footer(&mut self, cx: &mut Cx) { + let Some(room_name_id) = &self.room_name_id else { return; }; + if !cx.has_global::() { + return; + } + let rooms_list_ref = cx.get_global::(); + let room_input_bar = self.view.room_input_bar(ids!(room_input_bar)); + let mut actual_state = None; + let mut actual_room = None; + if let Some(client) = get_client() + && let Some(room) = client.get_room(room_name_id.room_id()) + { + actual_state = Some(room.state()); + actual_room = Some(room); + } + + let invite_state = rooms_list_ref.get_invite_state(room_name_id.room_id()); + let mut is_invited = invite_state.is_some() + || rooms_list_ref.get_room_state(room_name_id.room_id()) == Some(RoomState::Invited); + if !is_invited && matches!(actual_state, Some(RoomState::Invited)) { + is_invited = true; + if rooms_list_ref.get_room_state(room_name_id.room_id()) != Some(RoomState::Invited) { + if let Some(room) = actual_room.as_ref() { + let room_avatar = utils::avatar_from_room_name(room_name_id.name_for_avatar().as_deref()); + enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(InvitedRoomInfo { + room_name_id: room_name_id.clone(), + canonical_alias: room.canonical_alias(), + alt_aliases: room.alt_aliases(), + room_avatar, + inviter_info: None, + latest: None, + invite_state: invite_state.unwrap_or(InviteState::WaitingOnUserInput), + is_selected: false, + is_direct: false, + })); + } + } + } + if is_invited { + self.was_invited = true; + if self.removed_state.is_some() + || self.removed_join_rule.is_some() + || self.removed_preview_requested + || self.removed_preview_attempted + { + self.removed_state = None; + self.removed_join_rule = None; + self.removed_preview_requested = false; + self.removed_preview_attempted = false; + } + room_input_bar.update_membership_footer( + cx, + room_name_id, + RoomState::Invited, + None, + false, + invite_state, + ); + return; + } + let mut removed_state = { + rooms_list_ref.get_removed_room_state(room_name_id.room_id()) + }; + + if let Some(actual_state) = actual_state { + let desired_removed_state = match actual_state { + RoomState::Left | RoomState::Banned => Some(actual_state), + _ => None, + }; + if desired_removed_state != removed_state { + let rooms_list_ref = cx.get_global::(); + rooms_list_ref.set_removed_room_state( + room_name_id.room_id().clone(), + desired_removed_state, + ); + removed_state = desired_removed_state; + } + } + + match removed_state { + Some(state) if matches!(state, RoomState::Left | RoomState::Banned) => { + self.was_invited = false; + if self.removed_state != Some(state) { + self.removed_state = Some(state); + self.removed_join_rule = None; + self.removed_preview_requested = false; + self.removed_preview_attempted = false; + } + + if state == RoomState::Left + && self.removed_join_rule.is_none() + && !self.removed_preview_attempted + { + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id: room_name_id.room_id().clone().into(), + via: Vec::new(), + }); + self.removed_preview_requested = true; + self.removed_preview_attempted = true; + } + + let preview_pending = self.removed_preview_requested; + room_input_bar.update_membership_footer( + cx, + room_name_id, + state, + self.removed_join_rule.clone(), + preview_pending, + None, + ); + } + _ => { + if self.removed_state.is_some() || self.was_invited { + self.removed_state = None; + self.was_invited = false; + self.removed_join_rule = None; + self.removed_preview_requested = false; + self.removed_preview_attempted = false; + room_input_bar.hide_membership_footer(cx); + if let Some(tl_state) = self.tl_state.as_ref() { + room_input_bar.update_tombstone_footer( + cx, + &tl_state.room_id, + tl_state.tombstone_info.as_ref(), + ); + room_input_bar.update_user_power_levels(cx, tl_state.user_power); + } + } + } + } + } + /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. @@ -2311,6 +2586,12 @@ impl RoomScreen { if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_name_id.room_id()) { return; } self.hide_timeline(); + self.removed_state = None; + self.was_invited = false; + self.removed_join_rule = None; + self.removed_preview_requested = false; + self.removed_preview_attempted = false; + self.view.room_input_bar(ids!(room_input_bar)).hide_membership_footer(cx); // Reset the the state of the inner loading pane. self.loading_pane(ids!(loading_pane)).take_state(); @@ -2326,6 +2607,7 @@ impl RoomScreen { }); self.show_timeline(cx); + self.update_membership_footer(cx); } /// Sends read receipts based on the current scroll position of the timeline. diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index e49a9d1b..35681e5e 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -192,12 +192,22 @@ pub enum RoomsListUpdate { room_id: OwnedRoomId, is_direct: bool, }, - /// Remove the given room from the rooms list + /// Remove the given room from the rooms list, or mark it as removed for history. RemoveRoom { room_id: OwnedRoomId, /// The new state of the room (which caused its removal). new_state: RoomState, }, + /// Update the removed state of a room without removing it. + SetRemovedRoomState { + room_id: OwnedRoomId, + removed_state: Option, + }, + /// Update the invite state for the given invited room. + SetInviteState { + room_id: OwnedRoomId, + invite_state: InviteState, + }, /// Update the tags for the given room. Tags { room_id: OwnedRoomId, @@ -258,6 +268,8 @@ pub enum RoomsListAction { pub struct JoinedRoomInfo { /// The displayable name of this room (includes room ID for fallback). pub room_name_id: RoomNameId, + /// If set, the user is no longer a member of this room but we keep it for history. + pub removed_state: Option, /// The number of unread messages in this room. pub num_unread_messages: u64, /// The number of unread mentions in this room. @@ -487,15 +499,30 @@ impl RoomsList { /// Returns the state of the room if it is loaded and known to our client. pub fn get_room_state(&self, room_id: &OwnedRoomId) -> Option { - if self.all_joined_rooms.contains_key(room_id) { - return Some(RoomState::Joined); - } if self.invited_rooms.borrow().contains_key(room_id) { return Some(RoomState::Invited); } + if self.all_joined_rooms.contains_key(room_id) { + return Some(RoomState::Joined); + } None } + /// Returns the invite state of the room, if it is currently an invite. + pub fn get_invite_state(&self, room_id: &OwnedRoomId) -> Option { + self.invited_rooms + .borrow() + .get(room_id) + .map(|info| info.invite_state) + } + + /// Returns the removal state of the room, if it was removed (e.g., left or banned). + pub fn get_removed_room_state(&self, room_id: &OwnedRoomId) -> Option { + self.all_joined_rooms + .get(room_id) + .and_then(|jr| jr.removed_state) + } + /// Handle all pending updates to the list of all rooms. fn handle_rooms_list_updates(&mut self, cx: &mut Cx, _event: &Event, scope: &mut Scope) { let mut num_updates: usize = 0; @@ -506,7 +533,22 @@ impl RoomsList { let room_id = invited_room.room_name_id.room_id().clone(); let should_display = should_display_room!(self, &room_id, &invited_room); let _replaced = self.invited_rooms.borrow_mut().insert(room_id.clone(), invited_room); - if should_display { + let mut keep_joined_room = false; + if let Some(joined_room) = self.all_joined_rooms.get(&room_id) { + keep_joined_room = joined_room.removed_state.is_some(); + let list_to_remove_from = if joined_room.is_direct { + &mut self.displayed_direct_rooms + } else { + &mut self.displayed_regular_rooms + }; + list_to_remove_from.iter() + .position(|r| r == &room_id) + .map(|index| list_to_remove_from.remove(index)); + } + if !keep_joined_room { + self.all_joined_rooms.remove(&room_id); + } + if should_display && !self.displayed_invited_rooms.contains(&room_id) { self.displayed_invited_rooms.push(room_id); } self.update_status(); @@ -519,9 +561,19 @@ impl RoomsList { let _replaced = self.all_joined_rooms.insert(room_id.clone(), joined_room); if should_display { if is_direct { - self.displayed_direct_rooms.push(room_id.clone()); + if !self.displayed_direct_rooms.contains(&room_id) { + self.displayed_direct_rooms.push(room_id.clone()); + } + if let Some(index) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { + self.displayed_regular_rooms.remove(index); + } } else { - self.displayed_regular_rooms.push(room_id.clone()); + if !self.displayed_regular_rooms.contains(&room_id) { + self.displayed_regular_rooms.push(room_id.clone()); + } + if let Some(index) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { + self.displayed_direct_rooms.remove(index); + } } } @@ -663,28 +715,84 @@ impl RoomsList { error!("Error: couldn't find room {room_id} to update is_direct"); } } - RoomsListUpdate::RemoveRoom { room_id, new_state: _ } => { - if let Some(removed) = self.all_joined_rooms.remove(&room_id) { - log!("Removed room {room_id} from the list of all joined rooms"); - let list_to_remove_from = if removed.is_direct { - &mut self.displayed_direct_rooms - } else { - &mut self.displayed_regular_rooms - }; - list_to_remove_from.iter() - .position(|r| r == &room_id) - .map(|index| list_to_remove_from.remove(index)); + RoomsListUpdate::RemoveRoom { room_id, new_state } => { + if matches!(new_state, RoomState::Left | RoomState::Banned) { + if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { + room.removed_state = Some(new_state); + room.num_unread_mentions = 0; + room.num_unread_messages = 0; + } else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { + log!("Removed room {room_id} from the list of all invited rooms"); + self.displayed_invited_rooms.iter() + .position(|r| r == &room_id) + .map(|index| self.displayed_invited_rooms.remove(index)); + } + } else { + if let Some(removed) = self.all_joined_rooms.remove(&room_id) { + log!("Removed room {room_id} from the list of all joined rooms"); + let list_to_remove_from = if removed.is_direct { + &mut self.displayed_direct_rooms + } else { + &mut self.displayed_regular_rooms + }; + list_to_remove_from.iter() + .position(|r| r == &room_id) + .map(|index| list_to_remove_from.remove(index)); + } + else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { + log!("Removed room {room_id} from the list of all invited rooms"); + self.displayed_invited_rooms.iter() + .position(|r| r == &room_id) + .map(|index| self.displayed_invited_rooms.remove(index)); + } + + self.hidden_rooms.remove(&room_id); } - else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { - log!("Removed room {room_id} from the list of all invited rooms"); - self.displayed_invited_rooms.iter() - .position(|r| r == &room_id) - .map(|index| self.displayed_invited_rooms.remove(index)); + self.update_status(); + } + RoomsListUpdate::SetRemovedRoomState { room_id, removed_state } => { + if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { + if room.removed_state != removed_state { + room.removed_state = removed_state; + if room.removed_state.is_some() { + room.num_unread_mentions = 0; + room.num_unread_messages = 0; + } + } } - - self.hidden_rooms.remove(&room_id); self.update_status(); } + RoomsListUpdate::SetInviteState { room_id, invite_state } => { + if invite_state == InviteState::RoomLeft { + if self.invited_rooms.borrow_mut().remove(&room_id).is_some() { + self.displayed_invited_rooms.iter() + .position(|r| r == &room_id) + .map(|index| self.displayed_invited_rooms.remove(index)); + if let Some(joined_room) = self.all_joined_rooms.get(&room_id) { + let should_display = should_display_room!(self, &room_id, joined_room); + if should_display { + let list_to_update = if joined_room.is_direct { + &mut self.displayed_direct_rooms + } else { + &mut self.displayed_regular_rooms + }; + if !list_to_update.contains(&room_id) { + list_to_update.push(room_id.clone()); + } + } + } + self.update_status(); + } else { + warning!("Warning: couldn't find invited room {} to remove after reject", room_id); + num_updates -= 1; + } + } else if let Some(invite) = self.invited_rooms.borrow_mut().get_mut(&room_id) { + invite.invite_state = invite_state; + } else { + warning!("Warning: couldn't find invited room {} to update invite state", room_id); + num_updates -= 1; + } + } RoomsListUpdate::ClearRooms => { self.all_joined_rooms.clear(); self.displayed_direct_rooms.clear(); @@ -776,6 +884,8 @@ impl RoomsList { } if num_updates > 0 { self.redraw(cx); + // Signal other widgets (e.g., RoomScreen) to re-evaluate room state after updates. + SignalToUI::set_ui_signal(); } } @@ -1287,7 +1397,10 @@ impl Widget for RoomsList { self.current_active_room.as_ref() == Some(direct_room_id); // Paginate the room if it hasn't been paginated yet. - if PREPAGINATE_VISIBLE_ROOMS && !direct_room.has_been_paginated { + if PREPAGINATE_VISIBLE_ROOMS + && !direct_room.has_been_paginated + && direct_room.removed_state.is_none() + { direct_room.has_been_paginated = true; submit_async_request(MatrixRequest::PaginateRoomTimeline { room_id: direct_room.room_name_id.room_id().clone(), @@ -1322,7 +1435,10 @@ impl Widget for RoomsList { self.current_active_room.as_ref() == Some(regular_room_id); // Paginate the room if it hasn't been paginated yet. - if PREPAGINATE_VISIBLE_ROOMS && !regular_room.has_been_paginated { + if PREPAGINATE_VISIBLE_ROOMS + && !regular_room.has_been_paginated + && regular_room.removed_state.is_none() + { regular_room.has_been_paginated = true; submit_async_request(MatrixRequest::PaginateRoomTimeline { room_id: regular_room.room_name_id.room_id().clone(), @@ -1375,6 +1491,40 @@ impl RoomsListRef { self.borrow()?.get_room_state(room_id) } + /// See [`RoomsList::get_invite_state()`]. + pub fn get_invite_state(&self, room_id: &OwnedRoomId) -> Option { + self.borrow()?.get_invite_state(room_id) + } + + /// See [`RoomsList::get_removed_room_state()`]. + pub fn get_removed_room_state(&self, room_id: &OwnedRoomId) -> Option { + self.borrow()?.get_removed_room_state(room_id) + } + + /// Updates the removed state of a room via the RoomsList update queue. + pub fn set_removed_room_state( + &self, + room_id: OwnedRoomId, + removed_state: Option, + ) { + enqueue_rooms_list_update(RoomsListUpdate::SetRemovedRoomState { + room_id, + removed_state, + }); + } + + /// Updates the invite state of an invited room via the RoomsList update queue. + pub fn set_invite_state( + &self, + room_id: OwnedRoomId, + invite_state: InviteState, + ) { + enqueue_rooms_list_update(RoomsListUpdate::SetInviteState { + room_id, + invite_state, + }); + } + /// Returns the name of the given room, if it is known and loaded. pub fn get_room_name(&self, room_id: &OwnedRoomId) -> Option { let inner = self.borrow()?; diff --git a/src/home/rooms_list_entry.rs b/src/home/rooms_list_entry.rs index aeda32f6..ac944502 100644 --- a/src/home/rooms_list_entry.rs +++ b/src/home/rooms_list_entry.rs @@ -1,5 +1,5 @@ use makepad_widgets::*; -use matrix_sdk::ruma::OwnedRoomId; +use matrix_sdk::{RoomState, ruma::OwnedRoomId}; use crate::{ room::FetchedRoomAvatar, shared::{ @@ -8,7 +8,7 @@ use crate::{ }, utils::{self, relative_format} }; -use super::rooms_list::{InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListScopeProps}; +use super::rooms_list::{InviteState, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListScopeProps}; live_design! { use link::theme::*; use link::shaders::*; @@ -297,7 +297,20 @@ impl RoomsListEntryContent { room_info: &JoinedRoomInfo, ) { self.view.label(ids!(room_name)).set_text(cx, &room_info.room_name_id.to_string()); - if let Some((ts, msg)) = room_info.latest.as_ref() { + if let Some(removed_state) = room_info.removed_state { + let status_text = match removed_state { + RoomState::Banned => "You have been banned from this room.", + RoomState::Left => "You are no longer a member of this room.", + _ => "You are no longer a member of this room.", + }; + self.view.label(ids!(timestamp)).set_text(cx, ""); + self.view + .html_or_plaintext(ids!(latest_message)) + .show_html(cx, status_text); + self.view + .unread_badge(ids!(unread_badge)) + .update_counts(0, 0); + } else if let Some((ts, msg)) = room_info.latest.as_ref() { if let Some(human_readable_date) = relative_format(*ts) { self.view .label(ids!(timestamp)) @@ -308,9 +321,11 @@ impl RoomsListEntryContent { .show_html(cx, msg); } - self.view - .unread_badge(ids!(unread_badge)) - .update_counts(room_info.num_unread_mentions, room_info.num_unread_messages); + if room_info.removed_state.is_none() { + self.view + .unread_badge(ids!(unread_badge)) + .update_counts(room_info.num_unread_mentions, room_info.num_unread_messages); + } self.draw_common(cx, &room_info.room_avatar, room_info.is_selected); // Show tombstone icon if the room is tombstoned self.view.view(ids!(tombstone_icon)).set_visible(cx, room_info.is_tombstoned); @@ -325,11 +340,22 @@ impl RoomsListEntryContent { self.view.label(ids!(room_name)).set_text(cx, &room_info.room_name_id.to_string()); // Hide the timestamp field, and use the latest message field to show the inviter. self.view.label(ids!(timestamp)).set_text(cx, ""); - let inviter_string = match &room_info.inviter_info { + let mut inviter_string = match &room_info.inviter_info { Some(InviterInfo { user_id, display_name: Some(dn), .. }) => format!("Invited by {dn} ({user_id})"), Some(InviterInfo { user_id, .. }) => format!("Invited by {user_id}"), None => String::from("You were invited"), }; + let status_suffix = match room_info.invite_state { + InviteState::WaitingOnUserInput => "", + InviteState::WaitingForJoinResult => "Joining...", + InviteState::WaitingForLeaveResult => "Rejecting...", + InviteState::WaitingForJoinedRoom => "Waiting for room...", + InviteState::RoomLeft => "Invite rejected", + }; + if !status_suffix.is_empty() { + inviter_string.push_str(" - "); + inviter_string.push_str(status_suffix); + } self.view.html_or_plaintext(ids!(latest_message)).show_html(cx, &inviter_string); match room_info.room_avatar { diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 982f430e..6fdb3f9d 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -13,13 +13,14 @@ //! * The editing pane, which is shown when the user is editing a previous message. //! * A tombstone footer, which is shown if the room has been tombstoned (replaced). //! * A "cannot-send-message" notice, which is shown if the user cannot send messages to the room. +//! * A membership footer, which is shown if the user is no longer a room member. //! use makepad_widgets::*; -use matrix_sdk::room::reply::{EnforceThread, Reply}; +use matrix_sdk::{RoomState, room::reply::{EnforceThread, Reply}}; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; -use ruma::{events::room::message::{LocationMessageEventContent, MessageType, RoomMessageEventContent}, OwnedRoomId}; -use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt}, location_preview::LocationPreviewWidgetExt, room_screen::{populate_preview_of_timeline_item, MessageAction, RoomScreenProps}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, styles::*}, sliding_sync::{submit_async_request, MatrixRequest, UserPowerLevels}, utils}; +use ruma::{events::room::message::{LocationMessageEventContent, MessageType, RoomMessageEventContent}, room::JoinRuleSummary, OwnedRoomId}; +use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt}, location_preview::LocationPreviewWidgetExt, room_screen::{populate_preview_of_timeline_item, MessageAction, RoomScreenProps}, rooms_list::{InviteState, RoomsListRef}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, styles::*}, sliding_sync::{submit_async_request, MatrixRequest, UserPowerLevels}, utils::{self, RoomNameId}}; live_design! { use link::theme::*; @@ -159,6 +160,70 @@ live_design! { } } + membership_footer = { + visible: false + show_bg: true + draw_bg: { + color: (COLOR_SECONDARY) + } + padding: {left: 50, right: 50, top: 20, bottom: 20} + align: {y: 0.5} + width: Fill, height: Fit + flow: Down, + spacing: 8 + + status_text =