From bd14ffa20066cc4bb3a16b7953b440ae49e3b898 Mon Sep 17 00:00:00 2001 From: Tyrese Luo Date: Fri, 31 Oct 2025 20:52:22 +0800 Subject: [PATCH 1/2] Add room preview support and "Cannot Preview" screen; support PreviewedRoom in Dock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add src/room/can_not_preview_screen.rs: a UI for rooms that cannot be previewed or joined, with a Join button / messaging. - Introduce PreviewedRoom variant in SelectedRoom and propagate through persistence and UI: - src/app.rs: import reordering and PreviewedRoom handling for room_id/room_name getters. - src/persistence/app_state.rs: include PreviewedRoom when saving dock state. - Desktop & mobile UI updates for room previews: - src/home/main_desktop_ui.rs: register CanNotPreviewScreen, implement focus_or_crate_preview_screen_tab to create/select preview tabs, and handle room preview fetch results (show preview or can't-preview screen). - src/home/main_mobile_ui.rs: small logic fix (handle None | _ case). - Improved matrix link handling in room UI: - src/home/room_screen.rs: enhanced handling of Matrix links (user, room id/alias, event links) — jump to known rooms (handling different RoomState cases) or request room preview for unknown rooms. - Serialization / utils: - src/utils.rs: add OwnedServerNameRon wrapper to support RON (de)serialization of OwnedServerName and adjust imports. - Misc: - Register new can_not_preview_screen module in src/room/mod.rs. - Tidy imports and adapt code to new flow. Rationale: Improve UX when clicking room links by showing a room preview or a clear message when the room cannot be previewed or joined. Treat previewed rooms as first-class tabs in the Dock and persist them in app state for consistent restore behaviour. --- src/app.rs | 11 +- src/home/main_desktop_ui.rs | 91 +++++++++++++++- src/home/main_mobile_ui.rs | 2 +- src/home/room_screen.rs | 109 +++++++++++++++---- src/persistence/app_state.rs | 3 +- src/room/can_not_preview_screen.rs | 168 +++++++++++++++++++++++++++++ src/room/mod.rs | 2 + src/utils.rs | 58 ++++++++++ 8 files changed, 416 insertions(+), 28 deletions(-) create mode 100644 src/room/can_not_preview_screen.rs diff --git a/src/app.rs b/src/app.rs index f7029e9cd..06eeed9b0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,7 +10,7 @@ use makepad_widgets::{makepad_micro_serde::*, *}; use matrix_sdk::ruma::{OwnedRoomId, RoomId}; use crate::{ avatar_cache::clear_avatar_cache, home::{ - main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{clear_timeline_states, MessageAction}, rooms_list::{clear_all_invited_rooms, enqueue_rooms_list_update, RoomsListAction, RoomsListRef, RoomsListUpdate} + main_desktop_ui::MainDesktopUiAction, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_screen::{MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update} }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::callout_tooltip::{ @@ -18,8 +18,7 @@ use crate::{ CalloutTooltipWidgetRefExt, TooltipAction, }, sliding_sync::current_user_id, utils::{ - room_name_or_id, - OwnedRoomIdRon, + OwnedRoomIdRon, room_name_or_id }, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, @@ -630,6 +629,10 @@ pub enum SelectedRoom { room_id: OwnedRoomIdRon, room_name: Option, }, + PreviewedRoom { + room_id: OwnedRoomIdRon, + room_name: Option, + }, } impl SelectedRoom { @@ -637,6 +640,7 @@ impl SelectedRoom { match self { SelectedRoom::JoinedRoom { room_id, .. } => room_id, SelectedRoom::InvitedRoom { room_id, .. } => room_id, + SelectedRoom::PreviewedRoom { room_id, .. } => room_id, } } @@ -644,6 +648,7 @@ impl SelectedRoom { match self { SelectedRoom::JoinedRoom { room_name, .. } => room_name.as_ref(), SelectedRoom::InvitedRoom { room_name, .. } => room_name.as_ref(), + SelectedRoom::PreviewedRoom { room_name, .. } => room_name.as_ref(), } } diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 06fedf540..aeef5db06 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::{BasicRoomDetails, FetchedRoomPreview, RoomPreviewAction, can_not_preview_screen::{CanNotPreviewDetails, CanNotPreviewScreenWidgetRefExt}}, shared::popup_list::{PopupItem, PopupKind, enqueue_popup_notification}, 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::can_not_preview_screen::CanNotPreviewScreen; pub MainDesktopUI = {{MainDesktopUI}} { dock = { @@ -54,6 +55,7 @@ live_design! { welcome_screen = {} room_screen = {} invite_screen = {} + can_not_preview_screen = {} } } } @@ -138,6 +140,7 @@ impl MainDesktopUI { id!(invite_screen), room_name_or_id(room_name.as_ref(), room_id), ), + _ => return }; let new_tab_widget = dock.create_and_select_tab( cx, @@ -168,6 +171,7 @@ impl MainDesktopUI { room.room_name().cloned() ); } + _ => {} } cx.action(MainDesktopUiAction::SaveDockIntoAppState); } else { @@ -178,6 +182,77 @@ impl MainDesktopUI { self.most_recently_selected_room = Some(room); } + fn focus_or_crate_preview_screen_tab(&mut self, cx: &mut Cx, preview_info: &FetchedRoomPreview) { + let room_id = preview_info.room_id.clone(); + let room_name = preview_info.name.clone(); + let is_world_readable = preview_info.room_preview.is_world_readable.unwrap_or(false); + + let dock = self.view.dock(ids!(dock)); + let selected_room = SelectedRoom::PreviewedRoom { room_id: room_id.to_owned().into(), room_name: room_name.clone() }; + + // Do nothing if the room to select is already created and focused. + if self.most_recently_selected_room.as_ref().is_some_and(|r| r == &selected_room) { + return; + } + + // If the room is already open, select (jump to) its existing tab + let room_id_as_live_id = LiveId::from_str(room_id.as_str()); + 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(selected_room); + return; + } + + + // Create a new tab for the room preview + let (tab_bar, _pos) = dock.find_tab_bar_of_tab(id!(home_tab)).unwrap(); + let (kind, name) = if is_world_readable { + (id!(room_screen), room_name_or_id(room_name.as_ref(), &room_id)) + } else { + (id!(can_not_preview_screen), room_name_or_id(room_name.as_ref(), &room_id)) + }; + + let new_tab_widget = dock.create_and_select_tab( + cx, + tab_bar, + room_id_as_live_id, + kind, + name, + id!(CloseableTab), + None, + ); + + if let Some(new_widget) = new_tab_widget { + self.room_order.push(selected_room.clone()); + if is_world_readable { + new_widget.as_room_screen().set_displayed_room( + cx, + room_id.clone().into(), + selected_room.room_name().cloned(), + ); + } else { + new_widget.as_can_not_preview_screen().set_displayed( + cx, + room_id.clone().into(), + selected_room.room_name().cloned(), + CanNotPreviewDetails { + room_basic_details: BasicRoomDetails { + room_id: room_id.clone(), + room_name: room_name.clone(), + room_avatar: preview_info.room_avatar.clone(), + }, + join_rule: preview_info.room_preview.join_rule.clone(), + }, + ); + } + cx.action(MainDesktopUiAction::SaveDockIntoAppState); + } else { + error!("BUG: failed to create tab for {selected_room:?}"); + } + self.open_rooms.insert(room_id_as_live_id, selected_room.clone()); + self.most_recently_selected_room = Some(selected_room); + } + /// Closes a tab in the dock and focuses on the latest open room. fn close_tab(&mut self, cx: &mut Cx, tab_id: LiveId) { let dock = self.view.dock(ids!(dock)); @@ -282,6 +357,20 @@ impl WidgetMatchEvent for MainDesktopUI { continue; } + if let Some(RoomPreviewAction::Fetched(res)) = action.downcast_ref() { + match res { + Ok(room_preview) => self.focus_or_crate_preview_screen_tab(cx, room_preview), + Err(err) => { + error!("Failed to fetch room preview: {}", err); + enqueue_popup_notification(PopupItem { + message: "This room preview is not available.".to_string(), + kind: PopupKind::Error, + auto_dismissal_duration: None + }); + }, + } + } + // 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? // Whenever a tab (except for the home_tab) is pressed, notify the app state. diff --git a/src/home/main_mobile_ui.rs b/src/home/main_mobile_ui.rs index 1772348b7..76f87ebfa 100644 --- a/src/home/main_mobile_ui.rs +++ b/src/home/main_mobile_ui.rs @@ -90,7 +90,7 @@ impl Widget for MainMobileUI { .invite_screen(ids!(invite_screen)) .set_displayed_invite(cx, room_id.clone().into(), room_name.clone()); } - None => { + None | _ => { show_welcome = true; show_room = false; show_invite = false; diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4b582d94a..abc5064b9 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -7,34 +7,32 @@ use bytesize::ByteSize; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - room::RoomMember, ruma::{ - events::{ + OwnedServerName, RoomState, room::RoomMember, ruma::{ + EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ receipt::Receipt, room::{ - message::{ + ImageInfo, MediaSource, message::{ AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent - }, - ImageInfo, MediaSource + } }, sticker::{StickerEventContent, StickerMediaSource}, - }, - matrix_uri::MatrixId, uint, EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId - }, OwnedServerName + }, matrix_uri::MatrixId, uint + } }; use matrix_sdk_ui::timeline::{ self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem }; 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_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::RoomsListRef, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppStateAction, SelectedRoom}, 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_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, room::{room_input_bar::RoomInputBarState, typing_notice::TypingNoticeWidgetExt}, shared::{ - avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{enqueue_popup_notification, PopupItem, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt + avatar::AvatarWidgetRefExt, callout_tooltip::TooltipAction, 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 }, - sliding_sync::{get_client, submit_async_request, take_timeline_endpoints, BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineRequestSender, UserPowerLevels}, utils::{self, room_name_or_id, unix_time_millis_to_datetime, ImageFormat, MEDIA_THUMBNAIL_FORMAT} + sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, room_name_or_id, unix_time_millis_to_datetime} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -1435,9 +1433,10 @@ impl RoomScreen { action: &Action, pane: &UserProfileSlidingPaneRef, ) -> bool { + let uid = self.widget_uid(); // A closure that handles both MatrixToUri and MatrixUri links, // and returns whether the link was handled. - let mut handle_matrix_link = |id: &MatrixId, _via: &[OwnedServerName]| -> bool { + let mut handle_matrix_link = |id: &MatrixId, via: &[OwnedServerName]| -> bool { match id { MatrixId::User(user_id) => { // There is no synchronous way to get the user's full profile info @@ -1474,20 +1473,86 @@ 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(known_room) = get_client().and_then(|c| c.get_room(room_id)) { + if known_room.is_space() { + // TODO: Show space home page + enqueue_popup_notification(PopupItem { + message: "Showing a space's home page is not yet supported.".into(), + kind: PopupKind::Warning, + auto_dismissal_duration: Some(3.0) + }); + } + + if known_room.is_tombstoned() { + // TODO: To join the successor room, we need to: known_room.tombstone_content() + enqueue_popup_notification(PopupItem { + message: "This room has been replaced by another room. You must join the new room.".into(), + kind: PopupKind::Warning, + auto_dismissal_duration: None + }); + } + + match known_room.state() { + RoomState::Joined => { + cx.widget_action( + uid, + &Scope::empty().path, + RoomsListAction::Selected( + SelectedRoom::JoinedRoom { + room_id: known_room.room_id().to_owned().into(), + room_name: known_room.name(), + } + ) + ); + } + RoomState::Invited => { + cx.widget_action( + uid, + &Scope::empty().path, + RoomsListAction::Selected( + SelectedRoom::InvitedRoom { + room_id: known_room.room_id().to_owned().into(), + room_name: known_room.name(), + } + ) + ); + } + RoomState::Knocked => { + enqueue_popup_notification(PopupItem { + message: "Already knocked. Waiting for approval.".into(), + kind: PopupKind::Info, + auto_dismissal_duration: None + }); + } + RoomState::Banned => { + enqueue_popup_notification(PopupItem { + message: "You are banned from that room. You cannot join. Unless the admin lifts the ban.".into(), + kind: PopupKind::Error, + auto_dismissal_duration: None + }); + } + RoomState::Left => { + enqueue_popup_notification(PopupItem { + message: "You have left that room. You must be re-invited to join again.".into(), + kind: PopupKind::Info, + auto_dismissal_duration: None + }); + } + } } else { - log!("TODO: fetch and display room preview for room {}", room_id); + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id: room_id.to_owned().into(), + via: via.to_owned(), + }); } - false + true } MatrixId::RoomAlias(room_alias) => { - log!("TODO: open room alias {}", room_alias); - // TODO: open a room loading screen that shows a spinner - // 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 + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id: room_alias.to_owned().into(), + via: via.to_owned(), + }); + true } MatrixId::Event(room_id, event_id) => { log!("TODO: open event {} in room {}", event_id, room_id); diff --git a/src/persistence/app_state.rs b/src/persistence/app_state.rs index 0944f790d..aef0935cf 100644 --- a/src/persistence/app_state.rs +++ b/src/persistence/app_state.rs @@ -33,7 +33,8 @@ pub fn save_app_state( for (tab_id, room) in &app_state.saved_dock_state.open_rooms { match room { SelectedRoom::JoinedRoom { room_id, .. } - | SelectedRoom::InvitedRoom { room_id, .. } => { + | SelectedRoom::InvitedRoom { room_id, .. } + | SelectedRoom::PreviewedRoom { room_id, .. } => { if !app_state.saved_dock_state.dock_items.contains_key(tab_id) { error!("Room id: {} already in dock state", room_id); } diff --git a/src/room/can_not_preview_screen.rs b/src/room/can_not_preview_screen.rs new file mode 100644 index 000000000..68d1f5999 --- /dev/null +++ b/src/room/can_not_preview_screen.rs @@ -0,0 +1,168 @@ +use std::ops::Deref; + +use makepad_widgets::*; +use ruma::{OwnedRoomId, room::JoinRuleSummary}; + +use crate::{app::AppStateAction, room::BasicRoomDetails, shared::restore_status_view::RestoreStatusViewWidgetExt, utils::room_name_or_id}; + +live_design! { + use link::theme::*; + use link::shaders::*; + use link::widgets::*; + + use crate::shared::helpers::*; + use crate::shared::styles::*; + use crate::shared::avatar::*; + use crate::shared::icon_button::*; + use crate::shared::restore_status_view::*; + + pub CanNotPreviewScreen = {{CanNotPreviewScreen}} { + width: Fill, + height: Fill, + flow: Down, + align: {x: 0.5, y: 0.5}, + spacing: 0, + + show_bg: true, + draw_bg: { + color: (COLOR_PRIMARY_DARKER), + } + restore_status_view = {} + + preview_message =