From 3a2daf19d0c46c8bf9952221eb09f068ddde616a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Sun, 18 Jan 2026 17:40:50 +0800 Subject: [PATCH 1/2] Add room loading screen and loading tabs Introduce RoomLoadingScreen with tab actions, and use it in room, invite, and space lobby screens to replace the old restore status UI. --- src/home/invite_screen.rs | 47 ++++++---- src/home/main_desktop_ui.rs | 71 ++++++++++++++- src/home/room_screen.rs | 49 +++++++++-- src/home/space_lobby.rs | 12 ++- src/room/loading_screen.rs | 170 ++++++++++++++++++++++++++++++++++++ src/room/mod.rs | 2 + 6 files changed, 324 insertions(+), 27 deletions(-) create mode 100644 src/room/loading_screen.rs diff --git a/src/home/invite_screen.rs b/src/home/invite_screen.rs index dc412f35..605029e9 100644 --- a/src/home/invite_screen.rs +++ b/src/home/invite_screen.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{app::AppStateAction, home::rooms_list::RoomsListRef, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt}, sliding_sync::{submit_async_request, MatrixRequest}, utils::{self, RoomNameId}}; +use crate::{app::AppStateAction, home::rooms_list::RoomsListRef, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::{BasicRoomDetails, FetchedRoomAvatar, loading_screen::RoomLoadingScreenWidgetExt}, shared::{avatar::AvatarWidgetRefExt, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, submit_async_request}, utils::{self, RoomNameId}}; use super::rooms_list::{InviteState, InviterInfo}; @@ -22,7 +22,7 @@ live_design! { use crate::shared::styles::*; use crate::shared::avatar::*; use crate::shared::icon_button::*; - use crate::shared::restore_status_view::*; + use crate::room::loading_screen::RoomLoadingScreen; pub InviteScreen = {{InviteScreen}} { width: Fill, @@ -36,7 +36,7 @@ live_design! { draw_bg: { color: (COLOR_PRIMARY_DARKER), } - restore_status_view = {} + loading_screen = { visible: false } // This view is only shown if `inviter` is Some. inviter_view = { @@ -394,11 +394,14 @@ impl Widget for InviteScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { 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); - } - return restore_status_view.draw(cx, scope); + let mut loading_screen = self.view.room_loading_screen(ids!(loading_screen)); + let (title, details) = if let Some(room_name) = &self.room_name_id { + self.loading_screen_content(room_name) + } else { + ("Loading...".to_string(), None) + }; + loading_screen.show(cx, Some(&title), details.as_deref()); + return loading_screen.draw(cx, scope); } let Some(info) = self.info.as_ref() else { // If we don't have any info, just return. @@ -534,16 +537,28 @@ impl InviteScreen { self.redraw(cx); } - let restore_status_view = self.view.restore_status_view(ids!(restore_status_view)); + let loading_screen = self.view.room_loading_screen(ids!(loading_screen)); if !self.is_loaded { - restore_status_view.set_content( - cx, - self.all_rooms_loaded, - room_name_id, - ); - restore_status_view.set_visible(cx, true); + let (title, details) = self.loading_screen_content(room_name_id); + loading_screen.show(cx, Some(&title), details.as_deref()); + } else { + loading_screen.hide(cx); + } + } + + fn loading_screen_content(&self, room_name: &RoomNameId) -> (String, Option) { + if self.all_rooms_loaded { + ( + format!( + "Room {room_name} was not found in the homeserver's list of all rooms." + ), + Some("You may close this screen.".to_owned()), + ) } else { - restore_status_view.set_visible(cx, false); + ( + "Waiting for this room to be loaded from the homeserver".to_owned(), + None, + ) } } } diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 49ef0f48..43409d63 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, utils::RoomNameId}; +use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, room::loading_screen::{RoomLoadingScreenAction, RoomLoadingScreenWidgetRefExt, get_room_loading_screen_actions, loading_tab_live_id}, utils::RoomNameId}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; live_design! { @@ -18,6 +18,7 @@ live_design! { use crate::home::room_screen::RoomScreen; use crate::home::invite_screen::InviteScreen; use crate::home::space_lobby::SpaceLobbyScreen; + use crate::room::loading_screen::RoomLoadingScreen; pub MainDesktopUI = {{MainDesktopUI}} { dock = { @@ -59,6 +60,7 @@ live_design! { room_screen = {} invite_screen = {} space_lobby_screen = {} + loading_screen = { visible: true } } } } @@ -77,6 +79,9 @@ pub struct MainDesktopUI { #[rust] open_rooms: HashMap, + #[rust] + loading_tabs: HashMap, Option)>, + /// The tab that should be closed in the next draw event #[rust] tab_to_close: Option, @@ -114,6 +119,19 @@ impl LiveHook for MainDesktopUI { impl Widget for MainDesktopUI { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Event::Signal = event { + for action in get_room_loading_screen_actions() { + match action { + RoomLoadingScreenAction::ShowTab { tab_id, tab_name, title, details } => { + self.show_loading_tab(cx, tab_id, &tab_name, title.as_deref(), details.as_deref()); + } + RoomLoadingScreenAction::HideTab { tab_id } => { + self.close_loading_tab(cx, tab_id); + } + } + } + } + self.widget_match_event(cx, event, scope); // invokes `WidgetMatchEvent` impl self.view.handle_event(cx, event, scope); } @@ -143,6 +161,11 @@ impl MainDesktopUI { let dock = self.view.dock(ids!(dock)); + let loading_id = loading_tab_live_id(room.room_id().as_str()); + if self.loading_tabs.remove(&loading_id).is_some() { + dock.close_tab(cx, loading_id); + } + // If the room is already open, select (jump to) its existing tab let room_id_as_live_id = LiveId::from_str(room.room_id().as_str()); if self.open_rooms.contains_key(&room_id_as_live_id) { @@ -389,6 +412,52 @@ impl MainDesktopUI { app_state.selected_room = selected_room; self.redraw(cx); } + + fn show_loading_tab(&mut self, cx: &mut Cx, tab_id: LiveId, tab_name: &str, title: Option<&str>, details: Option<&str>) { + let dock_ref = self.view.dock(ids!(dock)); + + // If the tab already exists and is a loading tab, just update it and select it. + let mut should_select_existing = false; + if let Some(mut dock) = dock_ref.borrow_mut() { + if let Some((_, widget)) = dock.items().get(&tab_id) { + widget.as_room_loading_screen().show(cx, title, details); + self.loading_tabs.insert(tab_id, (title.map(str::to_owned), details.map(str::to_owned))); + should_select_existing = true; + } + } + + if should_select_existing { + dock_ref.select_tab(cx, tab_id); + return; + } + + // Otherwise, create a new loading tab at the end. + let (tab_bar, _pos) = dock_ref.find_tab_bar_of_tab(id!(home_tab)).unwrap(); + let new_tab_widget = dock_ref.create_and_select_tab( + cx, + tab_bar, + tab_id, + id!(loading_screen), + tab_name.to_string(), + id!(CloseableTab), + None, + ); + + if let Some(widget) = new_tab_widget { + widget.as_room_loading_screen().show(cx, title, details); + self.loading_tabs.insert(tab_id, (title.map(str::to_owned), details.map(str::to_owned))); + dock_ref.select_tab(cx, tab_id); + } else { + error!("BUG: failed to create loading tab for {tab_name}"); + } + + } + + fn close_loading_tab(&mut self, cx: &mut Cx, tab_id: LiveId) { + if self.loading_tabs.remove(&tab_id).is_some() { + self.view.dock(ids!(dock)).close_tab(cx, tab_id); + } + } } impl WidgetMatchEvent for MainDesktopUI { diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 5a0799c2..4edf949c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -29,9 +29,9 @@ use crate::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{BasicRoomDetails, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, + room::{BasicRoomDetails, loading_screen::RoomLoadingScreenWidgetExt, 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 + 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}, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} }; @@ -78,7 +78,7 @@ live_design! { use crate::room::typing_notice::*; use crate::home::room_read_receipt::*; use crate::rooms_list::*; - use crate::shared::restore_status_view::*; + use crate::room::loading_screen::RoomLoadingScreen; use crate::home::link_preview::LinkPreview; use link::tsp_link::TspSignIndicator; @@ -516,7 +516,7 @@ live_design! { color: (COLOR_PRIMARY_DARKER) } - restore_status_view = {} + loading_screen = { visible: false } // Widgets within this view will get shifted upwards when the on-screen keyboard is shown. keyboard_view = { @@ -976,15 +976,16 @@ impl Widget for RoomScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - // If the room isn't loaded yet, we show the restore status label only. + // If the room isn't loaded yet, we show the loading screen only. if !self.is_loaded { let Some(room_name) = &self.room_name_id else { // No room selected yet, nothing to show. 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); - return restore_status_view.draw(cx, scope); + let mut loading_screen = self.view.room_loading_screen(ids!(loading_screen)); + let (title, details) = self.loading_screen_content(room_name); + loading_screen.show(cx, Some(&title), details.as_deref()); + return loading_screen.draw(cx, scope); } if self.tl_state.is_none() { // Tl_state may not be ready after dock loading. @@ -1166,6 +1167,36 @@ impl RoomScreen { self.room_name_id.as_ref().map(|r| r.room_id()) } + fn loading_screen_content(&self, room_name: &RoomNameId) -> (String, Option) { + if self.all_rooms_loaded { + ( + format!( + "Room {room_name} was not found in the homeserver's list of all rooms." + ), + Some("You may close this screen.".to_owned()), + ) + } else { + ( + "Waiting for this room to be loaded from the homeserver".to_owned(), + None, + ) + } + } + + fn update_loading_screen(&mut self, cx: &mut Cx) { + let loading_screen = self.view.room_loading_screen(ids!(loading_screen)); + if self.is_loaded { + loading_screen.hide(cx); + return; + } + let Some(room_name) = &self.room_name_id else { + loading_screen.hide(cx); + return; + }; + let (title, details) = self.loading_screen_content(room_name); + loading_screen.show(cx, Some(&title), details.as_deref()); + } + /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. @@ -2154,7 +2185,7 @@ impl RoomScreen { self.is_loaded = is_loaded_now; } - self.view.restore_status_view(ids!(restore_status_view)).set_visible(cx, !self.is_loaded); + self.update_loading_screen(cx); // Kick off a back pagination request if it's the first time loading this room, // because we want to show the user some messages as soon as possible diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index 2a6f27b6..63738a74 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -14,7 +14,7 @@ use matrix_sdk_ui::spaces::SpaceRoom; use ruma::room::JoinRuleSummary; use tokio::sync::mpsc::UnboundedSender; use crate::{ - home::rooms_list::RoomsListRef, shared::avatar::{AvatarState, AvatarWidgetExt, AvatarWidgetRefExt}, space_service_sync::{SpaceRequest, SpaceRoomExt, SpaceRoomListAction}, utils::{self, RoomNameId} + home::rooms_list::RoomsListRef, room::loading_screen::RoomLoadingScreenWidgetExt, shared::avatar::{AvatarState, AvatarWidgetExt, AvatarWidgetRefExt}, space_service_sync::{SpaceRequest, SpaceRoomExt, SpaceRoomListAction}, utils::{self, RoomNameId} }; @@ -26,6 +26,7 @@ live_design! { use crate::shared::styles::*; use crate::shared::helpers::*; use crate::shared::avatar::*; + use crate::room::loading_screen::RoomLoadingScreen; ICON_COLLAPSE = dep("crate://self/resources/icons/triangle_fill.svg") @@ -378,6 +379,8 @@ live_design! { color: #fff } + loading_screen = { visible: false } + // Header with parent space info header = { width: Fill, @@ -739,6 +742,13 @@ impl Widget for SpaceLobbyScreen { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + if self.is_loading { + let mut loading_screen = self.view.room_loading_screen(ids!(loading_screen)); + loading_screen.show(cx, Some("Loading rooms and spaces..."), None); + return loading_screen.draw(cx, scope); + } + self.view.room_loading_screen(ids!(loading_screen)).hide(cx); + while let Some(widget_to_draw) = self.view.draw_walk(cx, scope, walk).step() { let portal_list_ref = widget_to_draw.as_portal_list(); let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; diff --git a/src/room/loading_screen.rs b/src/room/loading_screen.rs new file mode 100644 index 00000000..1fbda94f --- /dev/null +++ b/src/room/loading_screen.rs @@ -0,0 +1,170 @@ +use crossbeam_queue::SegQueue; +use makepad_widgets::*; + +/// Pending actions that should be applied on the UI thread. +static PENDING_LOADING_ACTIONS: SegQueue = SegQueue::new(); + +pub fn show_room_loading_tab( + tab_id: LiveId, + tab_name: impl Into, + title: impl Into>, + details: impl Into>, +) { + PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::ShowTab { + tab_id, + tab_name: tab_name.into(), + title: title.into(), + details: details.into(), + }); + SignalToUI::set_ui_signal(); +} + +pub fn hide_room_loading_tab(tab_id: LiveId) { + PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::HideTab { tab_id }); + SignalToUI::set_ui_signal(); +} + +pub fn get_room_loading_screen_actions() -> impl Iterator { + std::iter::from_fn(|| PENDING_LOADING_ACTIONS.pop()) +} + +pub fn loading_tab_live_id(key: &str) -> LiveId { + LiveId::from_str(&format!("loading_{key}")) +} + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::shared::helpers::*; + use crate::shared::styles::*; + + pub RoomLoadingScreen = {{RoomLoadingScreen}} { + width: Fill, height: Fill, + flow: Down, + align: {x: 0.5, y: 0.5}, + spacing: 10.0, + + show_bg: true, + draw_bg: { + color: (COLOR_PRIMARY_DARKER), + } + + loading_spinner = { + width: 60, + height: 60, + visible: true, + draw_bg: { + color: (COLOR_ACTIVE_PRIMARY) + border_size: 4.0, + } + } + + title =