diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 06fedf54..bedb2832 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SelectedRoom}, utils::room_name_or_id}; +use crate::{app::{AppState, AppStateAction, SelectedRoom}, room::loading_screen::{RoomLoadingScreenAction, RoomLoadingScreenWidgetRefExt, drain_room_loading_screen_actions, loading_tab_live_id}, utils::room_name_or_id}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; live_design! { @@ -17,6 +17,7 @@ live_design! { use crate::home::welcome_screen::WelcomeScreen; use crate::home::room_screen::RoomScreen; use crate::home::invite_screen::InviteScreen; + use crate::room::loading_screen::RoomLoadingScreen; pub MainDesktopUI = {{MainDesktopUI}} { dock = { @@ -54,6 +55,7 @@ live_design! { welcome_screen = {} room_screen = {} invite_screen = {} + loading_screen = { visible: true } } } } @@ -67,6 +69,10 @@ pub struct MainDesktopUI { #[rust] open_rooms: HashMap, + /// Tabs that are currently showing a loading screen (tab_id -> last message). + #[rust] + loading_tabs: HashMap, Option)>, + /// The tab that should be closed in the next draw event #[rust] tab_to_close: Option, @@ -92,6 +98,20 @@ pub struct MainDesktopUI { impl Widget for MainDesktopUI { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + // Apply queued loading tab actions when the UI thread is signalled. + if let Event::Signal = event { + for action in drain_room_loading_screen_actions() { + match action { + RoomLoadingScreenAction::ShowTab { tab_id, tab_name, message, details } => { + self.show_loading_tab(cx, tab_id, &tab_name, message.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); } @@ -121,6 +141,10 @@ impl MainDesktopUI { // 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()); + 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 self.open_rooms.contains_key(&room_id_as_live_id) { dock.select_tab(cx, room_id_as_live_id); self.most_recently_selected_room = Some(room); @@ -204,6 +228,8 @@ impl MainDesktopUI { dock.select_tab(cx, id!(home_tab)); self.most_recently_selected_room = None; } + } else if self.loading_tabs.remove(&tab_id).is_some() { + // Nothing else to do; just close the loading tab. } dock.close_tab(cx, tab_id); @@ -226,6 +252,57 @@ impl MainDesktopUI { self.tab_to_close = None; self.room_order.clear(); self.most_recently_selected_room = None; + + for tab_id in self.loading_tabs.keys().copied().collect::>() { + dock.close_tab(cx, tab_id); + } + self.loading_tabs.clear(); + } + + /// Show or create a loading-only tab. + fn show_loading_tab(&mut self, cx: &mut Cx, tab_id: LiveId, tab_name: &str, message: 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, message, details); + self.loading_tabs.insert(tab_id, (message.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, message, details); + self.loading_tabs.insert(tab_id, (message.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}"); + } + } + + /// Close a loading-only tab if it exists. + 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); + } } /// Replaces an invite with a joined room in the dock. diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index f903c0bf..083cb5f1 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -30,7 +30,7 @@ use crate::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, + room::{loading_screen::{loading_tab_live_id, show_room_loading_tab}, room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, shared::{ avatar::AvatarWidgetRefExt, callout_tooltip::{CalloutTooltipOptions, TooltipAction, TooltipPosition}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, @@ -1483,8 +1483,14 @@ impl RoomScreen { log!("TODO: jump to known room {}", room_id); } else { log!("TODO: fetch and display room preview for room {}", room_id); + show_room_loading_tab( + loading_tab_live_id(room_id.as_str()), + "Loading...", + Some("Loading...".to_string()), + Some("Loading Room ID...".to_string()), + ); } - false + true } MatrixId::RoomAlias(room_alias) => { log!("TODO: open room alias {}", room_alias); @@ -1492,7 +1498,13 @@ impl RoomScreen { // while our background async task calls Client::resolve_room_alias() // and then either jumps to the room if known, or fetches and displays // a room preview for that room. - false + show_room_loading_tab( + loading_tab_live_id(room_alias.alias()), + "Loading...", + Some("Loading...".to_string()), + Some("Loading Room Alias...".to_string()), + ); + true } MatrixId::Event(room_id, event_id) => { log!("TODO: open event {} in room {}", event_id, room_id); @@ -4174,4 +4186,4 @@ pub fn clear_timeline_states(_cx: &mut Cx) { TIMELINE_STATES.with_borrow_mut(|states| { states.clear(); }); -} \ No newline at end of file +} diff --git a/src/room/loading_screen.rs b/src/room/loading_screen.rs new file mode 100644 index 00000000..27dfb307 --- /dev/null +++ b/src/room/loading_screen.rs @@ -0,0 +1,180 @@ +//! A dock tab loading view for room operations. +//! Use `show_room_loading_tab()` / `hide_room_loading_tab()` to toggle. + +use crossbeam_queue::SegQueue; +use makepad_widgets::*; + +/// Actions for loading tabs inside the dock. +#[derive(Debug, Clone)] +pub enum RoomLoadingScreenAction { + /// Show or create a loading tab with the given id/name/message. + ShowTab { + tab_id: LiveId, + tab_name: String, + message: Option, + details: Option, + }, + /// Close the loading tab with the given id. + HideTab { + tab_id: LiveId, + }, +} + +/// Pending actions that should be applied on the UI thread. +static PENDING_LOADING_ACTIONS: SegQueue = SegQueue::new(); + +/// Show (or create) a dock tab that only contains a room loading screen. +/// +/// `tab_id` should be unique within the dock; a common pattern is +/// `LiveId::from_str(room_id.as_str())` or `LiveId::from_str(&format!(\"loading_{room_id}\"))`. +pub fn show_room_loading_tab( + tab_id: LiveId, + tab_name: impl Into, + message: impl Into>, + details: impl Into>, +) { + PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::ShowTab { + tab_id, + tab_name: tab_name.into(), + message: message.into(), + details: details.into(), + }); + SignalToUI::set_ui_signal(); +} + +/// Hide and close the loading tab with the given id, if it exists. +pub fn hide_room_loading_tab(tab_id: LiveId) { + PENDING_LOADING_ACTIONS.push(RoomLoadingScreenAction::HideTab { tab_id }); + SignalToUI::set_ui_signal(); +} + +/// Drain all pending actions for loading tabs. +pub fn drain_room_loading_screen_actions( +) -> impl Iterator { + std::iter::from_fn(|| PENDING_LOADING_ACTIONS.pop()) +} + +/// Deterministic helper to derive a unique LiveId for a loading tab +/// from any stable string (e.g., room id or alias). This keeps the same tab +/// reusable across multiple jumps/clicks. +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 =