diff --git a/src/app.rs b/src/app.rs index 93eba9cb..22476ba1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -340,7 +340,7 @@ impl MatchEvent for App { // we can now navigate to it and optionally close a previous room. Some(AppStateAction::RoomLoadedSuccessfully(room_name_id)) if self.waiting_to_navigate_to_joined_room.as_ref() - .is_some_and(|(dr, _)| dr.room_name_id.room_id() == room_name_id.room_id()) => + .is_some_and(|(dr, _)| dr.room_id() == room_name_id.room_id()) => { log!("Joined awaited room {room_name_id:?}, navigating to it now..."); if let Some((dest_room, room_to_close)) = self.waiting_to_navigate_to_joined_room.take() { @@ -570,7 +570,7 @@ impl App { }); // If the successor room is not loaded, show a join modal. - let destination_room_id = destination_room.room_name_id.room_id(); + let destination_room_id = destination_room.room_id(); if !cx.get_global::().is_room_loaded(destination_room_id) { log!("Destination room {} not loaded, showing join modal...", destination_room_id); self.waiting_to_navigate_to_joined_room = Some(( @@ -584,15 +584,14 @@ impl App { return; } - log!( - "Navigating to destination room {} ({}), closing room {room_to_close:?}", - destination_room_id, - destination_room.room_name_id + log!("Navigating to destination room {:?}, closing room {:?}", + destination_room.room_name_id(), + room_to_close, ); // Select and scroll to the destination room in the rooms list. let new_selected_room = SelectedRoom::JoinedRoom { - room_name_id: destination_room.room_name_id.clone(), + room_name_id: destination_room.room_name_id().clone(), }; enqueue_rooms_list_update(RoomsListUpdate::ScrollToRoom(destination_room_id.clone())); cx.widget_action( diff --git a/src/home/invite_screen.rs b/src/home/invite_screen.rs index a6077698..4735d0c5 100644 --- a/src/home/invite_screen.rs +++ b/src/home/invite_screen.rs @@ -310,7 +310,7 @@ impl Widget for InviteScreen { self.invite_state = InviteState::WaitingForLeaveResult; if modifiers.shift { submit_async_request(MatrixRequest::LeaveRoom { - room_id: info.room_name_id.room_id().clone(), + room_id: info.room_id().clone(), }); self.has_shown_confirmation = false; } else { @@ -325,7 +325,7 @@ impl Widget for InviteScreen { self.invite_state = InviteState::WaitingForJoinResult; if modifiers.shift { submit_async_request(MatrixRequest::JoinRoom { - room_id: info.room_name_id.room_id().clone(), + room_id: info.room_id().clone(), }); self.has_shown_confirmation = false; } else { @@ -339,17 +339,17 @@ impl Widget for InviteScreen { for action in actions { match action.downcast_ref() { - Some(JoinRoomResultAction::Joined { room_id }) if room_id == info.room_name_id.room_id() => { + Some(JoinRoomResultAction::Joined { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingForJoinedRoom; if !self.has_shown_confirmation { enqueue_popup_notification(PopupItem{ message: "Successfully joined room.".into(), kind: PopupKind::Success, auto_dismissal_duration: Some(5.0) }); } continue; } - Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == info.room_name_id.room_id() => { + Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingOnUserInput; if !self.has_shown_confirmation { - let msg = utils::stringify_join_leave_error(error, &info.room_name_id, true, true); + let msg = utils::stringify_join_leave_error(error, info.room_name_id(), true, true); enqueue_popup_notification(PopupItem { message: msg, kind: PopupKind::Error, auto_dismissal_duration: None }); } continue; @@ -358,14 +358,14 @@ impl Widget for InviteScreen { } match action.downcast_ref() { - Some(LeaveRoomResultAction::Left { room_id }) if room_id == info.room_name_id.room_id() => { + Some(LeaveRoomResultAction::Left { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::RoomLeft; if !self.has_shown_confirmation { enqueue_popup_notification(PopupItem { message: "Successfully rejected invite.".into(), kind: PopupKind::Success, auto_dismissal_duration: Some(5.0) }); } continue; } - Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == info.room_name_id.room_id() => { + Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingOnUserInput; if !self.has_shown_confirmation { enqueue_popup_notification(PopupItem { message: format!("Failed to reject invite: {error}"), kind: PopupKind::Error, auto_dismissal_duration: None }); @@ -450,7 +450,7 @@ impl Widget for InviteScreen { // Second, populate the room info, if we have it. let room_view = self.view.view(ids!(room_view)); let room_avatar = room_view.avatar(ids!(room_avatar)); - match &info.room_avatar { + match &info.room_avatar() { FetchedRoomAvatar::Text(text) => { room_avatar.show_text( cx, @@ -467,7 +467,7 @@ impl Widget for InviteScreen { ); } } - let invite_room_label = info.room_name_id.to_string(); + let invite_room_label = info.room_name_id().to_string(); room_view.label(ids!(room_name)).set_text(cx, &invite_room_label); // Third, set the buttons' text based on the invite state. @@ -521,7 +521,7 @@ impl InviteScreen { .get(room_name_id.room_id()) { self.info = Some(InviteDetails { - room_info: BasicRoomDetails { + room_info: BasicRoomDetails::NameAndAvatar { room_name_id: room_name_id.clone(), room_avatar: invite.room_avatar.clone(), }, diff --git a/src/home/light_themed_dock.rs b/src/home/light_themed_dock.rs index bda1a9cb..f9c6f8cd 100644 --- a/src/home/light_themed_dock.rs +++ b/src/home/light_themed_dock.rs @@ -100,7 +100,6 @@ live_design! { } pub TabCloseButton = { - // TODO: NEEDS FOCUS STATE height: 10.0, width: 10.0, margin: { right: (THEME_SPACE_2), left: -1 }, draw_button: { diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 752b805c..49ef0f48 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -424,7 +424,7 @@ impl WidgetMatchEvent for MainDesktopUI { } // Handle actions emitted by the dock within the MainDesktopUI - match widget_action.cast() { // TODO: don't we need to call `widget_uid_eq(dock.widget_uid())` here? + match widget_action.cast() { // Whenever a tab (except for the home_tab) is pressed, notify the app state. DockAction::TabWasPressed(tab_id) => { if tab_id == id!(home_tab) { diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index bb52b6e2..97ab73d2 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -1,33 +1,31 @@ -//! The NavigationTabBar shows a bar of radio-button icons -//! on the left side bar or along the bottom. -//! These buttons allow the user navigate/switch between -//! the top-level views in Robrix. +//! The NavigationTabBar shows a bar of icon buttons that allow the user to +//! navigate or switch between various top-level views in Robrix. +//! +//! The bar is positioned either within the left side bar (in the wide "Desktop" view mode) +//! or along the bottom of the app window (in the narrow "Mobile" view mode). //! //! Their order in Mobile view (horizontally from left to right) is: -//! 1. Home [house icon]: the main view with the rooms list and the room content -//! * TODO: add a SpacesBar: a skinny scrollable PortalList showing all Spaces avatars. -//! * In the Mobile view, this will be shown horizontally on the bottom of the main view -//! (just above the current NavigationTabBar). -//! * In the Desktop view, this will be "embedded" within the NavigationTabBar itself. -//! We could optionally allow users to "pop it out", just to the right of the NavigationTabBar -//! such that it could occupy the full height of the app window. -//! 2. Add/Join [plus sign icon]: a new view to handle adding (joining) existing rooms, exploring public rooms, -//! or creating new rooms/spaces. +//! 1. Home (house icon): the main view that shows all rooms across all spaces. +//! 2. Add Room (plus sign icon): a separate view that allows adding (joining) existing rooms, +//! exploring public rooms, or creating new rooms/spaces. //! 3. Spaces: a button that toggles the `SpacesBar` (shows/hides it). -//! This is NOT a regular radio button. -//! 4. Activity [an inbox, alert bell, or notifications icon]: like Cinny, this shows a new view -//! with a list of notifications, mentions, invitations, etc. -//! 5. Profile/Settings [ProfileIcon]: the existing ProfileIcon with the verification badge. -//! Upon click, this shows the SettingsScreen as normal. +//! * This is NOT a regular radio button, it's a separate toggle. +//! * This is only shown in Mobile view mode, because the `SpacesBar` is always shown +//! within the NavigationTabBar itself in Desktop view mode. +//! 4. Activity (an inbox, alert bell, or notifications icon): a separate view that shows +//! a list of notifications, mentions, invitations, etc. +//! 5. Profile/Settings (user profile avatar): the existing `ProfileIcon` with a +//! verification badge. +//! * Upon click, this shows the SettingsScreen as normal. //! //! The order in Desktop view (vertically from top to bottom) is: -//! 1. Profile/Settings -//! 2. Home +//! 1. Home +//! 2. Add/Join //! 3. ----- separator ----- -//! (the Spaces Bar) +//! SpacesBar content //! ----- separator ----- //! 4. Activity/Inbox -//! 5. Add/Join +//! 5. Profile/Settings //! use makepad_widgets::*; @@ -331,9 +329,8 @@ impl Widget for ProfileIcon { } } - // TODO: handle login/logout actions, as well as actions related to - // the currently-logged-in user's account (such as them changing - // their avatar, display name, etc.) + // TODO: handle actions related to the currently-logged-in user account, + // such as changing their avatar, display name, etc. if let Event::Actions(actions) = event { for action in actions { diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 89b7480c..31aa813a 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -28,7 +28,7 @@ use crate::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, + room::{BasicRoomDetails, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, shared::{ avatar::AvatarWidgetRefExt, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, 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 }, @@ -1516,8 +1516,12 @@ impl RoomScreen { }); return true; } - if let Some(_known_room) = get_client().and_then(|c| c.get_room(room_id)) { - log!("TODO: jump to known room {}", room_id); + if let Some(room_name_id) = cx.get_global::().get_room_name(room_id) { + cx.action(AppStateAction::NavigateToRoom { + room_to_close: None, + destination_room: BasicRoomDetails::Name(room_name_id), + }); + return true; } else { log!("TODO: fetch and display room preview for room {}", room_id); } diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 8b345609..f698bf40 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -309,7 +309,7 @@ pub struct InvitedRoomInfo { pub inviter_info: Option, /// The timestamp and Html text content of the latest message in this room. pub latest: Option<(MilliSecondsSinceUnixEpoch, String)>, - /// The state of this how this invite is being handled by the client backend + /// The state of how this invite is being handled by the client backend /// and what should be shown in the UI. /// /// We maintain this state here instead of in the `InviteScreen` @@ -1374,6 +1374,19 @@ impl RoomsListRef { inner.is_room_loaded(room_id) } + /// 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()?; + inner.all_joined_rooms + .get(room_id) + .map(|jr| jr.room_name_id.clone()) + .or_else(|| + inner.invited_rooms.borrow() + .get(room_id) + .map(|ir| ir.room_name_id.clone()) + ) + } + /// Returns the currently-selected space (the one selected in the SpacesBar). pub fn get_selected_space(&self) -> Option { self.borrow()?.selected_space.clone() diff --git a/src/home/tombstone_footer.rs b/src/home/tombstone_footer.rs index fdff7236..e7da3fc2 100644 --- a/src/home/tombstone_footer.rs +++ b/src/home/tombstone_footer.rs @@ -120,7 +120,14 @@ impl Widget for TombstoneFooter { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { if self.view.button(ids!(join_successor_button)).clicked(actions) { - self.navigate_to_successor_room(cx, scope); + let Some(destination_room) = self.successor_info.clone() else { + error!("BUG: cannot navigate to replacement room: no successor room information."); + return; + }; + cx.action(AppStateAction::NavigateToRoom { + room_to_close: self.room_id.clone(), + destination_room, + }); } } self.view.handle_event(cx, event, scope); @@ -189,16 +196,16 @@ impl TombstoneFooter { cx, None, None, - room_preview.name.as_deref().unwrap_or("?"), + room_preview.room_name_id.name_for_avatar().as_deref().unwrap_or("?"), ); } } } - match room_preview.name.as_deref() { + match room_preview.room_name_id.name_for_avatar().as_deref() { Some(n) => successor_room_name.set_text(cx, n), - _ => successor_room_name.set_text(cx, &format!("Unnamed Room, ID: {}", room_preview.room_id)), + _ => successor_room_name.set_text(cx, &format!("Unnamed Room, ID: {}", room_preview.room_name_id.room_id())), } - self.successor_info = Some(room_preview.into()); + self.successor_info = Some(room_preview.clone().into()); } } @@ -207,22 +214,6 @@ impl TombstoneFooter { self.set_visible(cx, true); } - /// Navigate to the successor room or show join room modal if not loaded. - /// - /// If the successor room is not loaded, show a join room modal. Otherwise, - /// close the tombstone room and show the successor room in the room list. - fn navigate_to_successor_room(&mut self, cx: &mut Cx, _scope: &mut Scope) { - let Some(destination_room) = self.successor_info.clone() else { - error!("BUG: cannot navigate to replacement room: no successor room information."); - return; - }; - - cx.action(AppStateAction::NavigateToRoom { - room_to_close: self.room_id.clone(), - destination_room, - }); - } - /// Hides the tombstone footer and clears any successor room information. fn hide(&mut self, cx: &mut Cx) { self.set_visible(cx, false); diff --git a/src/join_leave_room_modal.rs b/src/join_leave_room_modal.rs index 2b4b0db0..19fbf7ce 100644 --- a/src/join_leave_room_modal.rs +++ b/src/join_leave_room_modal.rs @@ -157,7 +157,6 @@ pub enum JoinLeaveModalKind { /// The user wants to reject an invite to a room. RejectInvite(InviteDetails), /// The user wants to join a room that they have not joined yet. - #[allow(unused)] JoinRoom(BasicRoomDetails), /// The user wants to leave an already-joined room. #[allow(unused)] @@ -165,21 +164,37 @@ pub enum JoinLeaveModalKind { } impl JoinLeaveModalKind { pub fn room_id(&self) -> &OwnedRoomId { - self.room_name().room_id() + match self { + JoinLeaveModalKind::AcceptInvite(invite) + | JoinLeaveModalKind::RejectInvite(invite) => invite.room_id(), + JoinLeaveModalKind::JoinRoom(details) + |JoinLeaveModalKind::LeaveRoom(details) => details.room_id(), + } } pub fn room_name(&self) -> &RoomNameId { match self { - JoinLeaveModalKind::AcceptInvite(invite) => &invite.room_name_id, - JoinLeaveModalKind::RejectInvite(invite) => &invite.room_name_id, - JoinLeaveModalKind::JoinRoom(room) => &room.room_name_id, - JoinLeaveModalKind::LeaveRoom(room) => &room.room_name_id, + JoinLeaveModalKind::AcceptInvite(invite) + | JoinLeaveModalKind::RejectInvite(invite) => invite.room_name_id(), + JoinLeaveModalKind::JoinRoom(details) + | JoinLeaveModalKind::LeaveRoom(details) => details.room_name_id(), + } + } + + #[allow(unused)] // remove when we use it in navigate_to_room + pub fn basic_room_details(&self) -> &BasicRoomDetails { + match self { + JoinLeaveModalKind::AcceptInvite(invite) + | JoinLeaveModalKind::RejectInvite(invite) => &invite.room_info, + JoinLeaveModalKind::JoinRoom(details) + | JoinLeaveModalKind::LeaveRoom(details) => details, } } } /// Actions handled by the parent widget of the [`JoinLeaveRoomModal`]. #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum JoinLeaveRoomModalAction { /// The modal should be opened by its parent widget. Open { @@ -244,11 +259,11 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { description = format!( "Accepting an invitation to join \"{}\".\n\n\ Waiting for confirmation from the homeserver...", - invite.room_name_id, + invite.room_name_id(), ); accept_button_text = "Joining..."; submit_async_request(MatrixRequest::JoinRoom { - room_id: invite.room_name_id.room_id().clone(), + room_id: invite.room_id().clone(), }); } JoinLeaveModalKind::RejectInvite(invite) => { @@ -256,11 +271,11 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { description = format!( "Rejecting an invitation to join \"{}\".\n\n\ Waiting for confirmation from the homeserver...", - invite.room_name_id, + invite.room_name_id(), ); accept_button_text = "Rejecting..."; submit_async_request(MatrixRequest::LeaveRoom { - room_id: invite.room_name_id.room_id().clone(), + room_id: invite.room_id().clone(), }); } JoinLeaveModalKind::JoinRoom(room) => { @@ -268,11 +283,11 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { description = format!( "Joining \"{}\".\n\n\ Waiting for confirmation from the homeserver...", - room.room_name_id, + room.room_name_id(), ); accept_button_text = "Joining..."; submit_async_request(MatrixRequest::JoinRoom { - room_id: room.room_name_id.room_id().clone(), + room_id: room.room_id().clone(), }); } JoinLeaveModalKind::LeaveRoom(room) => { @@ -280,11 +295,11 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { description = format!( "Leaving \"{}\".\n\n\ Waiting for confirmation from the homeserver...", - room.room_name_id, + room.room_name_id(), ); accept_button_text = "Leaving..."; submit_async_request(MatrixRequest::LeaveRoom { - room_id: room.room_name_id.room_id().clone(), + room_id: room.room_id().clone(), }); } } @@ -409,6 +424,8 @@ impl JoinLeaveRoomModal { self.final_success = None; } + /// Populates this modal with the proper info based on + /// the given `kind of join or leave action. fn set_kind( &mut self, cx: &mut Cx, @@ -425,7 +442,7 @@ impl JoinLeaveRoomModal { title = "Accept this invite?"; description = format!( "Are you sure you want to accept this invite to join \"{}\"?", - invite.room_name_id, + invite.room_name_id(), ); tip_button = "Join"; } @@ -435,7 +452,7 @@ impl JoinLeaveRoomModal { "Are you sure you want to reject this invite to join \"{}\"?\n\n\ If this is a private room, you won't be able to join this room \ without being re-invited to it.", - invite.room_name_id + invite.room_name_id() ); tip_button = "Reject"; } @@ -443,7 +460,7 @@ impl JoinLeaveRoomModal { title = "Join this room?"; description = format!( "Are you sure you want to join \"{}\"?", - room.room_name_id + room.room_name_id() ); tip_button = "Join"; } @@ -453,7 +470,7 @@ impl JoinLeaveRoomModal { "Are you sure you want to leave \"{}\"?\n\n\ If this is a private room, you won't be able to join this room \ without being re-invited to it.", - room.room_name_id + room.room_name_id() ); tip_button = "Leave"; } @@ -494,8 +511,7 @@ impl JoinLeaveRoomModalRef { kind: JoinLeaveModalKind, show_tip: bool, ) { - if let Some(mut inner) = self.borrow_mut() { - inner.set_kind(cx, kind, show_tip); - } + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_kind(cx, kind, show_tip); } } diff --git a/src/room/mod.rs b/src/room/mod.rs index ef92a1a3..cc885025 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -1,8 +1,11 @@ -use std::{ops::Deref, sync::Arc}; +//! Widgets, types, and functions related to a Matrix room. + +use std::sync::Arc; use makepad_widgets::Cx; -use matrix_sdk::{room_preview::RoomPreview, RoomDisplayName, SuccessorRoom}; +use matrix_sdk::{RoomDisplayName, RoomHero, RoomState, SuccessorRoom, room_preview::RoomPreview}; +use ruma::{OwnedRoomAliasId, OwnedRoomId, room::{JoinRuleSummary, RoomType}}; -use crate::utils::{avatar_from_room_name, RoomNameId}; +use crate::utils::RoomNameId; pub mod reply_preview; pub mod room_input_bar; @@ -15,32 +18,72 @@ pub fn live_design(cx: &mut Cx) { typing_notice::live_design(cx); } -/// Basic details needed to display a brief summary of a room. +/// Info about a room, either partially or completely known. /// -/// You can construct this manually, but it also can be created from a -/// [`SuccessorRoom`] or a [`FetchedRoomPreview`]. +/// This is useful to represent a room that you may want to know more about, +/// but haven't yet fetched the full room preview, +/// such as a room to navigate to, or a room to join. #[derive(Clone, Debug)] -pub struct BasicRoomDetails { - pub room_name_id: RoomNameId, - pub room_avatar: FetchedRoomAvatar, +pub enum BasicRoomDetails { + /// We only know the room's ID, so we'll need to fetch the room preview + /// to get more info about the room. + // Implementation Note: instead of just a room ID, this variant contains + // a `RoomNameId` with an `Empty` name so that we can borrow it + // in the `room_name_id()` method below. + RoomId(RoomNameId), + /// We know the room's displayable name, but nothing else. + Name(RoomNameId), + /// We know the room's name and have fetched its full avatar, + /// but not any auxiliary info like topic, aliases, etc. + NameAndAvatar { + room_name_id: RoomNameId, + room_avatar: FetchedRoomAvatar, + }, + /// We have fetched the full preview for this room, + /// including its avatar and all other possible info about it. + FetchedRoomPreview(FetchedRoomPreview), } impl From<&SuccessorRoom> for BasicRoomDetails { - fn from(successor_room: &SuccessorRoom) -> Self { - BasicRoomDetails { - room_name_id: RoomNameId::new(RoomDisplayName::Empty, successor_room.room_id.clone()), - room_avatar: avatar_from_room_name(None), - } + fn from(sr: &SuccessorRoom) -> Self { + BasicRoomDetails::RoomId(RoomNameId::empty(sr.room_id.clone())) + } +} +impl From for BasicRoomDetails { + fn from(frp: FetchedRoomPreview) -> Self { + BasicRoomDetails::FetchedRoomPreview(frp) } } -impl From<&FetchedRoomPreview> for BasicRoomDetails { - fn from(frp: &FetchedRoomPreview) -> Self { - let room_name = frp.name.clone() - .map(RoomDisplayName::Named) - .unwrap_or(RoomDisplayName::Empty); - let room_name = RoomNameId::new(room_name, frp.room_id.clone()); - BasicRoomDetails { - room_name_id: room_name, - room_avatar: frp.room_avatar.clone(), +impl BasicRoomDetails { + pub fn room_id(&self) -> &OwnedRoomId { + match self { + Self::RoomId(room_name_id) + | Self::Name(room_name_id) + | Self::NameAndAvatar { room_name_id, ..} => room_name_id.room_id(), + Self::FetchedRoomPreview(frp) => frp.room_name_id.room_id(), + } + } + + /// Returns the displayable name of this room. + /// + /// If this is the `RoomId` variant, the name will be `Empty`. + pub fn room_name_id(&self) -> &RoomNameId { + match self { + Self::RoomId(room_name_id) + | Self::Name(room_name_id) + | Self::NameAndAvatar { room_name_id, .. } => room_name_id, + Self::FetchedRoomPreview(frp) => &frp.room_name_id, + } + } + + /// Returns the fetched avatar of this room. + /// + /// If this is the `RoomId` or `Name` variants, the avatar will be empty. + pub fn room_avatar(&self) -> &FetchedRoomAvatar { + match self { + Self::RoomId(_) + | Self::Name(_) => &EMPTY_AVATAR, + Self::NameAndAvatar { room_avatar, ..} => room_avatar, + Self::FetchedRoomPreview(frp) => &frp.room_avatar, } } } @@ -52,19 +95,66 @@ pub enum RoomPreviewAction { Fetched(Result), } -/// A [`RoomPreview`] from the Matrix SDK, plus the room's fetched avatar. -#[derive(Debug)] +/// A modified [`RoomPreview`], augmented with the room's fetched avatar. +#[derive(Clone, Debug)] pub struct FetchedRoomPreview { - pub room_preview: RoomPreview, + /// The room's ID and displayable name. + pub room_name_id: RoomNameId, + /// The room's fetched avatar, ready to be displayed. pub room_avatar: FetchedRoomAvatar, + + // Below: copied from the `RoomPreview` struct. + + /// The canonical alias for the room. + pub canonical_alias: Option, + /// The room's topic, if set. + pub topic: Option, + /// The number of joined members. + pub num_joined_members: u64, + /// The number of active members, if known (joined + invited). + pub num_active_members: Option, + /// The room type (space, custom) or nothing, if it's a regular room. + pub room_type: Option, + /// What's the join rule for this room? + pub join_rule: Option, + /// Is the room world-readable (i.e. is its history_visibility set to + /// world_readable)? + pub is_world_readable: Option, + /// Has the current user been invited/joined/left this room? + /// + /// Set to `None` if the room is unknown to the user. + pub state: Option, + /// The `m.room.direct` state of the room, if known. + pub is_direct: Option, + /// Room heroes. + pub heroes: Option>, } -impl Deref for FetchedRoomPreview { - type Target = RoomPreview; - fn deref(&self) -> &Self::Target { - &self.room_preview +impl FetchedRoomPreview { + pub fn from(room_preview: RoomPreview, room_avatar: FetchedRoomAvatar) -> Self { + let display_name = room_preview.name.map_or( + RoomDisplayName::Empty, + RoomDisplayName::Named, + ); + Self { + room_name_id: RoomNameId::new(display_name, room_preview.room_id), + room_avatar, + canonical_alias: room_preview.canonical_alias, + topic: room_preview.topic, + num_joined_members: room_preview.num_joined_members, + num_active_members: room_preview.num_active_members, + room_type: room_preview.room_type, + join_rule: room_preview.join_rule, + is_world_readable: room_preview.is_world_readable, + state: room_preview.state, + is_direct: room_preview.is_direct, + heroes: room_preview.heroes, + } } } + +static EMPTY_AVATAR: FetchedRoomAvatar = FetchedRoomAvatar::Text(String::new()); + /// A fully-fetched room avatar ready to be displayed. #[derive(Clone)] pub enum FetchedRoomAvatar { diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index c5e6ea4e..1c481b06 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -2745,10 +2745,7 @@ async fn fetch_room_preview_with_avatar( // The successor room did not have an avatar URL avatar_from_room_name(room_preview.name.as_deref()) }; - Ok(FetchedRoomPreview { - room_preview, - room_avatar, - }) + Ok(FetchedRoomPreview::from(room_preview, room_avatar)) } diff --git a/src/utils.rs b/src/utils.rs index 3eb9ebe7..693cf9f9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -669,11 +669,16 @@ pub struct RoomNameId { } impl RoomNameId { - /// Create a new RoomName with the given display name and room ID. + /// Create a new `RoomNameId` with the given display name and room ID. pub fn new(display_name: RoomDisplayName, room_id: OwnedRoomId) -> Self { Self { display_name, room_id } } + /// Creates a new `RoomNameId` with an empty display name. + pub fn empty(room_id: OwnedRoomId) -> Self { + Self::new(RoomDisplayName::Empty, room_id) + } + /// Get a reference to the underlying display name. #[inline] pub fn display_name(&self) -> &RoomDisplayName { @@ -686,7 +691,7 @@ impl RoomNameId { &self.room_id } - /// Check if the display name is Empty (not EmptyWas or other variants). + /// Returns `true` if the display name is `Empty` only (not `EmptyWas` or other). #[inline] pub fn is_empty(&self) -> bool { matches!(self.display_name, RoomDisplayName::Empty) @@ -709,7 +714,7 @@ impl RoomNameId { } /// Convert into the inner display name and room ID. - pub fn into_parts(self) -> (RoomDisplayName, OwnedRoomId) { + pub fn into_inner(self) -> (RoomDisplayName, OwnedRoomId) { (self.display_name, self.room_id) } }