diff --git a/src/app.rs b/src/app.rs index f7029e9c..3a658ad3 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, OwnedRoomOrAliasIdRon, OwnedServerNameRon, 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 06fedf54..64f9cdeb 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -1,9 +1,10 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; +use ruma::{OwnedRoomAliasId, OwnedRoomOrAliasId}; 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}, home::room_screen::handle_switch_to_known_room, room::{self, BasicRoomDetails, RoomAliasResolutionAction, preview_screen::{PreviewDetails, PreviewScreenWidgetRefExt, RoomPreviewAction}}, sliding_sync::{MatrixRequest, submit_async_request}, utils::room_name_or_id}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; live_design! { @@ -17,6 +18,7 @@ live_design! { use crate::home::welcome_screen::WelcomeScreen; use crate::home::room_screen::RoomScreen; use crate::home::invite_screen::InviteScreen; + use crate::room::preview_screen::PreviewScreen; pub MainDesktopUI = {{MainDesktopUI}} { dock = { @@ -54,6 +56,7 @@ live_design! { welcome_screen = {} room_screen = {} invite_screen = {} + preview_screen = {} } } } @@ -88,6 +91,9 @@ pub struct MainDesktopUI { /// If true, this widget proceeds to draw the desktop UI as normal. #[rust] drawn_previously: bool, + + #[rust] + already_resolved_aliases: HashMap, } impl Widget for MainDesktopUI { @@ -138,6 +144,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 +175,7 @@ impl MainDesktopUI { room.room_name().cloned() ); } + _ => {} } cx.action(MainDesktopUiAction::SaveDockIntoAppState); } else { @@ -178,6 +186,70 @@ impl MainDesktopUI { self.most_recently_selected_room = Some(room); } + fn focus_or_create_preview_tab(&mut self, cx: &mut Cx, room: SelectedRoom, preview_details: PreviewDetails) { + let dock = self.view.dock(ids!(dock)); + + if self.most_recently_selected_room.as_ref().is_some_and(|r| r == &room) { + return; + } + + 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) { + dock.select_tab(cx, room_id_as_live_id); + self.most_recently_selected_room = Some(room); + return; + } + + let (tab_bar, _pos) = match dock.find_tab_bar_of_tab(id!(home_tab)) { + Some(found) => found, + None => { + error!("BUG: unable to find home_tab tab bar when creating preview tab"); + return; + } + }; + + let (kind, name) = match &room { + SelectedRoom::PreviewedRoom { room_id, room_name } => ( + id!(preview_screen), + room_name_or_id(room_name.as_ref(), room_id), + ), + _ => { + error!("BUG: focus_or_create_preview_tab called with non-preview room: {:?}", room); + return; + } + }; + + log!("Creating preview tab for room {:?}", name); + + let new_tab_widget = dock.create_and_select_tab( + cx, + tab_bar, + room_id_as_live_id, + kind, + name.clone(), + id!(CloseableTab), + None, + ); + + if let Some(new_widget) = new_tab_widget { + self.room_order.push(room.clone()); + new_widget.as_preview_screen().set_displayed_preview( + cx, + room.room_id().clone(), + room.room_name().cloned(), + preview_details.clone(), + ); + + cx.action(MainDesktopUiAction::SaveDockIntoAppState); + } else { + error!("BUG: failed to create preview tab for {room:?}"); + return; + } + + self.open_rooms.insert(room_id_as_live_id, room.clone()); + self.most_recently_selected_room = Some(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)); @@ -272,6 +344,7 @@ impl MainDesktopUI { impl WidgetMatchEvent for MainDesktopUI { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + let uid = self.widget_uid(); let mut should_save_dock_action: bool = false; for action in actions { let widget_action = action.as_widget_action(); @@ -282,6 +355,79 @@ impl WidgetMatchEvent for MainDesktopUI { continue; } + if let Some(RoomAliasResolutionAction::Resolved(room_alias, result)) = action.downcast_ref() { + match result { + Ok(data) => { + let room_id = &data.room_id; + let server_names = &data.servers; + + // Skip if we've already resolved this alias + if self.already_resolved_aliases.contains_key(room_alias) { + log!("Alias {} already resolved, skipping.", room_alias); + return; + } + + self.already_resolved_aliases + .insert(room_alias.clone(), room_id.clone()); + + // Try to switch to the known room + if !handle_switch_to_known_room(cx, uid, room_id.clone()) { + cx.widget_action( + uid, + &Scope::empty().path, + RoomPreviewAction::Selected { + room_or_alias_id: OwnedRoomOrAliasId::from(room_id.clone()), + via: server_names.to_vec(), + } + ); + } + } + Err(e) => { + error!("Failed to resolve alias {}: {}", room_alias, e); + } + } + } + + if let Some(room::RoomPreviewAction::Fetched(res)) = action.downcast_ref() { + match res { + Ok(data) => { + let BasicRoomDetails { + room_id, + room_name, + room_avatar, + } = BasicRoomDetails { + room_id: data.room_id.clone(), + room_name: data.name.clone(), + room_avatar: data.room_avatar.clone(), + }; + + let preview_details = PreviewDetails { + room_basic_details: BasicRoomDetails { + room_id: room_id.clone(), + room_name: room_name.clone(), + room_avatar, + }, + is_world_readable: data.is_world_readable.unwrap_or(false), + join_rule: data.join_rule.clone(), + }; + + log!("Opening room preview for {} (name: {:?})", room_id, room_name); + + self.focus_or_create_preview_tab( + cx, + SelectedRoom::PreviewedRoom { + room_id: room_id.into(), + room_name: room_name.clone(), + }, + preview_details, + ); + } + Err(e) => { + error!("Failed to fetch room preview: {e}"); + } + } + } + // 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. @@ -333,6 +479,36 @@ impl WidgetMatchEvent for MainDesktopUI { _ => (), } + match widget_action.cast() { + RoomPreviewAction::Selected { room_or_alias_id, via } => { + match OwnedRoomAliasId::try_from(room_or_alias_id) { + // We have a room alias ID to resolve it to a room ID, and then switch to it. + Ok(alias_id) => { + if let Some(room_id) = self.already_resolved_aliases.get(&alias_id) { + if handle_switch_to_known_room(cx, uid, room_id.clone()) { + return; + } + // If we couldn't switch to the known room, we need to fetch the preview info. + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id: OwnedRoomOrAliasId::from(room_id.clone()), + via: via.clone(), + }); + } else { + submit_async_request(MatrixRequest::ResolveRoomAlias(alias_id)); + } + } + // This Err returns means we have a room ID, so we can directly fetch the preview info. + Err(room_id) => { + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id: OwnedRoomOrAliasId::from(room_id.clone()), + via: via.clone() + }); + } + } + } + RoomPreviewAction::None => { } + } + // Handle RoomsList actions, which are updates from the rooms list. match widget_action.cast() { RoomsListAction::Selected(selected_room) => { diff --git a/src/home/main_mobile_ui.rs b/src/home/main_mobile_ui.rs index 1772348b..103fc963 100644 --- a/src/home/main_mobile_ui.rs +++ b/src/home/main_mobile_ui.rs @@ -95,6 +95,11 @@ impl Widget for MainMobileUI { show_room = false; show_invite = false; } + _ => { + show_welcome = false; + show_room = false; + show_invite = false; + } } self.view.view(ids!(welcome)).set_visible(cx, show_welcome); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4b582d94..3acd2e7b 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -7,34 +7,33 @@ 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 ruma::OwnedRoomOrAliasId; 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}, + room::{preview_screen::RoomPreviewAction, 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; @@ -566,6 +565,8 @@ pub struct RoomScreen { #[rust] is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). #[rust] all_rooms_loaded: bool, + /// Whether this room is being previewed (not fully joined). + #[rust] is_preview: bool, } impl Drop for RoomScreen { fn drop(&mut self) { @@ -1435,9 +1436,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 +1476,30 @@ 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); - } else { - log!("TODO: fetch and display room preview for room {}", room_id); + // Try to switch to an already-known room first. + if !handle_switch_to_known_room(cx, uid, room_id.to_owned()) { + // Otherwise, send a RoomPreviewAction to preview/join the room. + cx.widget_action( + uid, + &Scope::empty().path, + RoomPreviewAction::Selected { + room_or_alias_id: OwnedRoomOrAliasId::from(room_id.clone()).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 + cx.widget_action( + uid, + &Scope::empty().path, + RoomPreviewAction::Selected { + room_or_alias_id: OwnedRoomOrAliasId::from(room_alias.clone()).into(), + via: via.to_owned() + } + ); + true } MatrixId::Event(room_id, event_id) => { log!("TODO: open event {} in room {}", event_id, room_id); @@ -2085,6 +2097,92 @@ impl RoomScreen { self.redraw(cx); } + fn show_preview_timeline(&mut self, cx: &mut Cx) { + let room_id = self.room_id.clone() + .expect("BUG: Timeline::show_preview_timeline(): no room_id was set."); + + let state_opt = TIMELINE_STATES.with_borrow_mut(|ts| ts.remove(&room_id)); + let (mut tl_state, is_first_time_being_loaded) = if let Some(existing) = state_opt { + (existing, false) + } else { + let Some(timeline_endpoints) = take_timeline_endpoints(&room_id) else { + return; + }; + let TimelineEndpoints { + update_receiver, + update_sender, + request_sender, + // we don't need the successor room for preview timelines + successor_room: _, + } = timeline_endpoints; + + let tl_state = TimelineUiState { + room_id: room_id.clone(), + // For preview timelines, we set the user power level to read-only, + user_power: UserPowerLevels::read_only(), + room_members: None, + fully_paginated: false, + items: Vector::new(), + content_drawn_since_last_update: RangeSet::new(), + profile_drawn_since_last_update: RangeSet::new(), + update_receiver, + request_sender, + media_cache: MediaCache::new(Some(update_sender.clone())), + link_preview_cache: LinkPreviewCache::new(Some(update_sender)), + saved_state: SavedState::default(), + message_highlight_animation_state: MessageHighlightAnimationState::default(), + last_scrolled_index: usize::MAX, + prev_first_index: None, + scrolled_past_read_marker: false, + latest_own_user_receipt: None, + tombstone_info: None, + }; + (tl_state, true) + }; + + log!("Setting preview timeline for room \"{}\" ({}) as loaded", self.room_name, room_id); + + self.is_loaded = true; + + log!("Showing preview timeline for room \"{}\" ({})", self.room_name, room_id); + + if is_first_time_being_loaded { + if !tl_state.fully_paginated { + log!("Sending a first-time backwards pagination request for room \"{}\" {}", self.room_name, room_id); + submit_async_request(MatrixRequest::PaginateRoomTimeline { + room_id: room_id.clone(), + num_events: 50, + direction: PaginationDirection::Backwards, + }); + } + submit_async_request(MatrixRequest::SyncRoomMemberList { room_id: room_id.clone() }); + } + + self.view(ids!(typing_notice)).set_visible(cx, false); + if self.is_loaded { + submit_async_request(MatrixRequest::GetRoomPowerLevels { + room_id: room_id.clone(), + }); + submit_async_request(MatrixRequest::GetRoomMembers { + room_id: room_id.clone(), + memberships: matrix_sdk::RoomMemberships::JOIN, + // Because we're previewing, we get members from server. + local_only: false, + }); + submit_async_request(MatrixRequest::SubscribeToPinnedEvents { + room_id: room_id.clone(), + subscribe: true, + }); + } + + self.restore_state(cx, &mut tl_state); + self.tl_state = Some(tl_state); + + self.process_timeline_updates(cx, &self.portal_list(ids!(list))); + + self.redraw(cx); + } + /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown. fn hide_timeline(&mut self) { let Some(room_id) = self.room_id.clone() else { return }; @@ -2110,6 +2208,19 @@ impl RoomScreen { }); } + fn hide_preview_timeline(&mut self) { + let Some(room_id) = self.room_id.clone() else { return; }; + + self.save_state(); + + // The preview timeline only subscribes the pinned events when shown, + // so we only need to unsubscribe from that here. + submit_async_request(MatrixRequest::SubscribeToPinnedEvents { + room_id, + subscribe: false, + }); + } + /// Removes the current room's visual UI state from this widget /// and saves it to the map of `TIMELINE_STATES` such that it can be restored later. /// @@ -2173,6 +2284,11 @@ impl RoomScreen { // If the room is already being displayed, then do nothing. if self.room_id.as_ref().is_some_and(|id| id == &room_id) { return; } + // If we were previously displaying a preview timeline, hide it first. + if self.is_preview { + self.hide_preview_timeline(); + } + self.hide_timeline(); // Reset the the state of the inner loading pane. self.loading_pane(ids!(loading_pane)).take_state(); @@ -2190,6 +2306,34 @@ impl RoomScreen { self.show_timeline(cx); } + pub fn set_displayed_preview_room>>( + &mut self, + cx: &mut Cx, + room_id: OwnedRoomId, + room_name: S, + ) { + if self.room_id.as_ref().is_some_and(|id| id == &room_id) { return; } + + // If we were previously displaying a normal timeline, hide it first. + if !self.is_preview { + self.hide_timeline(); + } + + self.hide_preview_timeline(); + // Reset the the state of the inner loading pane. + self.loading_pane(ids!(loading_pane)).take_state(); + self.room_name = room_name_or_id(room_name.into(), &room_id); + self.room_id = Some(room_id.clone()); + self.is_preview = true; + + cx.action(MentionableTextInputAction::PowerLevelsUpdated { + room_id: room_id.clone(), + can_notify_room: false, + }); + + self.show_preview_timeline(cx); + } + /// Sends read receipts based on the current scroll position of the timeline. fn send_user_read_receipts_based_on_scroll_pos( &mut self, @@ -2300,6 +2444,90 @@ impl RoomScreenRef { let Some(mut inner) = self.borrow_mut() else { return }; inner.set_displayed_room(cx, room_id, room_name); } + + /// See [`RoomScreen::set_displayed_preview_room()`]. + pub fn set_displayed_preview_room>>( + &self, + cx: &mut Cx, + room_id: OwnedRoomId, + room_name: S, + ) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_displayed_preview_room(cx, room_id, room_name); + } +} + +/// Attempts to switch to a known room with the given `room_id`. +pub fn handle_switch_to_known_room(cx: &mut Cx, uid: WidgetUid, room_id: OwnedRoomId) -> bool { + 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 + }); + } + } + return true; + } + false } /// Immutable RoomScreen states passed via Scope props diff --git a/src/persistence/app_state.rs b/src/persistence/app_state.rs index 0944f790..aef0935c 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/mod.rs b/src/room/mod.rs index 77181c45..3be2e26f 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -1,6 +1,7 @@ use std::{ops::Deref, sync::Arc}; -use makepad_widgets::Cx; +use makepad_widgets::*; use matrix_sdk::{room_preview::RoomPreview, ruma::OwnedRoomId, SuccessorRoom}; +use ruma::{OwnedRoomAliasId, api::client::alias::get_alias}; use crate::utils::avatar_from_room_name; @@ -8,11 +9,13 @@ pub mod reply_preview; pub mod room_input_bar; pub mod room_display_filter; pub mod typing_notice; +pub mod preview_screen; pub fn live_design(cx: &mut Cx) { reply_preview::live_design(cx); room_input_bar::live_design(cx); typing_notice::live_design(cx); + preview_screen::live_design(cx); } /// Basic details needed to display a brief summary of a room. @@ -51,8 +54,13 @@ pub enum RoomPreviewAction { Fetched(Result), } -/// A [`RoomPreview`] from the Matrix SDK, plus the room's fetched avatar. #[derive(Debug)] +pub enum RoomAliasResolutionAction { + Resolved(OwnedRoomAliasId, Result), +} + +/// A [`RoomPreview`] from the Matrix SDK, plus the room's fetched avatar. +#[derive(Debug, Clone)] pub struct FetchedRoomPreview { pub room_preview: RoomPreview, pub room_avatar: FetchedRoomAvatar, diff --git a/src/room/preview_screen.rs b/src/room/preview_screen.rs new file mode 100644 index 00000000..d3b87434 --- /dev/null +++ b/src/room/preview_screen.rs @@ -0,0 +1,202 @@ +use std::ops::Deref; + +use makepad_widgets::*; +use ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, room::JoinRuleSummary}; + +use crate::{home::room_screen::RoomScreenWidgetRefExt, 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::*; + use crate::home::room_screen::RoomScreen; + + pub PreviewScreen = {{PreviewScreen}} { + width: Fill, height: Fill, + flow: Down, + spacing: 0, + + show_bg: true, + draw_bg: { + color: (COLOR_PRIMARY_DARKER), + } + restore_status_view = {} + + room_preview_screen_wrapper = { + width: Fill, height: Fill, + flow: Down, + show_bg: true, + draw_bg: { + color: (COLOR_PRIMARY_DARKER) + } + + can_not_preview_screen = { + visible: false, + width: Fill, height: Fill, + align: {x: 0.5, y: 0.5}, + flow: Down, + spacing: 10, + + preview_message =