From a859d40fe78be19ab382621f8701fa9ecac60a38 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 24 Mar 2026 09:58:19 -0700 Subject: [PATCH 01/18] Refactor how the stack nav history is maintained (in HomeScreen, not App) this makes a bit more sense imo, as the top-level App shouldn't really be responsible for dealing with these details. --- src/app.rs | 130 +--------------------------------------- src/home/home_screen.rs | 129 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 130 deletions(-) diff --git a/src/app.rs b/src/app.rs index f04e177d..b5df23ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ - event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, invite_screen::InviteScreenWidgetRefExt, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, RoomScreenWidgetRefExt, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, space_lobby::SpaceLobbyScreenWidgetRefExt + event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, 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::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ @@ -171,9 +171,6 @@ pub struct App { /// This can be either a room we're waiting to join, or one we're waiting to be invited to. /// Also includes an optional room ID to be closed once the awaited room has been loaded. #[rust] waiting_to_navigate_to_room: Option<(BasicRoomDetails, Option)>, - /// A stack of previously-selected rooms for mobile navigation. - /// When a view is popped off the stack, the previous `selected_room` is restored from here. - #[rust] mobile_room_nav_stack: Vec, } impl ScriptHook for App { @@ -357,33 +354,6 @@ impl MatchEvent for App { continue; } - // A new room has been selected; push the appropriate view onto the mobile - // StackNavigation and update the app state. - // In Desktop mode, MainDesktopUI also handles this action to manage dock tabs; - // the mobile push is harmless there (the view isn't drawn). - match action.as_widget_action().cast() { - RoomsListAction::Selected(selected_room) => { - self.push_selected_room_view(cx, selected_room); - continue; - } - // An invite was accepted; upgrade the selected room from invite to joined. - // In Desktop mode, MainDesktopUI also handles this (harmless duplicate). - RoomsListAction::InviteAccepted { room_name_id } => { - cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name_id.room_id().clone())); - continue; - } - _ => {} - } - - // When a stack navigation pop is initiated (back button pressed), - // pop the mobile nav stack so it stays in sync with StackNavigation. - if let StackNavigationAction::Pop = action.as_widget_action().cast() { - if self.app_state.selected_room.is_some() { - self.app_state.selected_room = self.mobile_room_nav_stack.pop(); - } - // Don't `continue` — let StackNavigation also process this Pop. - } - // Handle actions that instruct us to update the top-level app state. match action.downcast_ref() { Some(AppStateAction::RoomFocused(selected_room)) => { @@ -826,104 +796,6 @@ impl App { } } - /// Room StackNavigationView instances, one per stack depth. - /// Each depth gets its own dedicated view widget to avoid - /// complex state save/restore when views would otherwise be reused. - const ROOM_VIEW_IDS: [LiveId; 16] = [ - live_id!(room_view_0), live_id!(room_view_1), - live_id!(room_view_2), live_id!(room_view_3), - live_id!(room_view_4), live_id!(room_view_5), - live_id!(room_view_6), live_id!(room_view_7), - live_id!(room_view_8), live_id!(room_view_9), - live_id!(room_view_10), live_id!(room_view_11), - live_id!(room_view_12), live_id!(room_view_13), - live_id!(room_view_14), live_id!(room_view_15), - ]; - - /// The RoomScreen widget IDs inside each room view, - /// corresponding 1:1 with [`Self::ROOM_VIEW_IDS`]. - const ROOM_SCREEN_IDS: [LiveId; 16] = [ - live_id!(room_screen_0), live_id!(room_screen_1), - live_id!(room_screen_2), live_id!(room_screen_3), - live_id!(room_screen_4), live_id!(room_screen_5), - live_id!(room_screen_6), live_id!(room_screen_7), - live_id!(room_screen_8), live_id!(room_screen_9), - live_id!(room_screen_10), live_id!(room_screen_11), - live_id!(room_screen_12), live_id!(room_screen_13), - live_id!(room_screen_14), live_id!(room_screen_15), - ]; - - /// Returns the room view and room screen LiveIds for the given stack depth. - /// Clamps to the last available view if depth exceeds the pool size. - fn room_ids_for_depth(depth: usize) -> (LiveId, LiveId) { - let index = depth.min(Self::ROOM_VIEW_IDS.len() - 1); - (Self::ROOM_VIEW_IDS[index], Self::ROOM_SCREEN_IDS[index]) - } - - /// Pushes the appropriate StackNavigationView for the given `SelectedRoom`, - /// configuring the view's content widget and header title. - /// - /// Each stack depth gets its own dedicated room view widget, - /// supporting deep navigation (room → thread → room → thread → ...). - /// - /// In Desktop mode, the StackNavigation isn't drawn, so the push and - /// screen configuration are effectively no-ops — MainDesktopUI handles - /// room display via dock tabs instead. - fn push_selected_room_view(&mut self, cx: &mut Cx, selected_room: SelectedRoom) { - // Use the actual StackNavigation depth to pick the next room view slot. - let new_depth = self.ui.stack_navigation(cx, ids!(view_stack)).depth(); - - // Determine which view to push and configure its content. - // The `set_displayed_room` / `set_displayed_invite` / `set_displayed_space` calls - // configure the screen widget inside the mobile StackNavigationView. - // In Desktop mode, these widgets exist but aren't drawn; the configuration - // consumes timeline endpoints, but Desktop's MainDesktopUI processes the same - // `RoomsListAction::Selected` in its own handler to set up dock tabs. - let view_id = match &selected_room { - SelectedRoom::JoinedRoom { room_name_id } - | SelectedRoom::Thread { room_name_id, .. } => { - let (view_id, room_screen_id) = Self::room_ids_for_depth(new_depth); - - let thread_root = if let SelectedRoom::Thread { thread_root_event_id, .. } = &selected_room { - Some(thread_root_event_id.clone()) - } else { - None - }; - self.ui - .room_screen(cx, &[room_screen_id]) - .set_displayed_room(cx, room_name_id, thread_root); - - view_id - } - SelectedRoom::InvitedRoom { room_name_id } => { - self.ui - .invite_screen(cx, ids!(invite_screen)) - .set_displayed_invite(cx, room_name_id); - id!(invite_view) - } - SelectedRoom::Space { space_name_id } => { - self.ui - .space_lobby_screen(cx, ids!(space_lobby_screen)) - .set_displayed_space(cx, space_name_id); - id!(space_lobby_view) - } - }; - - // Set the header title for the view being pushed. - let title_path = &[view_id, live_id!(header), live_id!(content), live_id!(title_container), live_id!(title)]; - self.ui.label(cx, title_path).set_text(cx, &selected_room.display_name()); - - // Save the current selected_room onto the navigation stack before replacing it. - if let Some(prev) = self.app_state.selected_room.take() { - self.mobile_room_nav_stack.push(prev); - } - // Update app state (used by both Desktop and Mobile paths). - self.app_state.selected_room = Some(selected_room); - - // Push the view onto the mobile navigation stack. - self.ui.stack_navigation(cx, ids!(view_stack)).push(cx, view_id); - self.ui.redraw(cx); - } } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 910f817e..5fcd8c29 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,6 +1,16 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; +use crate::{ + app::{AppState, AppStateAction, SelectedRoom}, + home::{ + invite_screen::InviteScreenWidgetExt, + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room_screen::RoomScreenWidgetExt, + rooms_list::RoomsListAction, + space_lobby::SpaceLobbyScreenWidgetExt, + }, + settings::settings_screen::SettingsScreenWidgetRefExt, +}; script_mod! { use mod.prelude.widgets.* @@ -406,6 +416,10 @@ pub struct HomeScreen { /// other widgets can easily access it. #[rust] previous_selection: SelectedTab, #[rust] is_spaces_bar_shown: bool, + + /// A stack of previously-selected rooms for mobile stack navigation. + /// When a view is popped off the stack, the previous `selected_room` is restored. + #[rust] mobile_room_nav_stack: Vec, } impl Widget for HomeScreen { @@ -475,6 +489,29 @@ impl Widget for HomeScreen { Some(NavigationBarAction::TabSelected(_)) | None => { } } + + // Handle mobile stack navigation actions (push/pop room views). + // In Desktop mode, MainDesktopUI also handles RoomsListAction::Selected + // to manage dock tabs; the mobile push is harmless there (views aren't drawn). + match action.as_widget_action().cast() { + RoomsListAction::Selected(selected_room) => { + self.push_selected_room_view(cx, app_state, selected_room); + } + RoomsListAction::InviteAccepted { room_name_id } => { + cx.action(AppStateAction::UpgradedInviteToJoinedRoom( + room_name_id.room_id().clone(), + )); + } + _ => {} + } + + // When a stack navigation pop is initiated (back button pressed), + // pop the mobile nav stack so it stays in sync with StackNavigation. + if let StackNavigationAction::Pop = action.as_widget_action().cast() { + if app_state.selected_room.is_some() { + app_state.selected_room = self.mobile_room_nav_stack.pop(); + } + } } } @@ -511,5 +548,95 @@ impl HomeScreen { }, ) } + + /// Room StackNavigationView instances, one per stack depth. + /// Each depth gets its own dedicated view widget to avoid + /// complex state save/restore when views would otherwise be reused. + const ROOM_VIEW_IDS: [LiveId; 16] = [ + live_id!(room_view_0), live_id!(room_view_1), + live_id!(room_view_2), live_id!(room_view_3), + live_id!(room_view_4), live_id!(room_view_5), + live_id!(room_view_6), live_id!(room_view_7), + live_id!(room_view_8), live_id!(room_view_9), + live_id!(room_view_10), live_id!(room_view_11), + live_id!(room_view_12), live_id!(room_view_13), + live_id!(room_view_14), live_id!(room_view_15), + ]; + + /// The RoomScreen widget IDs inside each room view, + /// corresponding 1:1 with [`Self::ROOM_VIEW_IDS`]. + const ROOM_SCREEN_IDS: [LiveId; 16] = [ + live_id!(room_screen_0), live_id!(room_screen_1), + live_id!(room_screen_2), live_id!(room_screen_3), + live_id!(room_screen_4), live_id!(room_screen_5), + live_id!(room_screen_6), live_id!(room_screen_7), + live_id!(room_screen_8), live_id!(room_screen_9), + live_id!(room_screen_10), live_id!(room_screen_11), + live_id!(room_screen_12), live_id!(room_screen_13), + live_id!(room_screen_14), live_id!(room_screen_15), + ]; + + /// Returns the room view and room screen LiveIds for the given stack depth. + /// Clamps to the last available view if depth exceeds the pool size. + fn room_ids_for_depth(depth: usize) -> (LiveId, LiveId) { + let index = depth.min(Self::ROOM_VIEW_IDS.len() - 1); + (Self::ROOM_VIEW_IDS[index], Self::ROOM_SCREEN_IDS[index]) + } + + /// Pushes the appropriate StackNavigationView for the given `SelectedRoom`, + /// configuring the view's content widget and header title. + /// + /// Each stack depth gets its own dedicated room view widget, + /// supporting deep navigation (room → thread → room → thread → ...). + fn push_selected_room_view( + &mut self, + cx: &mut Cx, + app_state: &mut AppState, + selected_room: SelectedRoom, + ) { + let new_depth = self.view.stack_navigation(cx, ids!(view_stack)).depth(); + + let view_id = match &selected_room { + SelectedRoom::JoinedRoom { room_name_id } + | SelectedRoom::Thread { room_name_id, .. } => { + let (view_id, room_screen_id) = Self::room_ids_for_depth(new_depth); + let thread_root = if let SelectedRoom::Thread { thread_root_event_id, .. } = &selected_room { + Some(thread_root_event_id.clone()) + } else { + None + }; + self.view + .room_screen(cx, &[room_screen_id]) + .set_displayed_room(cx, room_name_id, thread_root); + view_id + } + SelectedRoom::InvitedRoom { room_name_id } => { + self.view + .invite_screen(cx, ids!(invite_screen)) + .set_displayed_invite(cx, room_name_id); + id!(invite_view) + } + SelectedRoom::Space { space_name_id } => { + self.view + .space_lobby_screen(cx, ids!(space_lobby_screen)) + .set_displayed_space(cx, space_name_id); + id!(space_lobby_view) + } + }; + + // Set the header title for the view being pushed. + let title_path = &[view_id, live_id!(header), live_id!(content), live_id!(title_container), live_id!(title)]; + self.view.label(cx, title_path).set_text(cx, &selected_room.display_name()); + + // Save the current selected_room onto the navigation stack before replacing it. + if let Some(prev) = app_state.selected_room.take() { + self.mobile_room_nav_stack.push(prev); + } + app_state.selected_room = Some(selected_room); + + // Push the view onto the mobile navigation stack. + self.view.stack_navigation(cx, ids!(view_stack)).push(cx, view_id); + self.view.redraw(cx); + } } From af6bb5ff5a04e834cc3b4fa9951739baae11d326 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 24 Mar 2026 11:41:47 -0700 Subject: [PATCH 02/18] Adjust home icon to be a better fit for Robrix's style --- resources/icon_home.svg | 5 ----- resources/icons/home.svg | 12 +++--------- 2 files changed, 3 insertions(+), 14 deletions(-) delete mode 100644 resources/icon_home.svg diff --git a/resources/icon_home.svg b/resources/icon_home.svg deleted file mode 100644 index f5edd734..00000000 --- a/resources/icon_home.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/icons/home.svg b/resources/icons/home.svg index 519a1bf2..5b5b85c8 100644 --- a/resources/icons/home.svg +++ b/resources/icons/home.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + \ No newline at end of file From 4b1d79c1650edb74a837d24cbb7aa26e6e307b72 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 24 Mar 2026 14:01:30 -0700 Subject: [PATCH 03/18] Fix TSP behavior and styling for 2.0, fix import icon --- resources/icons/import.svg | 6 ++++-- resources/icons/import2.svg | 6 ------ src/room/reply_preview.rs | 2 +- src/settings/account_settings.rs | 13 +++++++---- src/settings/settings_screen.rs | 1 - src/shared/styles.rs | 6 +++++- src/tsp/create_did_modal.rs | 7 ++++-- src/tsp/create_wallet_modal.rs | 2 +- src/tsp/sign_anycast_checkbox.rs | 5 +++++ src/tsp/tsp_settings_screen.rs | 37 +++++++++++++++----------------- src/tsp/wallet_entry/mod.rs | 11 +++++++++- 11 files changed, 57 insertions(+), 39 deletions(-) delete mode 100644 resources/icons/import2.svg diff --git a/resources/icons/import.svg b/resources/icons/import.svg index b23a1d1e..b07d957e 100644 --- a/resources/icons/import.svg +++ b/resources/icons/import.svg @@ -1,2 +1,4 @@ - - \ No newline at end of file + + + + diff --git a/resources/icons/import2.svg b/resources/icons/import2.svg deleted file mode 100644 index 8eef3aa3..00000000 --- a/resources/icons/import2.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/room/reply_preview.rs b/src/room/reply_preview.rs index 03ec0794..5a53687b 100644 --- a/src/room/reply_preview.rs +++ b/src/room/reply_preview.rs @@ -107,7 +107,7 @@ script_mod! { padding: 13, spacing: 0, margin: Inset{left: 5, right: 0}, - draw_bg.border_radius: 5.0 + draw_bg.border_radius: 4.0 draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{width: 16, height: 16, margin: 0} } diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 877f66bf..8f37cca1 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -57,6 +57,7 @@ script_mod! { upload_avatar_button := RobrixIconButton { width: 140, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: 0, draw_icon.svg: (ICON_UPLOAD) @@ -79,6 +80,7 @@ script_mod! { delete_avatar_button := RobrixNegativeIconButton { width: 140, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: 0, draw_icon.svg: (ICON_TRASH) @@ -117,7 +119,8 @@ script_mod! { // their styles to RobrixNeutralIconButton / RobrixPositiveIconButton. cancel_display_name_button := RobrixNeutralIconButton { enabled: false, - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, draw_icon.svg: (ICON_FORBIDDEN) @@ -127,10 +130,10 @@ script_mod! { accept_display_name_button := RobrixPositiveIconButton { enabled: false, - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_CHECKMARK) icon_walk: Walk{width: 16, height: 16, margin: 0} text: "Save Name" @@ -186,7 +189,8 @@ script_mod! { spacing: 10 manage_account_button := RobrixIconButton { - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + padding: Inset{left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_EXTERNAL_LINK) icon_walk: Walk{width: 16, height: 16} @@ -194,6 +198,7 @@ script_mod! { } logout_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_LOGOUT) diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 24baf849..a255b703 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -7,7 +7,6 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* - // The main, top-level settings screen widget. mod.widgets.SettingsScreen = #(SettingsScreen::register_widget(vm)) { width: Fill, height: Fill, diff --git a/src/shared/styles.rs b/src/shared/styles.rs index a80fa55e..4a0950ec 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -19,7 +19,7 @@ script_mod! { mod.widgets.ICON_COPY = crate_resource("self://resources/icons/copy.svg") mod.widgets.ICON_EDIT = crate_resource("self://resources/icons/edit.svg") mod.widgets.ICON_EXTERNAL_LINK = crate_resource("self://resources/icons/external_link.svg") - mod.widgets.ICON_IMPORT = crate_resource("self://resources/icons/import.svg") // TODO: FIX + mod.widgets.ICON_IMPORT = crate_resource("self://resources/icons/import.svg") mod.widgets.ICON_HIERARCHY = crate_resource("self://resources/icons/hierarchy.svg") mod.widgets.ICON_HOME = crate_resource("self://resources/icons/home.svg") mod.widgets.ICON_HTML_FILE = crate_resource("self://resources/icons/html_file.svg") @@ -187,6 +187,10 @@ script_mod! { mod.widgets.COLOR_IMAGE_VIEWER_META_BACKGROUND = #E8E8E8 + // Ensure all settings buttons have a consistent height + mod.widgets.SETTINGS_BUTTON_HEIGHT = 40 + + // A text input widget styled for Robrix. mod.widgets.RobrixTextInput = TextInput { width: Fill, height: Fit diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index f51e8bcc..361d6202 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -86,14 +86,17 @@ script_mod! { width: Fit, height: Fit, did_web := RadioButtonFlat { text: "Web" + draw_text +: { color: (COLOR_TEXT) } animator: { active: { default: on } } } did_webvh := RadioButtonFlat { text: "WebVH" + draw_text +: { color: (COLOR_TEXT) } animator: { disabled: { default: on } } } did_peer := RadioButtonFlat { text: "Peer", + draw_text +: { color: (COLOR_TEXT) } animator: { disabled: { default: on } } } } @@ -105,7 +108,7 @@ script_mod! { server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "p.teaspoon.world", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} @@ -147,7 +150,7 @@ script_mod! { did_server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "did.teaspoon.world", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} diff --git a/src/tsp/create_wallet_modal.rs b/src/tsp/create_wallet_modal.rs index 79c47759..1e1709b3 100644 --- a/src/tsp/create_wallet_modal.rs +++ b/src/tsp/create_wallet_modal.rs @@ -101,7 +101,7 @@ script_mod! { wallet_file_name_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset{top: 3, bottom: 3} + padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } empty_text: "my_wallet_file", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} diff --git a/src/tsp/sign_anycast_checkbox.rs b/src/tsp/sign_anycast_checkbox.rs index 8634c05e..971a4854 100644 --- a/src/tsp/sign_anycast_checkbox.rs +++ b/src/tsp/sign_anycast_checkbox.rs @@ -13,5 +13,10 @@ script_mod! { mod.widgets.TspSignAnycastCheckbox = CheckBoxFlat { text: "TSP", active: false, + draw_text +: { + color: COLOR_TEXT, + text_style: theme.font_regular {font_size: 11}, + mark_color_active: COLOR_TEXT, + } } } diff --git a/src/tsp/tsp_settings_screen.rs b/src/tsp/tsp_settings_screen.rs index 83d0e6f8..879ae3de 100644 --- a/src/tsp/tsp_settings_screen.rs +++ b/src/tsp/tsp_settings_screen.rs @@ -3,15 +3,12 @@ use makepad_widgets::*; use crate::{shared::{popup_list::{enqueue_popup_notification, PopupKind}, styles::*}, tsp::{create_did_modal::CreateDidModalAction, create_wallet_modal::CreateWalletModalAction, submit_tsp_request, tsp_state_ref, TspIdentityAction, TspRequest, TspWalletAction, TspWalletEntry, TspWalletMetadata}}; -const REPUBLISH_IDENTITY_BUTTON_TEXT: &str = "Republish Current Identity to DID Server"; - script_mod! { link tsp_enabled use mod.prelude.widgets.* use mod.widgets.* - mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT = "Republish Current Identity to DID Server" // The view containing all TSP-related settings. @@ -43,7 +40,7 @@ script_mod! { current_identity_label := Label { width: Fill, height: Fit flow: Flow.Right{wrap: true}, - margin: Inset{top: 10} + margin: Inset{top: 8} draw_text +: { text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } @@ -51,13 +48,13 @@ script_mod! { } republish_identity_button := RobrixIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{top: 8, bottom: 10, left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_UPLOAD) icon_walk: Walk{width: 16, height: 16} - text: (REPUBLISH_IDENTITY_BUTTON_TEXT) + text: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT } @@ -111,36 +108,36 @@ script_mod! { spacing: 10 create_did_button := RobrixPositiveIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_USER) - icon_walk: Walk{width: 21, height: Fit, margin: 0} + icon_walk: Walk{width: 19, height: Fit, margin: 0} text: "Create New Identity (DID)" } create_wallet_button := RobrixPositiveIconButton { - width: Fit, height: Fit, + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: 10, margin: Inset{left: 5}, - draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_WALLET) icon_walk: Walk{width: 21, height: Fit, margin: 0} text: "Create New Wallet" } import_wallet_button := RobrixIconButton { + width: Fit, + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} text: "Import Existing Wallet" - // TODO: fix this icon, or pick a different SVG - // draw_icon +: { - // svg: (ICON_IMPORT) - // color: (COLOR_PRIMARY) - // } - // icon_walk: Walk{width: 16, height: 16} - icon_walk: Walk{width: 0, height: 0} + draw_icon +: { + svg: (ICON_IMPORT) + color: (COLOR_PRIMARY) + } + icon_walk: Walk{width: 16, height: 16} } } } @@ -381,7 +378,7 @@ impl MatchEvent for TspSettingsScreen { // restore the republish button to its original state. script_apply_eval!(cx, republish_identity_button, { enabled: true, - text: #(REPUBLISH_IDENTITY_BUTTON_TEXT), + text: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT, }); match result { Ok(did) => { diff --git a/src/tsp/wallet_entry/mod.rs b/src/tsp/wallet_entry/mod.rs index 2c2de8ab..68bb2c4c 100644 --- a/src/tsp/wallet_entry/mod.rs +++ b/src/tsp/wallet_entry/mod.rs @@ -18,11 +18,13 @@ script_mod! { mod.widgets.WalletEntry = #(WalletEntry::register_widget(vm)) { width: Fill, height: Fit flow: Down + align: Align { y: 0.5 } View { width: Fill, height: Fit flow: Flow.Right{wrap: true}, padding: 10 + align: Align { y: 0.5 } wallet_name := Label { width: Fit, height: Fit @@ -50,9 +52,11 @@ script_mod! { visible: false, width: Fit, height: Fit margin: Inset{left: 20} + align: Align { y: 0.5 } Label { - margin: Inset{top: 2.9} width: Fit, height: Fit + margin: Inset{top: 3} + align: Align { y: 0.5 } flow: Right, draw_text +: { color: (COLOR_FG_ACCEPT_GREEN), @@ -66,10 +70,12 @@ script_mod! { visible: false, width: Fit, height: Fit margin: Inset{left: 20} + align: Align { y: 0.5 } Label { margin: Inset{top: 2.9} width: Fit, height: Fit flow: Right, + align: Align { y: 0.5 } draw_text +: { color: (COLOR_FG_DANGER_RED), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, @@ -79,6 +85,7 @@ script_mod! { } set_default_wallet_button := RobrixIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_CHECKMARK) @@ -87,6 +94,7 @@ script_mod! { } remove_wallet_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_CLOSE) @@ -95,6 +103,7 @@ script_mod! { } delete_wallet_button := RobrixNegativeIconButton { + height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 20} draw_icon.svg: (ICON_TRASH) From 6968397f2f89c3735900020df792b7363cd57b10 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 24 Mar 2026 14:11:46 -0700 Subject: [PATCH 04/18] improve add_user icon --- resources/icons/add_user.svg | 6 ++++-- src/shared/styles.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/resources/icons/add_user.svg b/resources/icons/add_user.svg index fad47b63..640aa9d9 100644 --- a/resources/icons/add_user.svg +++ b/resources/icons/add_user.svg @@ -1,4 +1,6 @@ - - + + + + \ No newline at end of file diff --git a/src/shared/styles.rs b/src/shared/styles.rs index 4a0950ec..feb778df 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -7,7 +7,7 @@ script_mod! { mod.widgets.ICON_ADD = crate_resource("self://resources/icons/add.svg") mod.widgets.ICON_ADD_REACTION = crate_resource("self://resources/icons/add_reaction.svg") - mod.widgets.ICON_ADD_USER = crate_resource("self://resources/icons/add_user.svg") // TODO: FIX + mod.widgets.ICON_ADD_USER = crate_resource("self://resources/icons/add_user.svg") mod.widgets.ICON_ADD_WALLET = crate_resource("self://resources/icons/add_wallet.svg") mod.widgets.ICON_FORBIDDEN = crate_resource("self://resources/icons/forbidden.svg") mod.widgets.ICON_CHECKMARK = crate_resource("self://resources/icons/checkmark.svg") From 9db4f53cc536967f63373b40578208a98093104f Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 24 Mar 2026 15:22:27 -0700 Subject: [PATCH 05/18] Update makepad to get our fixes for stack nav and Linux platform --- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 7 +-- 2 files changed, 83 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e87a838..42cc4af6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "bitmaps" @@ -728,7 +728,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "byteorder" @@ -739,7 +739,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "bytes" @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] @@ -2800,7 +2800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +2961,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-widgets", ] @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3005,15 +3005,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-bidi 0.3.18 (git+https://github.com/makepad/makepad?branch=dev)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3021,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3050,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "ttf-parser", ] @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3091,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,15 +3127,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3155,7 +3155,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "wayland-client", "wayland-egl", "wayland-protocols", @@ -3167,12 +3167,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-error-log", "makepad-html", @@ -3180,13 +3180,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-network", "makepad-script", @@ -3203,14 +3203,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3220,7 +3220,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3229,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3255,18 +3255,18 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "simd-adler32", ] @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3678,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "mime" @@ -4410,10 +4410,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", "unicase 2.9.0", ] @@ -5172,12 +5172,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "bytemuck", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5272,7 +5272,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "sealed" @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "socket2" @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "tungstenite" @@ -6424,7 +6424,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-bidi" @@ -6435,17 +6435,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-ident" @@ -6456,7 +6456,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-normalization" @@ -6476,12 +6476,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-segmentation" @@ -6492,7 +6492,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "unicode-width" @@ -6772,7 +6772,7 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "downcast-rs", "libc", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "wayland-backend", "wayland-sys", @@ -6803,7 +6803,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "log", "pkg-config", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +6985,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7049,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" [[package]] name = "windows-numerics" @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7110,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index 8bd24357..daf7ba9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,8 @@ version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -# makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } -# makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } - -makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "stack_nav_improvements", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "stack_nav_improvements" } +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. From ba1f400cfa21ac0ef19e2b150ad838827f7149f9 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Wed, 25 Mar 2026 10:21:15 -0700 Subject: [PATCH 06/18] update again --- Cargo.lock | 138 ++++++++++++++++++++++++++--------------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42cc4af6..c774c97e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "bitmaps" @@ -728,7 +728,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "byteorder" @@ -739,7 +739,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "bytes" @@ -1936,7 +1936,7 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", ] @@ -2800,7 +2800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.4", + "windows-targets 0.48.5", ] [[package]] @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +2961,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-widgets", ] @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3013,7 +3013,7 @@ dependencies = [ [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3021,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3050,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "ttf-parser", ] @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3091,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,12 +3127,12 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "ash", "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", @@ -3167,12 +3167,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-error-log", "makepad-html", @@ -3186,7 +3186,7 @@ dependencies = [ [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-network", "makepad-script", @@ -3203,12 +3203,12 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", @@ -3220,7 +3220,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3229,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3261,12 +3261,12 @@ dependencies = [ [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "simd-adler32", ] @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3678,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "mime" @@ -4410,7 +4410,7 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", @@ -5172,7 +5172,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "bytemuck", @@ -5272,7 +5272,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "sealed" @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "socket2" @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "tungstenite" @@ -6424,7 +6424,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-bidi" @@ -6435,17 +6435,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-ident" @@ -6456,7 +6456,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-normalization" @@ -6476,12 +6476,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-segmentation" @@ -6492,7 +6492,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "unicode-width" @@ -6772,7 +6772,7 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "downcast-rs", "libc", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "wayland-backend", "wayland-sys", @@ -6803,7 +6803,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "log", "pkg-config", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +6985,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7049,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" [[package]] name = "windows-numerics" @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7110,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#770e9cfbdd94309909708fa1318993a46061d4f9" +source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" dependencies = [ "windows-link 0.2.1", ] From af3a7a05c09df1136eb32c0f7808bf641c6bf89b Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Wed, 25 Mar 2026 10:51:38 -0700 Subject: [PATCH 07/18] update to our windows fix --- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 5 +- 2 files changed, 84 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c774c97e..5923ca13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "bitmaps" @@ -728,7 +728,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "byteorder" @@ -739,7 +739,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "bytes" @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", + "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", ] [[package]] @@ -2800,7 +2800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +2961,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-widgets", ] @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3005,15 +3005,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/makepad/makepad?branch=dev)", + "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3021,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3050,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "ttf-parser", ] @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3091,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,15 +3127,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3155,7 +3155,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "wayland-client", "wayland-egl", "wayland-protocols", @@ -3167,12 +3167,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-error-log", "makepad-html", @@ -3180,13 +3180,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-network", "makepad-script", @@ -3203,14 +3203,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3220,7 +3220,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3229,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3255,18 +3255,18 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "simd-adler32", ] @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3678,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "mime" @@ -4410,10 +4410,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", - "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "unicase 2.9.0", ] @@ -5172,12 +5172,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "bytemuck", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5272,7 +5272,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "sealed" @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "socket2" @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "tungstenite" @@ -6424,7 +6424,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-bidi" @@ -6435,17 +6435,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-ident" @@ -6456,7 +6456,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-normalization" @@ -6476,12 +6476,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-segmentation" @@ -6492,7 +6492,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "unicode-width" @@ -6772,7 +6772,7 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "downcast-rs", "libc", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "wayland-backend", "wayland-sys", @@ -6803,7 +6803,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "log", "pkg-config", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +6985,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7049,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" [[package]] name = "windows-numerics" @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7110,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#988d28505c29af180b0e924d1b782c6e03cab9ea" +source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index daf7ba9e..0cfccca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,9 @@ version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } +makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "windows-rs-build-fix", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "windows-rs-build-fix" } + ## Including this crate automatically configures all `robius-*` crates to work with Makepad. From 6bda528a0b363442415c155ed208ae542686a17f Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Wed, 25 Mar 2026 13:17:42 -0700 Subject: [PATCH 08/18] get our fixes to font parsing and text draw perf --- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 5 +- 2 files changed, 83 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5923ca13..ba456121 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "bitmaps" @@ -728,7 +728,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "byteorder" @@ -739,7 +739,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "bytes" @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", ] [[package]] @@ -2800,7 +2800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.4", + "windows-targets 0.48.5", ] [[package]] @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +2961,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-widgets", ] @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3005,15 +3005,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3021,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3050,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "ttf-parser", ] @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3091,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,15 +3127,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3155,7 +3155,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "wayland-client", "wayland-egl", "wayland-protocols", @@ -3167,12 +3167,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-error-log", "makepad-html", @@ -3180,13 +3180,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-network", "makepad-script", @@ -3203,14 +3203,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3220,7 +3220,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3229,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3255,18 +3255,18 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "simd-adler32", ] @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3678,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "mime" @@ -4410,10 +4410,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "unicase 2.9.0", ] @@ -5172,12 +5172,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "bytemuck", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5272,7 +5272,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "sealed" @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "socket2" @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "tungstenite" @@ -6424,7 +6424,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-bidi" @@ -6435,17 +6435,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-ident" @@ -6456,7 +6456,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-normalization" @@ -6476,12 +6476,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-segmentation" @@ -6492,7 +6492,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "unicode-width" @@ -6772,7 +6772,7 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "downcast-rs", "libc", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "wayland-backend", "wayland-sys", @@ -6803,7 +6803,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "log", "pkg-config", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +6985,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7049,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" [[package]] name = "windows-numerics" @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7110,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=windows-rs-build-fix#16fec9029982437840e5bfb08098592552927d1c" +source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index 0cfccca9..0d47cfde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,8 @@ version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "windows-rs-build-fix", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "windows-rs-build-fix" } - +makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "final_fix_hybrid_from_codex", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "final_fix_hybrid_from_codex" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. From be53e518404a8c71f768572180ca92000e946fc3 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Wed, 25 Mar 2026 14:13:46 -0700 Subject: [PATCH 09/18] track `dev` again now that our fixes were upstreamed into makepad --- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 4 +- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba456121..89c2a469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "bitmaps" @@ -728,7 +728,7 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "byteorder" @@ -739,7 +739,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "bytes" @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] @@ -2800,7 +2800,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -2953,7 +2953,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-objc-sys", ] @@ -2961,12 +2961,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-widgets", ] @@ -2974,7 +2974,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2991,7 +2991,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3005,15 +3005,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "unicode-bidi 0.3.18 (git+https://github.com/makepad/makepad?branch=dev)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-serde", ] @@ -3021,22 +3021,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", ] @@ -3050,7 +3050,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ttf-parser", ] @@ -3058,7 +3058,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id-macros", "serde", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3075,7 +3075,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-filesystem-watcher", ] @@ -3083,7 +3083,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-serde", ] @@ -3091,12 +3091,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3105,7 +3105,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3127,15 +3127,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3155,7 +3155,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "wayland-client", "wayland-egl", "wayland-protocols", @@ -3167,12 +3167,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-error-log", "makepad-html", @@ -3180,13 +3180,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-micro-proc-macro", ] @@ -3194,7 +3194,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-network", "makepad-script", @@ -3203,14 +3203,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3220,7 +3220,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-html", "makepad-live-id", @@ -3229,7 +3229,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-byteorder-lite", ] @@ -3246,7 +3246,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3255,18 +3255,18 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "simd-adler32", ] @@ -3274,7 +3274,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-zune-core", ] @@ -3282,7 +3282,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3678,7 +3678,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "mime" @@ -4410,10 +4410,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", "unicase 2.9.0", ] @@ -5172,12 +5172,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "bytemuck", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5272,7 +5272,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "sealed" @@ -5571,7 +5571,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "socket2" @@ -6363,7 +6363,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "tungstenite" @@ -6424,7 +6424,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-bidi" @@ -6435,17 +6435,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-ident" @@ -6456,7 +6456,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-normalization" @@ -6476,12 +6476,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-segmentation" @@ -6492,7 +6492,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "unicode-width" @@ -6772,7 +6772,7 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "downcast-rs", "libc", @@ -6784,7 +6784,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "wayland-backend", "wayland-sys", @@ -6803,7 +6803,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "log", "pkg-config", @@ -6912,7 +6912,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -6931,7 +6931,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-core 0.62.2", ] @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -6985,7 +6985,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-core 0.62.2", ] @@ -7049,7 +7049,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" [[package]] name = "windows-numerics" @@ -7093,7 +7093,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", ] @@ -7110,7 +7110,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=final_fix_hybrid_from_codex#96869fd9b4d32e65f3fcfcf05bdc72fe4fd06663" +source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index 0d47cfde..daf7ba9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "final_fix_hybrid_from_codex", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "final_fix_hybrid_from_codex" } +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. From 1a00e5c5133a0a2a955a174fb36494cec823fced Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:35:01 +0000 Subject: [PATCH 10/18] perf(sliding_sync): fix N+1 query in typing notifications Optimizes `SubscribeToTypingNotices` by using `join_all` instead of a sequential loop when fetching display names for typing users, reducing latency and making member lookups run concurrently. Co-authored-by: kevinaboos <1139460+kevinaboos@users.noreply.github.com> --- patch.diff | 28 + src/sliding_sync.rs | 16 +- src/sliding_sync.rs.orig | 4273 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 4307 insertions(+), 10 deletions(-) create mode 100644 patch.diff create mode 100644 src/sliding_sync.rs.orig diff --git a/patch.diff b/patch.diff new file mode 100644 index 00000000..77a3c0cb --- /dev/null +++ b/patch.diff @@ -0,0 +1,28 @@ +--- src/sliding_sync.rs ++++ src/sliding_sync.rs +@@ -1452,17 +1452,13 @@ + let _typing_notices_task = Handle::current().spawn(async move { + while let Ok(user_ids) = typing_notice_receiver.recv().await { + // log!("Received typing notifications for room {room_id}: {user_ids:?}"); +- let mut users = Vec::with_capacity(user_ids.len()); +- for user_id in user_ids { +- users.push( +- main_timeline.room() +- .get_member_no_sync(&user_id) +- .await +- .ok() +- .flatten() +- .and_then(|m| m.display_name().map(|d| d.to_owned())) +- .unwrap_or_else(|| user_id.to_string()) +- ); +- } ++ let users = join_all(user_ids.into_iter().map(|user_id| { ++ let room = main_timeline.room().clone(); ++ async move { ++ room.get_member_no_sync(&user_id).await.ok().flatten() ++ .and_then(|m| m.display_name().map(|d| d.to_owned())) ++ .unwrap_or_else(|| user_id.to_string()) ++ } ++ })).await; + if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { + error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 30fccc5a..51242f66 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1452,18 +1452,14 @@ async fn matrix_worker_task( let _typing_notices_task = Handle::current().spawn(async move { while let Ok(user_ids) = typing_notice_receiver.recv().await { // log!("Received typing notifications for room {room_id}: {user_ids:?}"); - let mut users = Vec::with_capacity(user_ids.len()); - for user_id in user_ids { - users.push( - main_timeline.room() - .get_member_no_sync(&user_id) - .await - .ok() - .flatten() + let users = join_all(user_ids.into_iter().map(|user_id| { + let room = main_timeline.room().clone(); + async move { + room.get_member_no_sync(&user_id).await.ok().flatten() .and_then(|m| m.display_name().map(|d| d.to_owned())) .unwrap_or_else(|| user_id.to_string()) - ); - } + } + })).await; if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); } diff --git a/src/sliding_sync.rs.orig b/src/sliding_sync.rs.orig new file mode 100644 index 00000000..d280a212 --- /dev/null +++ b/src/sliding_sync.rs.orig @@ -0,0 +1,4273 @@ +use anyhow::{anyhow, bail, Result}; +use bitflags::bitflags; +use clap::Parser; +use eyeball::Subscriber; +use eyeball_im::VectorDiff; +use futures_util::{future::join_all, pin_mut, StreamExt}; +use imbl::Vector; +use makepad_widgets::{error, log, warning, Cx, SignalToUI}; +use matrix_sdk_base::crypto::{DecryptionSettings, TrustRequirement}; +use matrix_sdk::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ + api::{Direction, client::{profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType}}, events::{ + relation::RelationType, + room::{ + message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource + }, MessageLikeEventType, StateEventType + }, matrix_uri::MatrixId, EventId, MatrixToUri, MatrixUri, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomOrAliasId, UserId, uint + }, sliding_sync::VersionBuilder, Client, ClientBuildError, Error, OwnedServerName, Room, RoomDisplayName, RoomMemberships, RoomState, SessionChange, SuccessorRoom +}; +use matrix_sdk_ui::{ + RoomListService, Timeline, encryption_sync_service, room_list_service::{RoomListItem, RoomListLoadingState, SyncIndicator, filters}, sync_service::{self, SyncService}, timeline::{LatestEventValue, RoomExt, TimelineEventItemId, TimelineFocus, TimelineItem, TimelineReadReceiptTracking, TimelineDetails} +}; +use robius_open::Uri; +use ruma::{OwnedRoomOrAliasId, RoomId, events::tag::Tags}; +use tokio::{ + runtime::Handle, + sync::{broadcast, mpsc::{Sender, UnboundedReceiver, UnboundedSender}, watch, Notify}, task::JoinHandle, time::error::Elapsed, +}; +use url::Url; +use std::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefault, DefaultHasher}, iter::Peekable, ops::{Deref, DerefMut, Not}, path:: Path, sync::{Arc, LazyLock, Mutex}, time::Duration}; +use std::io; +use hashbrown::{HashMap, HashSet}; +use crate::{ + app::AppStateAction, app_data_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{ + add_room::KnockResultAction, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, tombstone_footer::SuccessorRoomDetails + }, login::login_screen::LoginAction, logout::{logout_confirm_modal::LogoutAction, logout_state_machine::{LogoutConfig, is_logout_in_progress, logout_with_state_machine}}, media_cache::{MediaCacheEntry, MediaCacheEntryRef}, persistence::{self, ClientSessionPersisted, load_app_state}, profile::{ + user_profile::UserProfile, + user_profile_cache::{UserProfileUpdate, enqueue_user_profile_update}, + }, room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{ + avatar::AvatarState, html_or_plaintext::MatrixLinkPillState, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification} + }, space_service_sync::space_service_loop, utils::{self, AVATAR_THUMBNAIL_FORMAT, RoomNameId, VecDiff, avatar_from_room_name}, verification::add_verification_event_handlers_and_sync_client +}; + +#[derive(Parser, Default)] +struct Cli { + /// The user ID to login with. + #[clap(value_parser)] + user_id: String, + + /// The password that should be used for the login. + #[clap(value_parser)] + password: String, + + /// The homeserver to connect to. + #[clap(value_parser)] + homeserver: Option, + + /// Set the proxy that should be used for the connection. + #[clap(short, long)] + proxy: Option, + + /// Force login screen. + #[clap(short, long, action)] + login_screen: bool, + + /// Enable verbose logging output. + #[clap(short, long, action)] + verbose: bool, +} + +impl std::fmt::Debug for Cli { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Cli") + .field("user_id", &self.user_id) + .field("password", &"") + .field("homeserver", &self.homeserver) + .field("proxy", &self.proxy) + .field("login_screen", &self.login_screen) + .field("verbose", &self.verbose) + .finish() + } +} + +impl From for Cli { + fn from(login: LoginByPassword) -> Self { + Self { + user_id: login.user_id, + password: login.password, + homeserver: login.homeserver, + proxy: None, + login_screen: false, + verbose: false, + } + } +} + + +/// Build a new client. +async fn build_client( + cli: &Cli, + data_dir: &Path, +) -> Result<(Client, ClientSessionPersisted), ClientBuildError> { + // Generate a unique subfolder name for the client database, + // which allows multiple clients to run simultaneously. + let now = chrono::Local::now(); + let db_subfolder_name: String = format!("db_{}", now.format("%F_%H_%M_%S_%f")); + let db_path = data_dir.join(db_subfolder_name); + + // Generate a random passphrase. + let passphrase: String = { + use rand::{Rng, thread_rng}; + thread_rng() + .sample_iter(rand::distributions::Alphanumeric) + .take(32) + .map(char::from) + .collect() + }; + + let homeserver_url = cli.homeserver.as_deref() + .unwrap_or("https://matrix-client.matrix.org/"); + // .unwrap_or("https://matrix.org/"); + + let mut builder = Client::builder() + .server_name_or_homeserver_url(homeserver_url) + // Use a sqlite database to persist the client's encryption setup. + .sqlite_store(&db_path, Some(&passphrase)) + .with_threading_support(matrix_sdk::ThreadingSupport::Enabled { + with_subscriptions: true, + }) + // The sliding sync proxy has now been deprecated in favor of native sliding sync. + .sliding_sync_version_builder(VersionBuilder::DiscoverNative) + .with_decryption_settings(DecryptionSettings { + sender_device_trust_requirement: TrustRequirement::Untrusted, + }) + .with_encryption_settings(EncryptionSettings { + auto_enable_cross_signing: true, + backup_download_strategy: matrix_sdk::encryption::BackupDownloadStrategy::OneShot, + auto_enable_backups: true, + }) + .with_enable_share_history_on_invite(true) + .handle_refresh_tokens(); + + if let Some(proxy) = cli.proxy.as_ref() { + builder = builder.proxy(proxy.clone()); + } + + // Use a 60 second timeout for all requests to the homeserver. + // Yes, this is a long timeout, but the standard matrix homeserver is often very slow. + builder = builder.request_config( + RequestConfig::new() + .timeout(std::time::Duration::from_secs(60)) + ); + + let client = builder.build().await?; + let homeserver_url = client.homeserver().to_string(); + Ok(( + client, + ClientSessionPersisted { + homeserver: homeserver_url, + db_path, + passphrase, + }, + )) +} + +/// Logs in to the given Matrix homeserver using the given username and password. +/// +/// This function is used by the login screen to log in to the Matrix server. +/// +/// Upon success, this function returns the logged-in client and an optional sync token. +async fn login( + cli: &Cli, + login_request: LoginRequest, +) -> Result<(Client, Option)> { + match login_request { + LoginRequest::LoginByCli | LoginRequest::LoginByPassword(_) => { + let cli = if let LoginRequest::LoginByPassword(login_by_password) = login_request { + &Cli::from(login_by_password) + } else { + cli + }; + let (client, client_session) = build_client(cli, app_data_dir()).await?; + Cx::post_action(LoginAction::Status { + title: "Authenticating".into(), + status: format!("Logging in as {}...", cli.user_id), + }); + // Attempt to login using the CLI-provided username & password. + let login_result = client + .matrix_auth() + .login_username(&cli.user_id, &cli.password) + .initial_device_display_name("robrix-un-pw") + .send() + .await?; + if client.matrix_auth().logged_in() { + log!("Logged in successfully."); + let status = format!("Logged in as {}.\n → Loading rooms...", cli.user_id); + // enqueue_popup_notification(status.clone()); + enqueue_rooms_list_update(RoomsListUpdate::Status { status }); + if let Err(e) = persistence::save_session(&client, client_session).await { + let err_msg = format!("Failed to save session state to storage: {e}"); + error!("{err_msg}"); + enqueue_popup_notification(err_msg, PopupKind::Error, None); + } + Ok((client, None)) + } else { + let err_msg = format!("Failed to login as {}: {:?}", cli.user_id, login_result); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + bail!(err_msg); + } + } + + LoginRequest::LoginBySSOSuccess(client, client_session) => { + if let Err(e) = persistence::save_session(&client, client_session).await { + error!("Failed to save session state to storage: {e:?}"); + } + Ok((client, None)) + } + LoginRequest::HomeserverLoginTypesQuery(_) => { + bail!("LoginRequest::HomeserverLoginTypesQuery not handled earlier"); + } + } +} + + +/// Which direction to paginate in. +/// +/// * `Forwards` will retrieve later events (towards the end of the timeline), +/// which only works if the timeline is *focused* on a specific event. +/// * `Backwards`: the more typical choice, in which earlier events are retrieved +/// (towards the start of the timeline), which works in both live mode and focused mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaginationDirection { + Forwards, + Backwards, +} +impl std::fmt::Display for PaginationDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Forwards => write!(f, "forwards"), + Self::Backwards => write!(f, "backwards"), + } + } +} + +/// The function signature for the callback that gets invoked when media is fetched. +pub type OnMediaFetchedFn = fn( + &Mutex, + MediaRequestParameters, + matrix_sdk::Result>, + Option>, +); + +/// Error types for URL preview operations. +#[derive(Debug)] +pub enum UrlPreviewError { + /// HTTP request failed. + Request(reqwest::Error), + /// JSON parsing failed. + Json(serde_json::Error), + /// Client not available. + ClientNotAvailable, + /// Access token not available. + AccessTokenNotAvailable, + /// HTTP error status. + HttpStatus(u16), + /// URL parsing error. + UrlParse(url::ParseError), +} + +impl std::fmt::Display for UrlPreviewError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UrlPreviewError::Request(e) => write!(f, "HTTP request failed: {}", e), + UrlPreviewError::Json(e) => write!(f, "JSON parsing failed: {}", e), + UrlPreviewError::ClientNotAvailable => write!(f, "Matrix client not available"), + UrlPreviewError::AccessTokenNotAvailable => write!(f, "Access token not available"), + UrlPreviewError::HttpStatus(status) => write!(f, "HTTP {} error", status), + UrlPreviewError::UrlParse(e) => write!(f, "URL parsing failed: {}", e), + } + } +} + +impl std::error::Error for UrlPreviewError {} + +/// The function signature for the callback that gets invoked when link preview data is fetched. +pub type OnLinkPreviewFetchedFn = fn( + String, + Arc>, + Result, + Option>, +); + + +/// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. +#[derive(Clone, Debug)] +pub enum MatrixLinkAction { + MatrixToUri(MatrixToUri), + MatrixUri(MatrixUri), + Error(String), +} + +/// Actions emitted when account data (e.g., avatar, display name) changes. +#[derive(Clone, Debug)] +pub enum AccountDataAction { + /// The user's avatar was successfully updated or removed. + AvatarChanged(Option), + /// Failed to update or remove the user's avatar. + AvatarChangeFailed(String), + /// The user's display name was successfully updated or removed. + DisplayNameChanged(Option), + /// Failed to update the user's display name. + DisplayNameChangeFailed(String), +} + +/// Actions emitted in response to a [`MatrixRequest::OpenOrCreateDirectMessage`]. +#[derive(Debug)] +pub enum DirectMessageRoomAction { + /// A direct message room already existed with the given user. + FoundExisting { + user_id: OwnedUserId, + room_name_id: RoomNameId, + }, + /// A direct message room didn't exist, and we didn't attempt to create a new one. + DidNotExist { + user_profile: UserProfile, + }, + /// A direct message room didn't exist, but we successfully created a new one. + NewlyCreated { + user_profile: UserProfile, + room_name_id: RoomNameId, + }, + /// A direct message room didn't exist, and we failed to create a new one. + FailedToCreate { + user_profile: UserProfile, + error: matrix_sdk::Error, + }, +} + +/// Either a main room timeline or a thread-focused timeline. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum TimelineKind { + MainRoom { + room_id: OwnedRoomId, + }, + Thread { + room_id: OwnedRoomId, + thread_root_event_id: OwnedEventId, + }, +} +impl TimelineKind { + pub fn room_id(&self) -> &OwnedRoomId { + match self { + TimelineKind::MainRoom { room_id } => room_id, + TimelineKind::Thread { room_id, .. } => room_id, + } + } + + pub fn thread_root_event_id(&self) -> Option<&OwnedEventId> { + match self { + TimelineKind::MainRoom { .. } => None, + TimelineKind::Thread { thread_root_event_id, .. } => Some(thread_root_event_id), + } + } +} +impl std::fmt::Display for TimelineKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TimelineKind::MainRoom { room_id } => write!(f, "MainRoom({})", room_id), + TimelineKind::Thread { room_id, thread_root_event_id } => { + write!(f, "Thread({}, {})", room_id, thread_root_event_id) + } + } + } +} + +/// The set of requests for async work that can be made to the worker thread. +#[allow(clippy::large_enum_variant)] +pub enum MatrixRequest { + /// Request from the login screen to log in with the given credentials. + Login(LoginRequest), + /// Request to logout. + Logout { + is_desktop: bool, + }, + /// Request to paginate the older (or newer) events of a room or thread timeline. + PaginateTimeline { + timeline_kind: TimelineKind, + /// The maximum number of timeline events to fetch in each pagination batch. + num_events: u16, + direction: PaginationDirection, + }, + /// Request to edit the content of an event in the given room's timeline. + EditMessage { + timeline_kind: TimelineKind, + timeline_event_item_id: TimelineEventItemId, + edited_content: EditedContent, + }, + /// Request to fetch the full details of the given event in the given room's timeline. + FetchDetailsForEvent { + timeline_kind: TimelineKind, + event_id: OwnedEventId, + }, + /// Request to fetch the latest thread-reply preview and latest reply count + /// for the given thread root. + FetchThreadSummaryDetails { + timeline_kind: TimelineKind, + thread_root_event_id: OwnedEventId, + timeline_item_index: usize, + }, + /// Request to fetch profile information for all members of a room. + /// + /// This can be *very* slow depending on the number of members in the room. + /// + /// Even though it operates on a room itself, this accepts a `TimelineKind` + /// in order to be able to send the fetched room member list to a specific timeline UI. + SyncRoomMemberList { + timeline_kind: TimelineKind, + }, + /// Request to create a thread timeline focused on the given thread root event in the given room. + CreateThreadTimeline { + room_id: OwnedRoomId, + thread_root_event_id: OwnedEventId, + }, + /// Request to knock on (request an invite to) the given room. + Knock { + room_or_alias_id: OwnedRoomOrAliasId, + reason: Option, + #[doc(alias("via"))] + server_names: Vec, + }, + /// Request to invite the given user to the given room. + InviteUser { + room_id: OwnedRoomId, + user_id: OwnedUserId, + }, + /// Request to join the given room. + JoinRoom { + room_id: OwnedRoomId, + }, + /// Request to leave the given room. + LeaveRoom { + room_id: OwnedRoomId, + }, + /// Request to get the actual list of members in a room. + /// + /// This returns the list of members that can be displayed in the UI. + /// + /// Even though it operates on a room itself, this accepts a `TimelineKind` + /// in order to be able to send the fetched room member list to a specific timeline UI. + GetRoomMembers { + timeline_kind: TimelineKind, + memberships: RoomMemberships, + /// * If `true` (not recommended), only the local cache will be accessed. + /// * If `false` (recommended), details will be fetched from the server. + local_only: bool, + }, + /// Request to fetch the preview (basic info) for the given room, + /// either one that is joined locally or one that is unknown. + /// + /// Emits a [`RoomPreviewAction::Fetched`] when the fetch operation has completed. + GetRoomPreview { + room_or_alias_id: OwnedRoomOrAliasId, + via: Vec, + }, + /// Request to fetch the full details (the room preview) of a tombstoned room. + GetSuccessorRoomDetails { + tombstoned_room_id: OwnedRoomId, + }, + /// Request to create or open a direct message room with the given user. + /// + /// If there is no existing DM room with the given user, this will create a new DM room + /// if `allow_create` is `true`; otherwise it will emit an action indicating that + /// no DM room existed, upon which the UI will prompt the user to confirm that they want + /// to proceed with creating a new DM room. + #[doc(alias("dm"))] + OpenOrCreateDirectMessage { + user_profile: UserProfile, + allow_create: bool, + }, + /// Request to fetch profile information for the given user ID. + GetUserProfile { + user_id: OwnedUserId, + /// * If `Some`, the user is known to be a member of a room, so this will + /// fetch the user's profile from that room's membership info. + /// * If `None`, the user's profile info will be fetched from the server + /// in a room-agnostic manner, and no room membership info will be returned. + room_id: Option, + /// * If `true` (not recommended), only the local cache will be accessed. + /// * If `false` (recommended), details will be fetched from the server. + local_only: bool, + }, + /// Request to fetch the number of unread messages in the given room. + GetNumberUnreadMessages { + timeline_kind: TimelineKind, + }, + /// Request to set the unread flag for the given room. + SetUnreadFlag { + room_id: OwnedRoomId, + /// If `true`, marks the room as unread. + /// If `false`, marks the room as read. + mark_as_unread: bool, + }, + /// Request to set the favorite flag for the given room. + SetIsFavorite { + room_id: OwnedRoomId, + is_favorite: bool, + }, + /// Request to set the low priority flag for the given room. + SetIsLowPriority { + room_id: OwnedRoomId, + is_low_priority: bool, + }, + /// Request to generate a Matrix link (permalink) for a room or event. + GenerateMatrixLink { + /// The ID of the room to generate a link for. + room_id: OwnedRoomId, + /// * If `Some`, the link will point to this specific event within the room. + /// * If `None`, the link will point to the room itself. + event_id: Option, + /// * If `true`, the `matrix:` URI scheme will be used to create a [`MatrixUri`]. + /// * If `false` (default), the `https://matrix.to` scheme will be used to create a [`MatrixToUri`]. + use_matrix_scheme: bool, + /// * If `true` (default is false), the link will include an action hint to join the room. + join_on_click: bool, + }, + /// Request to ignore/block or unignore/unblock a user. + IgnoreUser { + /// Whether to ignore (`true`) or unignore (`false`) the user. + ignore: bool, + /// The room membership info of the user to (un)ignore. + room_member: RoomMember, + /// The room ID of the room where the user is a member, + /// which is only needed because it isn't present in the `RoomMember` object. + room_id: OwnedRoomId, + }, + /// Request to set or remove the avatar of the current user's account. + SetAvatar { + /// * If `Some`, the avatar will be set to the given MXC URI. + /// * If `None`, the avatar will be removed. + avatar_url: Option, + }, + /// Request to set or remove the display name of the current user's account. + SetDisplayName { + /// * If `Some`, the display name will be set to the given value. + /// * If `None`, the display name will be removed. + new_display_name: Option, + }, + /// Request to resolve a room alias into a room ID and the servers that know about that room. + ResolveRoomAlias(OwnedRoomAliasId), + /// Request to fetch an Avatar image from the server. + /// Upon completion of the async media request, the `on_fetched` function + /// will be invoked with the content of an `AvatarUpdate`. + FetchAvatar { + mxc_uri: OwnedMxcUri, + on_fetched: fn(AvatarUpdate), + }, + /// Request to fetch media from the server. + /// Upon completion of the async media request, the `on_fetched` function + /// will be invoked with four arguments: the `destination`, the `media_request`, + /// the result of the media fetch, and the `update_sender`. + FetchMedia { + media_request: MediaRequestParameters, + on_fetched: OnMediaFetchedFn, + destination: MediaCacheEntryRef, + update_sender: Option>, + }, + /// Request to send a message to the given room. + SendMessage { + timeline_kind: TimelineKind, + message: RoomMessageEventContent, + replied_to: Option, + #[cfg(feature = "tsp")] + sign_with_tsp: bool, + }, + /// Sends a notice to the given room that the current user is or is not typing. + /// + /// This request does not return a response or notify the UI thread, and + /// furthermore, there is no need to send a follow-up request to stop typing + /// (though you certainly can do so). + SendTypingNotice { + room_id: OwnedRoomId, + typing: bool, + }, + /// Spawn an async task to login to the given Matrix homeserver using the given SSO identity provider ID. + /// + /// While an SSO request is in flight, the login screen will temporarily prevent the user + /// from submitting another redundant request, until this request has succeeded or failed. + SpawnSSOServer{ + brand: String, + homeserver_url: String, + identity_provider_id: String, + }, + /// Subscribe to typing notices for the given room. + /// + /// This is only valid for the main room timeline, not for thread-focused timelines. + /// + /// This request does not immediately return a response or notify the UI thread, + /// but it will send updates to the UI via the timeline's update sender. + SubscribeToTypingNotices { + room_id: OwnedRoomId, + /// Whether to subscribe or unsubscribe. + subscribe: bool, + }, + /// Subscribe to changes in the read receipts of our own user. + /// + /// This request does not immediately return a response or notify the UI thread, + /// but it will send updates to the UI via the timeline's update sender. + SubscribeToOwnUserReadReceiptsChanged { + timeline_kind: TimelineKind, + /// Whether to subscribe or unsubscribe. + subscribe: bool, + }, + /// Subscribe to changes in the set of pinned events for the given room. + /// + /// This is only valid for the main room timeline, not for thread-focused timelines. + SubscribeToPinnedEvents { + room_id: OwnedRoomId, + /// Whether to subscribe or unsubscribe. + subscribe: bool, + }, + /// Sends a read receipt for the given event to the given room or thread timeline. + ReadReceipt { + timeline_kind: TimelineKind, + event_id: OwnedEventId, + receipt_type: ReceiptType, + }, + /// Sends a request to obtain the power levels for this room. + /// + /// The response is delivered back to the main UI thread via [`TimelineUpdate::UserPowerLevels`]. + /// + /// Even though it operates on a room itself, this accepts a `TimelineKind` + /// in order to be able to send the fetched room member list to a specific timeline UI. + GetRoomPowerLevels { + timeline_kind: TimelineKind, + }, + /// Toggles the given reaction to the given event in the given room. + ToggleReaction { + timeline_kind: TimelineKind, + timeline_event_id: TimelineEventItemId, + reaction: String, + }, + /// Redacts (deletes) the given event in the given room. + #[doc(alias("delete"))] + RedactMessage { + timeline_kind: TimelineKind, + timeline_event_id: TimelineEventItemId, + reason: Option, + }, + /// Pin or unpin the given event in the given room. + #[doc(alias("unpin"))] + PinEvent { + timeline_kind: TimelineKind, + event_id: OwnedEventId, + pin: bool, + }, + /// Sends a request to obtain the room's pill link info for the given Matrix ID. + /// + /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. + GetMatrixRoomLinkPillInfo { + matrix_id: MatrixId, + via: Vec + }, + /// Request to fetch URL preview from the Matrix homeserver. + GetUrlPreview { + url: String, + on_fetched: OnLinkPreviewFetchedFn, + destination: Arc>, + update_sender: Option>, + }, +} + +/// Submits a request to the worker thread to be executed asynchronously. +pub fn submit_async_request(req: MatrixRequest) { + if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() { + sender.send(req) + .expect("BUG: matrix worker task receiver has died!"); + } +} + +/// Details of a login request that get submitted within [`MatrixRequest::Login`]. +pub enum LoginRequest{ + LoginByPassword(LoginByPassword), + LoginBySSOSuccess(Client, ClientSessionPersisted), + LoginByCli, + HomeserverLoginTypesQuery(String), + +} +/// Information needed to log in to a Matrix homeserver. +pub struct LoginByPassword { + pub user_id: String, + pub password: String, + pub homeserver: Option, +} + + +/// The entry point for the worker task that runs Matrix-related operations. +/// +/// All this task does is wait for [`MatrixRequests`] from the main UI thread +/// and then executes them within an async runtime context. +async fn matrix_worker_task( + mut request_receiver: UnboundedReceiver, + login_sender: Sender, +) -> Result<()> { + log!("Started matrix_worker_task."); + // The async tasks that are spawned to subscribe to changes in our own user's read receipts for each timeline. + let mut subscribers_own_user_read_receipts: HashMap> = HashMap::new(); + // The async tasks that are spawned to subscribe to changes in the pinned events for each room. + let mut subscribers_pinned_events: HashMap> = HashMap::new(); + + while let Some(request) = request_receiver.recv().await { + match request { + MatrixRequest::Login(login_request) => { + if let Err(e) = login_sender.send(login_request).await { + error!("Error sending login request to login_sender: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(String::from( + "BUG: failed to send login request to login worker task." + ))); + } + } + + MatrixRequest::Logout { is_desktop } => { + log!("Received MatrixRequest::Logout, is_desktop: {}", is_desktop); + let _logout_task = Handle::current().spawn(async move { + log!("Starting logout task"); + // Use the state machine implementation + match logout_with_state_machine(is_desktop).await { + Ok(()) => { + log!("Logout completed successfully via state machine"); + }, + Err(e) => { + error!("Logout failed: {e:?}"); + } + } + }); + } + + MatrixRequest::PaginateTimeline {timeline_kind, num_events, direction} => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("Skipping pagination request for unknown {timeline_kind}"); + continue; + }; + + // Spawn a new async task that will make the actual pagination request. + let _paginate_task = Handle::current().spawn(async move { + log!("Starting {direction} pagination request for {timeline_kind}..."); + sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap(); + SignalToUI::set_ui_signal(); + + let res = if direction == PaginationDirection::Forwards { + timeline.paginate_forwards(num_events).await + } else { + timeline.paginate_backwards(num_events).await + }; + + match res { + Ok(fully_paginated) => { + log!("Completed {direction} pagination request for {timeline_kind}, hit {} of timeline? {}", + if direction == PaginationDirection::Forwards { "end" } else { "start" }, + if fully_paginated { "yes" } else { "no" }, + ); + sender.send(TimelineUpdate::PaginationIdle { + fully_paginated, + direction, + }).unwrap(); + SignalToUI::set_ui_signal(); + } + Err(error) => { + error!("Error sending {direction} pagination request for {timeline_kind}: {error:?}"); + sender.send(TimelineUpdate::PaginationError { + error, + direction, + }).unwrap(); + SignalToUI::set_ui_signal(); + } + } + }); + } + + MatrixRequest::EditMessage { timeline_kind, timeline_event_item_id, edited_content } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for edit request"); + continue; + }; + + // Spawn a new async task that will make the actual edit request. + let _edit_task = Handle::current().spawn(async move { + log!("Sending request to edit message {timeline_event_item_id:?} in {timeline_kind}..."); + let result = timeline.edit(&timeline_event_item_id, edited_content).await; + match result { + Ok(_) => log!("Successfully edited message {timeline_event_item_id:?} in {timeline_kind}."), + Err(ref e) => error!("Error editing message {timeline_event_item_id:?} in {timeline_kind}: {e:?}"), + } + sender.send(TimelineUpdate::MessageEdited { + timeline_event_item_id, + result, + }).unwrap(); + SignalToUI::set_ui_signal(); + }); + } + + MatrixRequest::FetchDetailsForEvent { timeline_kind, event_id } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for fetch details for event request"); + continue; + }; + + let _fetch_task = Handle::current().spawn(async move { + // log!("Sending request to fetch details for event {event_id} in {timeline_kind}..."); + let result = timeline.fetch_details_for_event(&event_id).await; + match &result { + Ok(_) => { + // log!("Successfully fetched details for event {event_id} in {timeline_kind}."); + } + Err(_e) => { + // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); + } + } + if sender.send(TimelineUpdate::EventDetailsFetched { event_id, result }).is_err() { + error!("Failed to send fetched event details to UI for {timeline_kind}"); + } + SignalToUI::set_ui_signal(); + }); + } + + MatrixRequest::FetchThreadSummaryDetails { + timeline_kind, + thread_root_event_id, + timeline_item_index, + } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for fetch thread summary details request"); + continue; + }; + + let _fetch_task = Handle::current().spawn(async move { + let (num_replies, latest_reply_event) = fetch_thread_summary_details( + timeline.room(), + &thread_root_event_id, + ).await; + let latest_reply_preview_text = match latest_reply_event.as_ref() { + Some(event) => text_preview_of_latest_thread_reply(timeline.room(), event).await, + None => None, + }; + + if sender.send(TimelineUpdate::ThreadSummaryDetailsFetched { + thread_root_event_id, + timeline_item_index, + num_replies, + latest_reply_preview_text, + }).is_err() { + error!("Failed to send fetched thread summary details to UI for {timeline_kind}"); + } + SignalToUI::set_ui_signal(); + }); + } + + MatrixRequest::SyncRoomMemberList { timeline_kind } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for sync members list request"); + continue; + }; + + let _fetch_task = Handle::current().spawn(async move { + log!("Sending sync room members request for {timeline_kind}..."); + timeline.fetch_members().await; + log!("Completed sync room members request for {timeline_kind}."); + sender.send(TimelineUpdate::RoomMembersSynced).unwrap(); + SignalToUI::set_ui_signal(); + }); + } + + MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { + let main_room_timeline = { + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { + error!("BUG: room info not found for create thread timeline request, room {room_id}"); + continue; + }; + if room_info.thread_timelines.contains_key(&thread_root_event_id) { + continue; + } + let newly_pending = room_info.pending_thread_timelines.insert(thread_root_event_id.clone()); + if !newly_pending { + continue; + } + room_info.main_timeline.timeline.clone() + }; + + let _create_thread_timeline_task = Handle::current().spawn(async move { + log!("Creating thread-focused timeline for room {room_id}, thread {thread_root_event_id}..."); + let build_result = main_room_timeline.room() + .timeline_builder() + .with_focus(TimelineFocus::Thread { + root_event_id: thread_root_event_id.clone(), + }) + .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) + .build() + .await; + + match build_result { + Ok(thread_timeline) => { + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { + return; + }; + log!("Successfully created thread-focused timeline for room {room_id}, thread {thread_root_event_id}."); + let thread_timeline = Arc::new(thread_timeline); + let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); + let (request_sender, request_receiver) = watch::channel(Vec::new()); + let timeline_subscriber_handler_task = Handle::current().spawn( + timeline_subscriber_handler( + main_room_timeline.room().clone(), + thread_timeline.clone(), + timeline_update_sender.clone(), + request_receiver, + Some(thread_root_event_id.clone()), + ) + ); + room_info + .pending_thread_timelines + .remove(&thread_root_event_id); + room_info.thread_timelines.insert( + thread_root_event_id.clone(), + PerTimelineDetails { + timeline: thread_timeline, + timeline_update_sender, + timeline_singleton_endpoints: Some(( + timeline_update_receiver, + request_sender, + )), + timeline_subscriber_handler_task, + }, + ); + SignalToUI::set_ui_signal(); + } + Err(error) => { + error!("Failed to create thread-focused timeline for room {room_id}, thread {thread_root_event_id}: {error}"); + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + if let Some(room_info) = all_joined_rooms.get_mut(&room_id) { + room_info + .pending_thread_timelines + .remove(&thread_root_event_id); + } + enqueue_popup_notification( + format!("Failed to create thread-focused timeline. Please retry opening the thread again later.\n\nError: {error}"), + PopupKind::Error, + None, + ); + } + } + }); + } + + MatrixRequest::Knock { room_or_alias_id, reason, server_names } => { + let Some(client) = get_client() else { continue }; + let _knock_room_task = Handle::current().spawn(async move { + log!("Sending request to knock on room {room_or_alias_id}..."); + match client.knock(room_or_alias_id.clone(), reason, server_names).await { + Ok(room) => { + let _ = room.display_name().await; // populate this room's display name cache + Cx::post_action(KnockResultAction::Knocked { + room_or_alias_id, + room, + }); + } + Err(error) => Cx::post_action(KnockResultAction::Failed { + room_or_alias_id, + error, + }), + } + }); + } + + MatrixRequest::InviteUser { room_id, user_id } => { + let Some(client) = get_client() else { continue }; + let _invite_task = Handle::current().spawn(async move { + // We use `client.get_room()` here because the room might also be a space, + // not just a joined room. + if let Some(room) = client.get_room(&room_id) { + log!("Sending request to invite user {user_id} to room {room_id}..."); + match room.invite_user_by_id(&user_id).await { + Ok(_) => Cx::post_action(InviteResultAction::Sent { + room_id, + user_id, + }), + Err(error) => Cx::post_action(InviteResultAction::Failed { + room_id, + user_id, + error, + }), + } + } + else { + error!("Room/Space not found for invite user request {room_id}, {user_id}"); + Cx::post_action(InviteResultAction::Failed { + room_id, + user_id, + error: matrix_sdk::Error::UnknownError("Room/Space not found in client's known list.".into()), + }) + } + }); + } + + MatrixRequest::JoinRoom { room_id } => { + let Some(client) = get_client() else { continue }; + let _join_room_task = Handle::current().spawn(async move { + log!("Sending request to join room {room_id}..."); + let result_action = if let Some(room) = client.get_room(&room_id) { + match room.join().await { + Ok(()) => { + log!("Successfully joined known room {room_id}."); + JoinRoomResultAction::Joined { room_id } + } + Err(e) => { + error!("Error joining known room {room_id}: {e:?}"); + JoinRoomResultAction::Failed { room_id, error: e } + } + } + } + else { + match client.join_room_by_id(&room_id).await { + Ok(_room) => { + log!("Successfully joined new unknown room {room_id}."); + JoinRoomResultAction::Joined { room_id } + } + Err(e) => { + error!("Error joining new unknown room {room_id}: {e:?}"); + JoinRoomResultAction::Failed { room_id, error: e } + } + } + }; + Cx::post_action(result_action); + }); + } + + MatrixRequest::LeaveRoom { room_id } => { + let Some(client) = get_client() else { continue }; + let _leave_room_task = Handle::current().spawn(async move { + log!("Sending request to leave room {room_id}..."); + let result_action = if let Some(room) = client.get_room(&room_id) { + match room.leave().await { + Ok(()) => { + log!("Successfully left room {room_id}."); + LeaveRoomResultAction::Left { room_id } + } + Err(e) => { + error!("Error leaving room {room_id}: {e:?}"); + LeaveRoomResultAction::Failed { room_id, error: e } + } + } + } else { + error!("BUG: client could not get room with ID {room_id}"); + LeaveRoomResultAction::Failed { + room_id, + error: matrix_sdk::Error::UnknownError("Client couldn't locate room to leave it.".into()), + } + }; + Cx::post_action(result_action); + }); + } + + MatrixRequest::GetRoomMembers { timeline_kind, memberships, local_only } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for get room members request"); + continue; + }; + + let _get_members_task = Handle::current().spawn(async move { + let send_update = |members: Vec, source: &str| { + log!("{} {} members for {timeline_kind}", source, members.len()); + sender.send(TimelineUpdate::RoomMembersListFetched { members }).unwrap(); + SignalToUI::set_ui_signal(); + }; + + let room = timeline.room(); + if local_only { + if let Ok(members) = room.members_no_sync(memberships).await { + send_update(members, "Got"); + } + } else { + if let Ok(members) = room.members(memberships).await { + send_update(members, "Successfully fetched"); + } + } + }); + } + + MatrixRequest::GetRoomPreview { room_or_alias_id, via } => { + let Some(client) = get_client() else { continue }; + let _fetch_task = Handle::current().spawn(async move { + let res = fetch_room_preview_with_avatar(&client, &room_or_alias_id, via).await; + Cx::post_action(RoomPreviewAction::Fetched(res)); + }); + } + + MatrixRequest::GetSuccessorRoomDetails { tombstoned_room_id } => { + let Some(client) = get_client() else { continue }; + let (sender, successor_room) = { + let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let Some(room_info) = all_joined_rooms.get(&tombstoned_room_id) else { + error!("BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request"); + continue; + }; + ( + room_info.main_timeline.timeline_update_sender.clone(), + room_info.main_timeline.timeline.room().successor_room(), + ) + }; + spawn_fetch_successor_room_preview( + client, + successor_room, + tombstoned_room_id, + sender, + ); + } + + MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create } => { + let Some(client) = get_client() else { continue }; + let _create_dm_task = Handle::current().spawn(async move { + if let Some(room) = client.get_dm_room(&user_profile.user_id) { + log!("Found existing DM room: {}", room.room_id()); + Cx::post_action(DirectMessageRoomAction::FoundExisting { + user_id: user_profile.user_id, + room_name_id: RoomNameId::from_room(&room).await, + }); + return; + } + if !allow_create { + Cx::post_action(DirectMessageRoomAction::DidNotExist { user_profile }); + return; + } + log!("Creating new DM room with {user_profile:?}..."); + match client.create_dm(&user_profile.user_id).await { + Ok(room) => { + log!("Successfully created DM room: {}", room.room_id()); + Cx::post_action(DirectMessageRoomAction::NewlyCreated { + user_profile, + room_name_id: RoomNameId::from_room(&room).await, + }); + }, + Err(error) => { + error!("Failed to create DM with {user_profile:?}: {error}"); + Cx::post_action(DirectMessageRoomAction::FailedToCreate { + user_profile, + error, + }); + } + } + }); + } + + MatrixRequest::GetUserProfile { user_id, room_id, local_only } => { + let Some(client) = get_client() else { continue }; + let _fetch_task = Handle::current().spawn(async move { + // log!("Sending get user profile request: user: {user_id}, \ + // room: {room_id:?}, local_only: {local_only}...", + // ); + + let mut update = None; + + if let Some(room_id) = room_id.as_ref() { + if let Some(room) = client.get_room(room_id) { + let member = if local_only { + room.get_member_no_sync(&user_id).await + } else { + room.get_member(&user_id).await + }; + if let Ok(Some(room_member)) = member { + update = Some(UserProfileUpdate::Full { + new_profile: UserProfile { + username: room_member.display_name().map(|u| u.to_owned()), + user_id: user_id.clone(), + avatar_state: AvatarState::Known(room_member.avatar_url().map(|u| u.to_owned())), + }, + room_id: room_id.to_owned(), + room_member, + }); + } else { + log!("User profile request: user {user_id} was not a member of room {room_id}"); + } + } else { + log!("User profile request: client could not get room with ID {room_id}"); + } + } + + if !local_only { + if update.is_none() { + if let Ok(response) = client.account().fetch_user_profile_of(&user_id).await { + update = Some(UserProfileUpdate::UserProfileOnly( + UserProfile { + username: response.get_static::().ok().flatten(), + user_id: user_id.clone(), + avatar_state: response.get_static::() + .ok() + .map_or(AvatarState::Unknown, AvatarState::Known), + } + )); + } else { + log!("User profile request: client could not get user with ID {user_id}"); + } + } + + match update.as_mut() { + Some(UserProfileUpdate::Full { new_profile: UserProfile { username, .. }, .. }) if username.is_none() => { + if let Ok(response) = client.account().fetch_user_profile_of(&user_id).await { + *username = response.get_static::().ok().flatten(); + } + } + _ => { } + } + } + + if let Some(upd) = update { + // log!("Successfully completed get user profile request: user: {user_id}, room: {room_id:?}, local_only: {local_only}."); + enqueue_user_profile_update(upd); + } else { + log!("Failed to get user profile: user: {user_id}, room: {room_id:?}, local_only: {local_only}."); + } + }); + } + + MatrixRequest::GetNumberUnreadMessages { timeline_kind } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("Skipping get number of unread messages request for {timeline_kind}"); + continue; + }; + + let _get_unreads_task = Handle::current().spawn(async move { + match sender.send(TimelineUpdate::NewUnreadMessagesCount( + UnreadMessageCount::Known(timeline.room().num_unread_messages()) + )) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(e) => log!("Failed to send timeline update: {e:?} for GetNumberUnreadMessages request for {timeline_kind}"), + } + if let TimelineKind::MainRoom { room_id } = timeline_kind { + enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { + room_id, + is_marked_unread: timeline.room().is_marked_unread(), + unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), + unread_mentions: timeline.room().num_unread_mentions(), + }); + } + }); + } + + MatrixRequest::SetUnreadFlag { room_id, mark_as_unread } => { + let Some(main_timeline) = get_room_timeline(&room_id) else { + log!("BUG: skipping set unread flag request for not-yet-known room {room_id}"); + continue; + }; + let _set_unread_task = Handle::current().spawn(async move { + let result = main_timeline.room().set_unread_flag(mark_as_unread).await; + match result { + Ok(_) => log!("Set unread flag to {} for room {}", mark_as_unread, room_id), + Err(e) => error!("Failed to set unread flag to {} for room {}: {:?}", mark_as_unread, room_id, e), + } + }); + } + + MatrixRequest::SetIsFavorite { room_id, is_favorite } => { + let Some(main_timeline) = get_room_timeline(&room_id) else { + log!("BUG: skipping set favorite flag request for not-yet-known room {room_id}"); + continue; + }; + let _set_favorite_task = Handle::current().spawn(async move { + let result = main_timeline.room().set_is_favourite(is_favorite, None).await; + match result { + Ok(_) => log!("Set favorite to {} for room {}", is_favorite, room_id), + Err(e) => error!("Failed to set favorite to {} for room {}: {:?}", is_favorite, room_id, e), + } + }); + } + + MatrixRequest::SetIsLowPriority { room_id, is_low_priority } => { + let Some(main_timeline) = get_room_timeline(&room_id) else { + log!("BUG: skipping set low priority flag request for not-yet-known room {room_id}"); + continue; + }; + let _set_lp_task = Handle::current().spawn(async move { + let result = main_timeline.room().set_is_low_priority(is_low_priority, None).await; + match result { + Ok(_) => log!("Set low priority to {} for room {}", is_low_priority, room_id), + Err(e) => error!("Failed to set low priority to {} for room {}: {:?}", is_low_priority, room_id, e), + } + }); + } + + MatrixRequest::SetAvatar { avatar_url } => { + let Some(client) = get_client() else { continue }; + let _set_avatar_task = Handle::current().spawn(async move { + let is_removing = avatar_url.is_none(); + log!("Sending request to {} avatar...", if is_removing { "remove" } else { "set" }); + let result = client.account().set_avatar_url(avatar_url.as_deref()).await; + match result { + Ok(_) => { + log!("Successfully {} avatar.", if is_removing { "removed" } else { "set" }); + Cx::post_action(AccountDataAction::AvatarChanged(avatar_url)); + } + Err(e) => { + let err_msg = format!("Failed to {} avatar: {e}", if is_removing { "remove" } else { "set" }); + Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); + } + } + }); + } + + MatrixRequest::SetDisplayName { new_display_name } => { + let Some(client) = get_client() else { continue }; + let _set_display_name_task = Handle::current().spawn(async move { + let is_removing = new_display_name.is_none(); + log!("Sending request to {} display name{}...", + if is_removing { "remove" } else { "set" }, + new_display_name.as_ref().map(|n| format!(" to '{n}'")).unwrap_or_default() + ); + let result = client.account().set_display_name(new_display_name.as_deref()).await; + match result { + Ok(_) => { + log!("Successfully {} display name.", if is_removing { "removed" } else { "set" }); + Cx::post_action(AccountDataAction::DisplayNameChanged(new_display_name)); + } + Err(e) => { + let err_msg = format!("Failed to {} display name: {e}", if is_removing { "remove" } else { "set" }); + Cx::post_action(AccountDataAction::DisplayNameChangeFailed(err_msg)); + } + } + }); + } + + MatrixRequest::GenerateMatrixLink { room_id, event_id, use_matrix_scheme, join_on_click } => { + let Some(client) = get_client() else { continue }; + let _gen_link_task = Handle::current().spawn(async move { + if let Some(room) = client.get_room(&room_id) { + let result = if use_matrix_scheme { + if let Some(event_id) = event_id { + room.matrix_event_permalink(event_id).await + .map(MatrixLinkAction::MatrixUri) + } else { + room.matrix_permalink(join_on_click).await + .map(MatrixLinkAction::MatrixUri) + } + } else { + if let Some(event_id) = event_id { + room.matrix_to_event_permalink(event_id).await + .map(MatrixLinkAction::MatrixToUri) + } else { + room.matrix_to_permalink().await + .map(MatrixLinkAction::MatrixToUri) + } + }; + + match result { + Ok(action) => Cx::post_action(action), + Err(e) => Cx::post_action(MatrixLinkAction::Error(e.to_string())), + } + } else { + Cx::post_action(MatrixLinkAction::Error(format!("Room {room_id} not found"))); + } + }); + } + + MatrixRequest::IgnoreUser { ignore, room_member, room_id } => { + let Some(client) = get_client() else { continue }; + let _ignore_task = Handle::current().spawn(async move { + let user_id = room_member.user_id(); + log!("Sending request to {}ignore user: {user_id}...", if ignore { "" } else { "un" }); + let ignore_result = if ignore { + room_member.ignore().await + } else { + room_member.unignore().await + }; + + log!("{} user {user_id} {}", + if ignore { "Ignoring" } else { "Unignoring" }, + if ignore_result.is_ok() { "succeeded." } else { "failed." }, + ); + + if ignore_result.is_err() { + return; + } + + // We need to re-acquire the `RoomMember` object now that its state + // has changed, i.e., the user has been (un)ignored. + // We then need to send an update to replace the cached `RoomMember` + // with the now-stale ignored state. + if let Some(room) = client.get_room(&room_id) { + if let Ok(Some(new_room_member)) = room.get_member(user_id).await { + log!("Enqueueing user profile update for user {user_id}, who went from {}ignored to {}ignored.", + if room_member.is_ignored() { "" } else { "un" }, + if new_room_member.is_ignored() { "" } else { "un" }, + ); + enqueue_user_profile_update(UserProfileUpdate::RoomMemberOnly { + room_id: room_id.clone(), + room_member: new_room_member, + }); + } + } + + // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. + // Therefore, we need to re-fetch all timelines for all rooms, + // and currently the only way to actually accomplish this is via pagination. + // See: + // + // Note that here we only proactively re-paginate the *current* room + // (the one being viewed by the user when this ignore request was issued), + // and all other rooms will be re-paginated in `handle_ignore_user_list_subscriber()`.` + submit_async_request(MatrixRequest::PaginateTimeline { + timeline_kind: TimelineKind::MainRoom { room_id }, + num_events: 50, + direction: PaginationDirection::Backwards, + }); + }); + } + + MatrixRequest::SendTypingNotice { room_id, typing } => { + let Some(main_room_timeline) = get_room_timeline(&room_id) else { + log!("BUG: skipping send typing notice request for not-yet-known room {room_id}"); + continue; + }; + let _typing_task = Handle::current().spawn(async move { + if let Err(e) = main_room_timeline.room().typing_notice(typing).await { + error!("Failed to send typing notice to room {room_id}: {e:?}"); + } + }); + } + + MatrixRequest::SubscribeToTypingNotices { room_id, subscribe } => { + let (main_timeline, timeline_update_sender, mut typing_notice_receiver) = { + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let Some(jrd) = all_joined_rooms.get_mut(&room_id) else { + log!("BUG: room info not found for subscribe to typing notices request, room {room_id}"); + continue; + }; + let (main_timeline, receiver) = if subscribe { + if jrd.typing_notice_subscriber.is_some() { + warning!("Note: room {room_id} is already subscribed to typing notices."); + continue; + } else { + let main_timeline = jrd.main_timeline.timeline.clone(); + let (drop_guard, receiver) = main_timeline.room().subscribe_to_typing_notifications(); + jrd.typing_notice_subscriber = Some(drop_guard); + (main_timeline, receiver) + } + } else { + jrd.typing_notice_subscriber.take(); + continue; + }; + // Here: we don't have an existing subscriber running, so we fall through and start one. + (main_timeline, jrd.main_timeline.timeline_update_sender.clone(), receiver) + }; + + let _typing_notices_task = Handle::current().spawn(async move { + while let Ok(user_ids) = typing_notice_receiver.recv().await { + // log!("Received typing notifications for room {room_id}: {user_ids:?}"); + let mut users = Vec::with_capacity(user_ids.len()); + for user_id in user_ids { + users.push( + main_timeline.room() + .get_member_no_sync(&user_id) + .await + .ok() + .flatten() + .and_then(|m| m.display_name().map(|d| d.to_owned())) + .unwrap_or_else(|| user_id.to_string()) + ); + } + if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { + error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); + } + SignalToUI::set_ui_signal(); + } + // log!("Note: typing notifications recv loop has ended for room {}", room_id); + }); + } + + MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { + if !subscribe { + if let Some(task_handler) = subscribers_own_user_read_receipts.remove(&timeline_kind) { + task_handler.abort(); + } + continue; + } + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}"); + continue; + }; + + let timeline_kind_clone = timeline_kind.clone(); + let subscribe_own_read_receipt_task = Handle::current().spawn(async move { + let update_receiver = timeline.subscribe_own_user_read_receipts_changed().await; + pin_mut!(update_receiver); + if let Some(client_user_id) = current_user_id() { + if let Some((event_id, receipt)) = timeline.latest_user_read_receipt(&client_user_id).await { + log!("Received own user read receipt for {timeline_kind}: {receipt:?}, event ID: {event_id:?}"); + if sender.send(TimelineUpdate::OwnUserReadReceipt(receipt)).is_err() { + error!("Failed to send own user read receipt to UI."); + } + } + + while update_receiver.next().await.is_some() { + if let Some((_, receipt)) = timeline.latest_user_read_receipt(&client_user_id).await { + if sender.send(TimelineUpdate::OwnUserReadReceipt(receipt)).is_err() { + error!("Failed to send own user read receipt to UI."); + } + // When read receipts change (from other devices), update unread count + let unread_count = timeline.room().num_unread_messages(); + let unread_mentions = timeline.room().num_unread_mentions(); + if sender.send(TimelineUpdate::NewUnreadMessagesCount( + UnreadMessageCount::Known(unread_count) + )).is_err() { + error!("Failed to send unread message count update to UI."); + } + if let TimelineKind::MainRoom { room_id } = &timeline_kind { + // Update the rooms list with new unread counts + enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { + room_id: room_id.clone(), + is_marked_unread: timeline.room().is_marked_unread(), + unread_messages: UnreadMessageCount::Known(unread_count), + unread_mentions, + }); + } + } + } + } + }); + subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); + } + + MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { + if !subscribe { + if let Some(task_handler) = subscribers_pinned_events.remove(&room_id) { + task_handler.abort(); + } + continue; + } + let kind = TimelineKind::MainRoom { room_id: room_id.clone() }; + let Some((main_timeline, sender)) = get_timeline_and_sender(&kind) else { + log!("BUG: skipping subscribe to pinned events request for unknown room {room_id}"); + continue; + }; + let subscribe_pinned_events_task = Handle::current().spawn(async move { + // Send an initial update, as the stream may not update immediately. + let pinned_events = main_timeline.room().pinned_event_ids().unwrap_or_default(); + match sender.send(TimelineUpdate::PinnedEvents(pinned_events)) { + Ok(()) => SignalToUI::set_ui_signal(), + Err(_) => log!("Failed to send initial pinned events update to UI."), + } + let update_receiver = main_timeline.room().pinned_event_ids_stream(); + pin_mut!(update_receiver); + while let Some(pinned_events) = update_receiver.next().await { + match sender.send(TimelineUpdate::PinnedEvents(pinned_events)) { + Ok(()) => SignalToUI::set_ui_signal(), + Err(e) => log!("Failed to send pinned events update: {e:?}"), + } + } + }); + subscribers_pinned_events.insert(room_id, subscribe_pinned_events_task); + } + + MatrixRequest::SpawnSSOServer { brand, homeserver_url, identity_provider_id} => { + spawn_sso_server(brand, homeserver_url, identity_provider_id, login_sender.clone()).await; + } + + MatrixRequest::ResolveRoomAlias(room_alias) => { + let Some(client) = get_client() else { continue }; + let _resolve_task = Handle::current().spawn(async move { + log!("Sending resolve room alias request for {room_alias}..."); + let res = client.resolve_room_alias(&room_alias).await; + log!("Resolved room alias {room_alias} to: {res:?}"); + todo!("Send the resolved room alias back to the UI thread somehow."); + }); + } + + MatrixRequest::FetchAvatar { mxc_uri, on_fetched } => { + let Some(client) = get_client() else { continue }; + Handle::current().spawn(async move { + // log!("Sending fetch avatar request for {mxc_uri:?}..."); + let media_request = MediaRequestParameters { + source: MediaSource::Plain(mxc_uri.clone()), + format: AVATAR_THUMBNAIL_FORMAT.into(), + }; + let res = client.media().get_media_content(&media_request, true).await; + // log!("Fetched avatar for {mxc_uri:?}, succeeded? {}", res.is_ok()); + on_fetched(AvatarUpdate { mxc_uri, avatar_data: res.map(|v| v.into()) }); + }); + } + + MatrixRequest::FetchMedia { media_request, on_fetched, destination, update_sender } => { + let Some(client) = get_client() else { continue }; + + let _fetch_task = Handle::current().spawn(async move { + // log!("Sending fetch media request for {media_request:?}..."); + let res = client.media().get_media_content(&media_request, true).await; + on_fetched(&destination, media_request, res, update_sender); + }); + } + + MatrixRequest::SendMessage { + timeline_kind, + message, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp, + } => { + // TODO: use this timeline `_sender` once we support sending-message status/operations in the UI. + let Some((timeline, _sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for send message request"); + continue; + }; + + // Spawn a new async task that will send the actual message. + let _send_message_task = Handle::current().spawn(async move { + log!("Sending message to {timeline_kind}: {message:?}..."); + let message = { + #[cfg(not(feature = "tsp"))] { + message + } + + #[cfg(feature = "tsp")] { + let mut message = message; + if sign_with_tsp { + log!("Signing message with TSP..."); + match serde_json::to_vec(&message) { + Ok(message_bytes) => { + log!("Serialized message to bytes, length {}", message_bytes.len()); + match crate::tsp::sign_anycast_with_default_vid(&message_bytes) { + Ok(signed_msg) => { + log!("Successfully signed message with TSP, length {}", signed_msg.len()); + use matrix_sdk::ruma::serde::Base64; + message.tsp_signature = Some(Base64::new(signed_msg)); + } + Err(e) => { + error!("Failed to sign message with TSP: {e:?}"); + enqueue_popup_notification( + format!("Failed to sign message with TSP: {e}"), + PopupKind::Error, + None, + ); + return; + } + } + } + Err(e) => { + error!("Failed to serialize message to bytes for TSP signing: {e:?}"); + enqueue_popup_notification( + format!("Failed to serialize message for TSP signing: {e}"), + PopupKind::Error, + None, + ); + return; + } + } + } + message + } + }; + + if let Some(replied_to_info) = replied_to { + let reply_content = match timeline + .room() + .make_reply_event(message.into(), replied_to_info) + .await + { + Ok(content) => content, + Err(_e) => { + error!("Failed to build reply content to send to {timeline_kind}: {_e:?}"); + enqueue_popup_notification( + format!("Failed to send reply: {_e}"), + PopupKind::Error, + None, + ); + return; + } + }; + match timeline.send(reply_content.into()).await { + Ok(_send_handle) => log!("Sent reply message to {timeline_kind}."), + Err(_e) => { + error!("Failed to send reply message to {timeline_kind}: {_e:?}"); + enqueue_popup_notification(format!("Failed to send reply: {_e}"), PopupKind::Error, None); + } + } + } else { + match timeline.send(message.into()).await { + Ok(_send_handle) => log!("Sent message to {timeline_kind}."), + Err(_e) => { + error!("Failed to send message to {timeline_kind}: {_e:?}"); + enqueue_popup_notification(format!("Failed to send message: {_e}"), PopupKind::Error, None); + } + } + } + SignalToUI::set_ui_signal(); + }); + } + + MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { + let Some(timeline) = get_timeline(&timeline_kind) else { + log!("BUG: {timeline_kind} not found when sending read receipt, {event_id}"); + continue; + }; + + let _send_rr_task = Handle::current().spawn(async move { + match timeline.send_single_receipt(receipt_type.clone(), event_id.clone()).await { + Ok(sent) => log!("{} {receipt_type} read receipt to {timeline_kind} for event {event_id}", if sent { "Sent" } else { "Already sent" }), + Err(_e) => error!("Failed to send {receipt_type} read receipt to {timeline_kind} for event {event_id}; error: {_e:?}"), + } + if let TimelineKind::MainRoom { room_id } = timeline_kind { + // Also update the number of unread messages in the room. + enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { + room_id, + is_marked_unread: timeline.room().is_marked_unread(), + unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), + unread_mentions: timeline.room().num_unread_mentions() + }); + } + }); + }, + + MatrixRequest::GetRoomPowerLevels { timeline_kind } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for room power levels request"); + continue; + }; + + let Some(user_id) = current_user_id() else { continue }; + + let _power_levels_task = Handle::current().spawn(async move { + match timeline.room().power_levels().await { + Ok(power_levels) => { + log!("Successfully fetched power levels for {timeline_kind}."); + if sender.send(TimelineUpdate::UserPowerLevels( + UserPowerLevels::from(&power_levels, &user_id), + )).is_err() { + error!("Failed to send room power levels to UI.") + } + SignalToUI::set_ui_signal(); + } + Err(e) => { + error!("Failed to fetch power levels for {timeline_kind}: {e:?}"); + } + } + }); + }, + + MatrixRequest::ToggleReaction { timeline_kind, timeline_event_id, reaction } => { + let Some(timeline) = get_timeline(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for toggle reaction request"); + continue; + }; + + let _toggle_reaction_task = Handle::current().spawn(async move { + log!("Sending toggle reaction {reaction:?} to {timeline_kind}: ..."); + match timeline.toggle_reaction(&timeline_event_id, &reaction).await { + Ok(_send_handle) => { + log!("Sent toggle reaction {reaction:?} to {timeline_kind}."); + SignalToUI::set_ui_signal(); + }, + Err(_e) => error!("Failed to send toggle reaction to {timeline_kind}; error: {_e:?}"), + } + }); + }, + + MatrixRequest::RedactMessage { timeline_kind, timeline_event_id, reason } => { + let Some(timeline) = get_timeline(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for redact message request"); + continue; + }; + + let _redact_task = Handle::current().spawn(async move { + match timeline.redact(&timeline_event_id, reason.as_deref()).await { + Ok(()) => log!("Successfully redacted message in {timeline_kind}."), + Err(e) => { + error!("Failed to redact message in {timeline_kind}; error: {e:?}"); + enqueue_popup_notification( + format!("Failed to redact message. Error: {e}"), + PopupKind::Error, + None, + ); + } + } + }); + }, + + MatrixRequest::PinEvent { timeline_kind, event_id, pin } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for pin event request"); + continue; + }; + + let _pin_task = Handle::current().spawn(async move { + let result = if pin { + timeline.pin_event(&event_id).await + } else { + timeline.unpin_event(&event_id).await + }; + match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => log!("Failed to send UI update for pin event."), + } + }); + } + + MatrixRequest::GetMatrixRoomLinkPillInfo { matrix_id, via } => { + let Some(client) = get_client() else { continue }; + let _fetch_matrix_link_pill_info_task = Handle::current().spawn(async move { + let room_or_alias_id: Option<&RoomOrAliasId> = match &matrix_id { + MatrixId::Room(room_id) => Some((&**room_id).into()), + MatrixId::RoomAlias(room_alias_id) => Some((&**room_alias_id).into()), + MatrixId::Event(room_or_alias_id, _event_id) => Some(room_or_alias_id), + _ => { + log!("MatrixLinkRoomPillInfoRequest: Unsupported MatrixId type: {matrix_id:?}"); + return; + } + }; + if let Some(room_or_alias_id) = room_or_alias_id { + match client.get_room_preview(room_or_alias_id, via).await { + Ok(preview) => Cx::post_action(MatrixLinkPillState::Loaded { + matrix_id: matrix_id.clone(), + name: preview.name.unwrap_or_else(|| room_or_alias_id.to_string()), + avatar_url: preview.avatar_url + }), + Err(_e) => { + log!("Failed to get room link pill info for {room_or_alias_id:?}: {_e:?}"); + } + }; + } + }); + } + + MatrixRequest::GetUrlPreview { url, on_fetched, destination, update_sender } => { + // const MAX_LOG_RESPONSE_BODY_LENGTH: usize = 1000; + // log!("Starting URL preview fetch for: {}", url); + let _fetch_url_preview_task = Handle::current().spawn(async move { + let result: Result = async { + // log!("Getting Matrix client for URL preview: {}", url); + let client = get_client().ok_or_else(|| { + // error!("Matrix client not available for URL preview: {}", url); + UrlPreviewError::ClientNotAvailable + })?; + + let token = client.access_token().ok_or_else(|| { + // error!("Access token not available for URL preview: {}", url); + UrlPreviewError::AccessTokenNotAvailable + })?; + // Official Doc: https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediapreview_url + // Element desktop is using /_matrix/media/v3/preview_url + let endpoint_url = client.homeserver().join("/_matrix/client/v1/media/preview_url") + .map_err(UrlPreviewError::UrlParse)?; + // log!("Fetching URL preview from endpoint: {} for URL: {}", endpoint_url, url); + + let response = client + .http_client() + .get(endpoint_url.clone()) + .bearer_auth(token) + .query(&[("url", url.as_str())]) + .header("Content-Type", "application/json") + .send() + .await + .map_err(|e| { + // error!("HTTP request failed for URL preview {}: {}", url, e); + UrlPreviewError::Request(e) + })?; + + let status = response.status(); + // log!("URL preview response status for {}: {}", url, status); + + if !status.is_success() && status.as_u16() != 429 { + // error!("URL preview request failed with status {} for URL: {}", status, url); + return Err(UrlPreviewError::HttpStatus(status.as_u16())); + } + + let text = response.text().await.map_err(|e| { + // error!("Failed to read response text for URL preview {}: {}", url, e); + UrlPreviewError::Request(e) + })?; + + // log!("URL preview response body length for {}: {} bytes", url, text.len()); + // if text.len() > MAX_LOG_RESPONSE_BODY_LENGTH { + // log!("URL preview response body preview for {}: {}...", url, &text[..MAX_LOG_RESPONSE_BODY_LENGTH]); + // } else { + // log!("URL preview response body for {}: {}", url, text); + // } + // This request is rate limited, retry after a duration we get from the server. + if status.as_u16() == 429 { + let link_preview_429_res = serde_json::from_str::(&text) + .map_err(|e| { + // error!("Failed to parse as LinkPreviewRateLimitResponse for URL preview {}: {}", url, e); + UrlPreviewError::Json(e) + }); + match link_preview_429_res { + Ok(link_preview_429_res) => { + if let Some(retry_after) = link_preview_429_res.retry_after_ms { + tokio::time::sleep(Duration::from_millis(retry_after.into())).await; + submit_async_request(MatrixRequest::GetUrlPreview{ + url: url.clone(), + on_fetched, + destination: destination.clone(), + update_sender: update_sender.clone(), + }); + + } + } + Err(_e) => { + // error!("Failed to parse as LinkPreviewRateLimitResponse for URL preview {}: {}", url, _e); + } + } + return Err(UrlPreviewError::HttpStatus(429)); + } + serde_json::from_str::(&text) + .or_else(|_first_error| { + // log!("Failed to parse as LinkPreviewData, trying LinkPreviewDataNonNumeric for URL: {}", url); + serde_json::from_str::(&text) + .map(|non_numeric| non_numeric.into()) + }) + .map_err(|e| { + // error!("Failed to parse JSON response for URL preview {}: {}", url, e); + // error!("Response body that failed to parse: {}", text); + UrlPreviewError::Json(e) + }) + }.await; + + // match &result { + // Ok(preview_data) => { + // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", + // url, preview_data.title, preview_data.site_name); + // } + // Err(e) => { + // error!("URL preview fetch failed for {}: {}", url, e); + // } + // } + + on_fetched(url, destination, result, update_sender); + SignalToUI::set_ui_signal(); + }); + } + } + } + + error!("matrix_worker_task task ended unexpectedly"); + bail!("matrix_worker_task task ended unexpectedly") +} + + +/// The single global Tokio runtime that is used by all async tasks. +static TOKIO_RUNTIME: Mutex> = Mutex::new(None); + +/// The sender used by [`submit_async_request`] to send requests to the async worker thread. +/// Currently there is only one, but it can be cloned if we need more concurrent senders. +static REQUEST_SENDER: Mutex>> = Mutex::new(None); + +/// A client object that is proactively created during initialization +/// in order to speed up the client-building process when the user logs in. +static DEFAULT_SSO_CLIENT: Mutex> = Mutex::new(None); + +/// Used to notify the SSO login task that the async creation of the `DEFAULT_SSO_CLIENT` has finished. +static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = LazyLock::new(|| Arc::new(Notify::new())); + +/// Blocks the current thread until the given future completes. +/// +/// ## Warning +/// This should be used with caution, especially on the main UI thread, +/// as blocking a thread prevents it from handling other events or running other tasks. +pub fn block_on_async_with_timeout( + timeout: Option, + async_future: impl Future, +) -> Result { + let rt = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| + tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") + ).handle().clone(); + + if let Some(timeout) = timeout { + rt.block_on(async { + tokio::time::timeout(timeout, async_future).await + }) + } else { + Ok(rt.block_on(async_future)) + } +} + + +/// The primary initialization routine for starting the Matrix client sync +/// and the async tokio runtime. +/// +/// Returns a handle to the Tokio runtime that is used to run async background tasks. +pub fn start_matrix_tokio() -> Result { + // Create a Tokio runtime, and save it in a static variable to ensure it isn't dropped. + let rt_handle = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| { + tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") + }).handle().clone(); + + // Proactively build a Matrix Client in the background so that the SSO Server + // can have a quicker start if needed (as it's rather slow to build this client). + rt_handle.spawn(async move { + match build_client(&Cli::default(), app_data_dir()).await { + Ok(client_and_session) => { + DEFAULT_SSO_CLIENT.lock().unwrap() + .get_or_insert(client_and_session); + } + Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), + }; + DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); + Cx::post_action(LoginAction::SsoPending(false)); + }); + + let rt = rt_handle.clone(); + // Spawn the main async task that drives the Matrix client SDK, which itself will + // start and monitor other related background tasks. + rt_handle.spawn(start_matrix_client_login_and_sync(rt)); + + Ok(rt_handle) +} + + +/// A tokio::watch channel sender for sending requests from the RoomScreen UI widget +/// to the corresponding background async task for that room (its `timeline_subscriber_handler`). +pub type TimelineRequestSender = watch::Sender>; + +/// The return type for [`take_timeline_endpoints()`]. +/// +/// This primarily contains endpoints for channels of communication +/// between the timeline UI (`RoomScreen`] and the background worker tasks. +/// If the relevant room was tombstoned, this also includes info about its successor room. +pub struct TimelineEndpoints { + pub update_sender: crossbeam_channel::Sender, + pub update_receiver: crossbeam_channel::Receiver, + pub request_sender: TimelineRequestSender, + pub successor_room: Option, +} + +/// Info about a timeline for a joined room or a thread in a joined room. +struct PerTimelineDetails { + /// A shared reference to a room's main timeline or thread's timeline of events. + timeline: Arc, + /// A clone-able sender for updates to this timeline. + timeline_update_sender: crossbeam_channel::Sender, + /// A tuple of two separate channel endpoints that can only be taken *once* by the main UI thread. + /// + /// 1. The single receiver that can receive updates from this timeline. + /// * When a new room is joined (or a thread is opened), an unbounded crossbeam channel will be created + /// and its sender given to a background task (the `timeline_subscriber_handler()`) + /// that enqueues timeline updates as it receives timeline vector diffs from the server. + /// * The UI thread can take ownership of this update receiver in order to receive updates + /// to this room or thread timeline, but only one receiver can exist at a time. + /// 2. The sender that can send requests to the background timeline subscriber handler, + /// e.g., to watch for a specific event to be prepended to the timeline (via back pagination). + timeline_singleton_endpoints: Option<( + crossbeam_channel::Receiver, + TimelineRequestSender, + )>, + /// The async task that listens for updates for this timeline. + timeline_subscriber_handler_task: JoinHandle<()>, +} + +struct JoinedRoomDetails { + /// The room ID of this joined room. + room_id: OwnedRoomId, + /// Details about the main timeline for this room. + main_timeline: PerTimelineDetails, + /// Thread-focused timelines for this room, keyed by thread root event ID. + thread_timelines: HashMap, + /// The set of thread timelines currently being created, to avoid duplicate in-flight work. + pending_thread_timelines: HashSet, + /// A drop guard for the event handler that represents a subscription to typing notices for this room. + typing_notice_subscriber: Option, + /// A drop guard for the event handler that represents a subscription to pinned events for this room. + pinned_events_subscriber: Option, +} +impl Drop for JoinedRoomDetails { + fn drop(&mut self) { + log!("Dropping JoinedRoomDetails for room {}", self.room_id); + self.main_timeline.timeline_subscriber_handler_task.abort(); + for thread_timeline in self.thread_timelines.values() { + thread_timeline.timeline_subscriber_handler_task.abort(); + } + drop(self.typing_notice_subscriber.take()); + drop(self.pinned_events_subscriber.take()); + } +} + + +/// A const-compatible hasher, used for `static` items containing `HashMap`s or `HashSet`s. +type ConstHasher = BuildHasherDefault; + +/// Information about all joined rooms that our client currently know about. +/// We use a `HashMap` for O(1) lookups, as this is accessed frequently (e.g. every timeline update). +static ALL_JOINED_ROOMS: Mutex> = Mutex::new(HashMap::with_hasher(BuildHasherDefault::new())); + +/// Returns the timeline and timeline update sender for the given joined room/thread timeline. +fn get_per_timeline_details<'a>( + all_joined_rooms: &'a mut HashMap, + kind: &TimelineKind, +) -> Option<&'a mut PerTimelineDetails> { + let room_info = all_joined_rooms.get_mut(kind.room_id())?; + match kind { + TimelineKind::MainRoom { .. } => Some(&mut room_info.main_timeline), + TimelineKind::Thread { thread_root_event_id, .. } => room_info.thread_timelines.get_mut(thread_root_event_id), + } +} + +/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the timeline for the given timeline kind. +fn get_timeline(kind: &TimelineKind) -> Option> { + get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap().deref_mut(), kind) + .map(|details| details.timeline.clone()) +} + +/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the timeline and timeline update sender for the given timeline kind. +fn get_timeline_and_sender(kind: &TimelineKind) -> Option<(Arc, crossbeam_channel::Sender)> { + get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap().deref_mut(), kind) + .map(|details| (details.timeline.clone(), details.timeline_update_sender.clone())) +} + +/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the main timeline for the given room. +fn get_room_timeline(room_id: &RoomId) -> Option> { + ALL_JOINED_ROOMS.lock().unwrap() + .get(room_id) + .map(|jrd| jrd.main_timeline.timeline.clone()) +} + +/// The logged-in Matrix client, which can be freely and cheaply cloned. +static CLIENT: Mutex> = Mutex::new(None); + +pub fn get_client() -> Option { + CLIENT.lock().unwrap().clone() +} + +/// Returns the user ID of the currently logged-in user, if any. +pub fn current_user_id() -> Option { + CLIENT.lock().unwrap().as_ref().and_then(|c| + c.session_meta().map(|m| m.user_id.clone()) + ) +} + +/// The singleton sync service. +static SYNC_SERVICE: Mutex>> = Mutex::new(None); + + +/// Get a reference to the current sync service, if available. +pub fn get_sync_service() -> Option> { + SYNC_SERVICE.lock().ok()?.as_ref().cloned() +} + +/// The list of users that the current user has chosen to ignore. +/// Ideally we shouldn't have to maintain this list ourselves, +/// but the Matrix SDK doesn't currently properly maintain the list of ignored users. +static IGNORED_USERS: Mutex> = Mutex::new(HashSet::with_hasher(BuildHasherDefault::new())); + +/// Returns a deep clone of the current list of ignored users. +pub fn get_ignored_users() -> HashSet { + IGNORED_USERS.lock().unwrap().clone() +} + +/// Returns whether the given user ID is currently being ignored. +pub fn is_user_ignored(user_id: &UserId) -> bool { + IGNORED_USERS.lock().unwrap().contains(user_id) +} + + +/// Returns three channel endpoints related to the timeline for the given joined room or thread. +/// +/// 1. A timeline update sender. +/// 2. The timeline update receiver, which is a singleton, and can only be taken once. +/// 3. A `tokio::watch` sender that can be used to send requests to the timeline subscriber handler. +/// +/// This will only succeed once per room (or once per room thread), +/// as only a single channel receiver can exist. +pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option { + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let jrd = all_joined_rooms.get_mut(kind.room_id())?; + let details = match kind { + TimelineKind::MainRoom { .. } => &mut jrd.main_timeline, + TimelineKind::Thread { thread_root_event_id, .. } => jrd.thread_timelines.get_mut(thread_root_event_id)?, + }; + let (update_receiver, request_sender) = details.timeline_singleton_endpoints.take()?; + Some(TimelineEndpoints { + update_sender: details.timeline_update_sender.clone(), + update_receiver, + request_sender, + successor_room: details.timeline.room().successor_room(), + }) +} + +const DEFAULT_HOMESERVER: &str = "matrix.org"; + +fn username_to_full_user_id( + username: &str, + homeserver: Option<&str>, +) -> Option { + username + .try_into() + .ok() + .or_else(|| { + let homeserver_url = homeserver.unwrap_or(DEFAULT_HOMESERVER); + let user_id_str = if username.starts_with("@") { + format!("{}:{}", username, homeserver_url) + } else { + format!("@{}:{}", username, homeserver_url) + }; + user_id_str.as_str().try_into().ok() + }) +} + + +/// Info we store about a room received by the room list service. +/// +/// This struct is necessary in order for us to track the previous state +/// of a room received from the room list service, so that we can +/// determine what room data has changed since the last update. +/// We can't just store the `matrix_sdk::Room` object itself, +/// because that is a shallow reference to an inner room object within +/// the room list service. +#[derive(Clone)] +struct RoomListServiceRoomInfo { + room_id: OwnedRoomId, + state: RoomState, + is_direct: bool, + is_marked_unread: bool, + is_tombstoned: bool, + tags: Option, + user_power_levels: Option, + latest_event_timestamp: Option, + num_unread_messages: u64, + num_unread_mentions: u64, + display_name: Option, + room_avatar: Option, + room: matrix_sdk::Room, +} +impl RoomListServiceRoomInfo { + async fn from_room(room: matrix_sdk::Room, current_user_id: &Option) -> Self { + // Parallelize fetching of independent room data. + let (is_direct, tags, display_name, user_power_levels) = tokio::join!( + room.is_direct(), + room.tags(), + room.display_name(), + async { + if let Some(user_id) = current_user_id { + UserPowerLevels::from_room(&room, user_id.deref()).await + } else { + None + } + } + ); + + Self { + room_id: room.room_id().to_owned(), + state: room.state(), + is_direct: is_direct.unwrap_or(false), + is_marked_unread: room.is_marked_unread(), + is_tombstoned: room.is_tombstoned(), + tags: tags.ok().flatten(), + user_power_levels, + latest_event_timestamp: room.latest_event_timestamp(), + num_unread_messages: room.num_unread_messages(), + num_unread_mentions: room.num_unread_mentions(), + display_name: display_name.ok(), + room_avatar: room.avatar_url(), + room, + } + } + async fn from_room_ref(room: &matrix_sdk::Room, current_user_id: &Option) -> Self { + Self::from_room(room.clone(), current_user_id).await + } +} + +/// Performs the Matrix client login or session restore, and starts the main sync service. +/// +/// After starting the sync service, this also starts the main room list service loop +/// and the main space service loop. +async fn start_matrix_client_login_and_sync(rt: Handle) { + // Create a channel for sending requests from the main UI thread to a background worker task. + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); + REQUEST_SENDER.lock().unwrap().replace(sender); + + let (login_sender, mut login_receiver) = tokio::sync::mpsc::channel(1); + + // Spawn the async worker task that handles matrix requests. + // We must do this now such that the matrix worker task can listen for incoming login requests + // from the UI, and forward them to this task (via the login_sender --> login_receiver). + let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); + + let most_recent_user_id = persistence::most_recent_user_id().await; + log!("Most recent user ID: {most_recent_user_id:?}"); + let cli_parse_result = Cli::try_parse(); + let cli_has_valid_username_password = cli_parse_result.as_ref() + .is_ok_and(|cli| !cli.user_id.is_empty() && !cli.password.is_empty()); + log!("CLI parsing succeeded? {}. CLI has valid UN+PW? {}", + cli_parse_result.as_ref().is_ok(), + cli_has_valid_username_password, + ); + let wait_for_login = !cli_has_valid_username_password && ( + most_recent_user_id.is_none() + || std::env::args().any(|arg| arg == "--login-screen" || arg == "--force-login") + ); + log!("Waiting for login? {}", wait_for_login); + + let new_login_opt = if !wait_for_login { + let specified_username = cli_parse_result.as_ref().ok().and_then(|cli| + username_to_full_user_id( + &cli.user_id, + cli.homeserver.as_deref(), + ) + ); + log!("Trying to restore session for user: {:?}", + specified_username.as_ref().or(most_recent_user_id.as_ref()) + ); + match persistence::restore_session(specified_username).await { + Ok(session) => Some(session), + Err(e) => { + let status_err = "Could not restore previous user session.\n\nPlease login again."; + log!("{status_err} Error: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); + + if let Ok(cli) = &cli_parse_result { + log!("Attempting auto-login from CLI arguments as user '{}'...", cli.user_id); + Cx::post_action(LoginAction::CliAutoLogin { + user_id: cli.user_id.clone(), + homeserver: cli.homeserver.clone(), + }); + match login(cli, LoginRequest::LoginByCli).await { + Ok(new_login) => Some(new_login), + Err(e) => { + error!("CLI-based login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure( + format!("Could not login with CLI-provided arguments.\n\nPlease login manually.\n\nError: {e}") + )); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: format!("Login failed: {e:?}"), + }); + None + } + } + } else { + None + } + } + } + } else { + None + }; + let cli: Cli = cli_parse_result.unwrap_or(Cli::default()); + // `initial_client_opt` holds the client obtained from the session restore or CLI auto-login. + // On subsequent iterations of the login loop (after a post-auth setup failure), it is `None`, + // which causes the loop to wait for the user to submit a new manual login request. + let mut initial_client_opt = new_login_opt; + + let (client, sync_service, logged_in_user_id) = 'login_loop: loop { + let (client, _sync_token) = match initial_client_opt.take() { + Some(login) => login, + None => { + loop { + log!("Waiting for login request..."); + match login_receiver.recv().await { + Some(login_request) => { + match login(&cli, login_request).await { + Ok((client, sync_token)) => break (client, sync_token), + Err(e) => { + error!("Login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: format!("Login failed: {e}"), + }); + } + } + }, + None => { + error!("BUG: login_receiver hung up unexpectedly"); + let err = String::from("Please restart Robrix.\n\nUnable to listen for login requests."); + Cx::post_action(LoginAction::LoginFailure(err.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err, + }); + return; + } + } + } + } + }; + + // Deallocate the default SSO client after a successful login. + if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { + let _ = client_opt.take(); + } + + let logged_in_user_id: OwnedUserId = client.user_id() + .expect("BUG: Client::user_id() returned None after successful login!") + .to_owned(); + let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); + enqueue_rooms_list_update(RoomsListUpdate::Status { status }); + + // Store this active client in our global Client state so that other tasks can access it. + if let Some(_existing) = CLIENT.lock().unwrap().replace(client.clone()) { + error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); + } + + // Listen for changes to our verification status and incoming verification requests. + add_verification_event_handlers_and_sync_client(client.clone()); + + // Listen for updates to the ignored user list. + handle_ignore_user_list_subscriber(client.clone()); + + // Listen for session changes, e.g., when the access token becomes invalid. + handle_session_changes(client.clone()); + + Cx::post_action(LoginAction::Status { + title: "Connecting".into(), + status: "Setting up sync service...".into(), + }); + let sync_service = match SyncService::builder(client.clone()) + .with_offline_mode() + .build() + .await + { + Ok(ss) => ss, + Err(e) => { + error!("Failed to create SyncService: {e:?}"); + let err_msg = if is_invalid_token_error(&e) { + "Your login token is no longer valid.\n\nPlease log in again.".to_string() + } else { + format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") + }; + Cx::post_action(LoginAction::LoginFailure(err_msg.clone())); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg }); + // Clear the stored client so the next login attempt doesn't trigger the + // "unexpectedly replaced an existing client" warning. + let _ = CLIENT.lock().unwrap().take(); + continue 'login_loop; + } + }; + + break 'login_loop (client, sync_service, logged_in_user_id); + }; + + // Signal login success now that SyncService::build() has already succeeded (inside + // 'login_loop), which is the only step that can fail with an invalid/expired token. + // Doing this before sync_service.start() lets the UI transition to the home screen + // without waiting for the sync loop to begin. + Cx::post_action(LoginAction::LoginSuccess); + + // Attempt to load the previously-saved app state. + handle_load_app_state(logged_in_user_id.to_owned()); + handle_sync_indicator_subscriber(&sync_service); + handle_sync_service_state_subscriber(sync_service.state()); + sync_service.start().await; + + let room_list_service = sync_service.room_list_service(); + + if let Some(_existing) = SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)) { + error!("BUG: unexpectedly replaced an existing sync service when initializing the matrix client."); + } + + let mut room_list_service_task = rt.spawn(room_list_service_loop(room_list_service)); + let mut space_service_task = rt.spawn(space_service_loop(client)); + + // Now, this task becomes an infinite loop that monitors the state of the + // three core matrix-related background tasks that we just spawned above. + #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. + loop { + tokio::select! { + result = &mut matrix_worker_task_handle => { + match result { + Ok(Ok(())) => { + // Check if this is due to logout + if is_logout_in_progress() { + log!("matrix worker task ended due to logout"); + } else { + error!("BUG: matrix worker task ended unexpectedly!"); + } + } + Ok(Err(e)) => { + // Check if this is due to logout + if is_logout_in_progress() { + log!("matrix worker task ended with error due to logout: {e:?}"); + } else { + error!("Error: matrix worker task ended:\n\t{e:?}"); + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { + status: e.to_string(), + }); + enqueue_popup_notification( + format!("Rooms list update error: {e}"), + PopupKind::Error, + None, + ); + } + }, + Err(e) => { + error!("BUG: failed to join matrix worker task: {e:?}"); + } + } + break; + } + result = &mut room_list_service_task => { + match result { + Ok(Ok(())) => { + error!("BUG: room list service loop task ended unexpectedly!"); + } + Ok(Err(e)) => { + error!("Error: room list service loop task ended:\n\t{e:?}"); + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { + status: e.to_string(), + }); + enqueue_popup_notification( + format!("Room list service error: {e}"), + PopupKind::Error, + None, + ); + }, + Err(e) => { + error!("BUG: failed to join room list service loop task: {e:?}"); + } + } + break; + } + result = &mut space_service_task => { + match result { + Ok(Ok(())) => { + error!("BUG: space service loop task ended unexpectedly!"); + } + Ok(Err(e)) => { + error!("Error: space service loop task ended:\n\t{e:?}"); + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { + status: e.to_string(), + }); + enqueue_popup_notification( + format!("Space service error: {e}"), + PopupKind::Error, + None, + ); + }, + Err(e) => { + error!("BUG: failed to join space service loop task: {e:?}"); + } + } + break; + } + } + } +} + + +/// The main async task that listens for changes to all rooms. +async fn room_list_service_loop(room_list_service: Arc) -> Result<()> { + let all_rooms_list = room_list_service.all_rooms().await?; + handle_room_list_service_loading_state(all_rooms_list.loading_state()); + + let (room_diff_stream, room_list_dynamic_entries_controller) = + // TODO: paginate room list to avoid loading all rooms at once + all_rooms_list.entries_with_dynamic_adapters(usize::MAX); + + // By default, our rooms list should only show rooms that are: + // 1. not spaces (those are handled by the SpaceService), + // 2. not left (clients don't typically show rooms that the user has already left), + // 3. not outdated (don't show tombstoned rooms whose successor is already joined). + room_list_dynamic_entries_controller.set_filter(Box::new( + filters::new_filter_all(vec![ + Box::new(filters::new_filter_not(Box::new(filters::new_filter_space()))), + Box::new(filters::new_filter_non_left()), + Box::new(filters::new_filter_deduplicate_versions()), + ]) + )); + + let mut all_known_rooms: Vector = Vector::new(); + let current_user_id = current_user_id(); + + pin_mut!(room_diff_stream); + while let Some(batch) = room_diff_stream.next().await { + let mut peekable_diffs = batch.into_iter().peekable(); + while let Some(diff) = peekable_diffs.next() { + let is_reset = matches!(diff, VectorDiff::Reset { .. }); + match diff { + VectorDiff::Append { values: new_rooms } + | VectorDiff::Reset { values: new_rooms } => { + // Append and Reset are identical, except for Reset first clears all rooms. + let _num_new_rooms = new_rooms.len(); + if is_reset { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Reset, old length {}, new length {}", all_known_rooms.len(), new_rooms.len()); } + // Iterate manually so we can know which rooms are being removed. + while let Some(room) = all_known_rooms.pop_back() { + remove_room(&room); + } + // ALL_JOINED_ROOMS should already be empty due to successive calls to `remove_room()`, + // so this is just a sanity check. + ALL_JOINED_ROOMS.lock().unwrap().clear(); + enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); + } else { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Append, old length {}, adding {} new items", all_known_rooms.len(), _num_new_rooms); } + } + + // Parallelize creating each room's RoomListServiceRoomInfo and adding that new room. + // We combine `from_room` and `add_new_room` into a single async task per room. + let new_room_infos: Vec = join_all( + new_rooms.into_iter().map(|room| async { + let room_info = RoomListServiceRoomInfo::from_room(room.into_inner(), ¤t_user_id).await; + if let Err(e) = add_new_room(&room_info, &room_list_service, false).await { + error!("Failed to add new room: {:?} ({}); error: {:?}", room_info.display_name, room_info.room_id, e); + } + room_info + }) + ).await; + + // Send room order update with the new room IDs + let (room_id_refs, room_ids) = { + let mut room_id_refs = Vec::with_capacity(new_room_infos.len()); + let mut room_ids = Vec::with_capacity(new_room_infos.len()); + for r in &new_room_infos { + room_id_refs.push(r.room_id.as_ref()); + room_ids.push(r.room_id.clone()); + } + (room_id_refs, room_ids) + }; + if !room_ids.is_empty() { + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Append { values: room_ids } + )); + room_list_service.subscribe_to_rooms(&room_id_refs).await; + all_known_rooms.extend(new_room_infos); + } + } + VectorDiff::Clear => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Clear"); } + all_known_rooms.clear(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); + enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); + } + VectorDiff::PushFront { value: new_room } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushFront"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; + let room_id = new_room.room_id.clone(); + add_new_room(&new_room, &room_list_service, true).await?; + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PushFront { value: room_id } + )); + all_known_rooms.push_front(new_room); + } + VectorDiff::PushBack { value: new_room } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushBack"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; + let room_id = new_room.room_id.clone(); + add_new_room(&new_room, &room_list_service, true).await?; + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PushBack { value: room_id } + )); + all_known_rooms.push_back(new_room); + } + remove_diff @ VectorDiff::PopFront => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PopFront"); } + if let Some(room) = all_known_rooms.pop_front() { + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopFront)); + optimize_remove_then_add_into_update( + remove_diff, + &room, + &mut peekable_diffs, + &mut all_known_rooms, + &room_list_service, + ¤t_user_id, + ).await?; + } + } + remove_diff @ VectorDiff::PopBack => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PopBack"); } + if let Some(room) = all_known_rooms.pop_back() { + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopBack)); + optimize_remove_then_add_into_update( + remove_diff, + &room, + &mut peekable_diffs, + &mut all_known_rooms, + &room_list_service, + ¤t_user_id, + ).await?; + } + } + VectorDiff::Insert { index, value: new_room } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Insert at {index}"); } + let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; + let room_id = new_room.room_id.clone(); + add_new_room(&new_room, &room_list_service, true).await?; + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Insert { index, value: room_id } + )); + all_known_rooms.insert(index, new_room); + } + VectorDiff::Set { index, value: changed_room } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Set at {index}"); } + let changed_room = RoomListServiceRoomInfo::from_room(changed_room.into_inner(), ¤t_user_id).await; + if let Some(old_room) = all_known_rooms.get(index) { + update_room(old_room, &changed_room, &room_list_service).await?; + } else { + error!("BUG: room list diff: Set index {index} was out of bounds."); + } + // Send order update (room ID at this index may have changed) + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Set { index, value: changed_room.room_id.clone() } + )); + all_known_rooms.set(index, changed_room); + } + remove_diff @ VectorDiff::Remove { index } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Remove at {index}"); } + if index < all_known_rooms.len() { + let room = all_known_rooms.remove(index); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Remove { index })); + optimize_remove_then_add_into_update( + remove_diff, + &room, + &mut peekable_diffs, + &mut all_known_rooms, + &room_list_service, + ¤t_user_id, + ).await?; + } else { + error!("BUG: room_list: diff Remove index {index} out of bounds, len {}", all_known_rooms.len()); + } + } + VectorDiff::Truncate { length } => { + if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Truncate to {length}"); } + // Iterate manually so we can know which rooms are being removed. + while all_known_rooms.len() > length { + if let Some(room) = all_known_rooms.pop_back() { + remove_room(&room); + } + } + all_known_rooms.truncate(length); // sanity check + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Truncate { length } + )); + } + } + } + } + + bail!("room list service sync loop ended unexpectedly") +} + + +/// Attempts to optimize a common RoomListService operation of remove + add. +/// +/// If a `Remove` diff (or `PopBack` or `PopFront`) is immediately followed by +/// an `Insert` diff (or `PushFront` or `PushBack`) for the same room, +/// we can treat it as a simple `Set` operation, in which we call `update_room()`. +/// This is much more efficient than removing the room and then adding it back. +/// +/// This tends to happen frequently in order to change the room's state +/// or to "sort" the room list by changing its positional order. +async fn optimize_remove_then_add_into_update( + remove_diff: VectorDiff, + room: &RoomListServiceRoomInfo, + peekable_diffs: &mut Peekable>>, + all_known_rooms: &mut Vector, + room_list_service: &RoomListService, + current_user_id: &Option, +) -> Result<()> { + let next_diff_was_handled: bool; + match peekable_diffs.peek() { + Some(VectorDiff::Insert { index: insert_index, value: new_room }) + if room.room_id == new_room.room_id() => + { + if LOG_ROOM_LIST_DIFFS { + log!("Optimizing {remove_diff:?} + Insert({insert_index}) into Update for room {}", room.room_id); + } + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; + update_room(room, &new_room, room_list_service).await?; + // Send order update for the insert + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Insert { index: *insert_index, value: new_room.room_id.clone() } + )); + all_known_rooms.insert(*insert_index, new_room); + next_diff_was_handled = true; + } + Some(VectorDiff::PushFront { value: new_room }) + if room.room_id == new_room.room_id() => + { + if LOG_ROOM_LIST_DIFFS { + log!("Optimizing {remove_diff:?} + PushFront into Update for room {}", room.room_id); + } + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; + update_room(room, &new_room, room_list_service).await?; + // Send order update for the push front + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PushFront { value: new_room.room_id.clone() } + )); + all_known_rooms.push_front(new_room); + next_diff_was_handled = true; + } + Some(VectorDiff::PushBack { value: new_room }) + if room.room_id == new_room.room_id() => + { + if LOG_ROOM_LIST_DIFFS { + log!("Optimizing {remove_diff:?} + PushBack into Update for room {}", room.room_id); + } + let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; + update_room(room, &new_room, room_list_service).await?; + // Send order update for the push back + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PushBack { value: new_room.room_id.clone() } + )); + all_known_rooms.push_back(new_room); + next_diff_was_handled = true; + } + _ => next_diff_was_handled = false, + } + if next_diff_was_handled { + peekable_diffs.next(); // consume the next diff + } else { + remove_room(room); + } + Ok(()) +} + + +/// Invoked when the room list service has received an update that changes an existing room. +async fn update_room( + old_room: &RoomListServiceRoomInfo, + new_room: &RoomListServiceRoomInfo, + room_list_service: &RoomListService, +) -> Result<()> { + let new_room_id = new_room.room_id.clone(); + if old_room.room_id == new_room_id { + // Handle state transitions for a room. + if LOG_ROOM_LIST_DIFFS { + log!("Room {:?} ({new_room_id}) state went from {:?} --> {:?}", new_room.display_name, old_room.state, new_room.state); + } + if old_room.state != new_room.state { + match new_room.state { + RoomState::Banned => { + // TODO: handle rooms that this user has been banned from. + log!("Removing Banned room: {:?} ({new_room_id})", new_room.display_name); + remove_room(new_room); + return Ok(()); + } + RoomState::Left => { + log!("Removing Left room: {:?} ({new_room_id})", new_room.display_name); + // TODO: instead of removing this, we could optionally add it to + // a separate list of left rooms, which would be collapsed by default. + // Upon clicking a left room, we could show a splash page + // that prompts the user to rejoin the room or forget it permanently. + // Currently, we just remove it and do not show left rooms at all. + remove_room(new_room); + return Ok(()); + } + RoomState::Joined => { + log!("update_room(): adding new Joined room: {:?} ({new_room_id})", new_room.display_name); + return add_new_room(new_room, room_list_service, true).await; + } + RoomState::Invited => { + log!("update_room(): adding new Invited room: {:?} ({new_room_id})", new_room.display_name); + return add_new_room(new_room, room_list_service, true).await; + } + RoomState::Knocked => { + // TODO: handle Knocked rooms (e.g., can you re-knock? or cancel a prior knock?) + return Ok(()); + } + } + } + + // First, we check for changes to room data that is relevant to any room, + // including joined, invited, and other rooms. + // This includes the room name and room avatar. + if old_room.room_avatar != new_room.room_avatar { + log!("Updating room avatar for room {}", new_room_id); + spawn_fetch_room_avatar(new_room); + } + if old_room.display_name != new_room.display_name { + log!("Updating room {} name: {:?} --> {:?}", new_room_id, old_room.display_name, new_room.display_name); + + enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName { + new_room_name: (new_room.display_name.clone(), new_room_id.clone()).into(), + }); + } + + // Then, we check for changes to room data that is only relevant to joined rooms: + // including the latest event, tags, unread counts, is_direct, tombstoned state, power levels, etc. + // Invited or left rooms don't care about these details. + if matches!(new_room.state, RoomState::Joined) { + // For some reason, the latest event API does not reliably catch *all* changes + // to the latest event in a given room, such as redactions. + // Thus, we have to re-obtain the latest event on *every* update, regardless of timestamp. + // + let update_latest = match (old_room.latest_event_timestamp, new_room.room.latest_event_timestamp()) { + (Some(old_ts), Some(new_ts)) => new_ts >= old_ts, + (None, Some(_)) => true, + _ => false, + }; + if update_latest { + update_latest_event(&new_room.room).await; + } + + + if old_room.tags != new_room.tags { + log!("Updating room {} tags from {:?} to {:?}", new_room_id, old_room.tags, new_room.tags); + enqueue_rooms_list_update(RoomsListUpdate::Tags { + room_id: new_room_id.clone(), + new_tags: new_room.tags.clone().unwrap_or_default(), + }); + } + + if old_room.is_marked_unread != new_room.is_marked_unread + || old_room.num_unread_messages != new_room.num_unread_messages + || old_room.num_unread_mentions != new_room.num_unread_mentions + { + log!("Updating room {}, marked unread {} --> {}, unread messages {} --> {}, unread mentions {} --> {}", + new_room_id, + old_room.is_marked_unread, new_room.is_marked_unread, + old_room.num_unread_messages, new_room.num_unread_messages, + old_room.num_unread_mentions, new_room.num_unread_mentions, + ); + enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { + room_id: new_room_id.clone(), + is_marked_unread: new_room.is_marked_unread, + unread_messages: UnreadMessageCount::Known(new_room.num_unread_messages), + unread_mentions: new_room.num_unread_mentions, + }); + } + + if old_room.is_direct != new_room.is_direct { + log!("Updating room {} is_direct from {} to {}", + new_room_id, + old_room.is_direct, + new_room.is_direct, + ); + enqueue_rooms_list_update(RoomsListUpdate::UpdateIsDirect { + room_id: new_room_id.clone(), + is_direct: new_room.is_direct, + }); + } + + let mut __timeline_update_sender_opt = None; + let mut get_timeline_update_sender = |room_id| { + if __timeline_update_sender_opt.is_none() { + if let Some(jrd) = ALL_JOINED_ROOMS.lock().unwrap().get(room_id) { + __timeline_update_sender_opt = Some(jrd.main_timeline.timeline_update_sender.clone()); + } + } + __timeline_update_sender_opt.clone() + }; + + if !old_room.is_tombstoned && new_room.is_tombstoned { + let successor_room = new_room.room.successor_room(); + log!("Updating room {new_room_id} to be tombstoned, {successor_room:?}"); + enqueue_rooms_list_update(RoomsListUpdate::TombstonedRoom { room_id: new_room_id.clone() }); + if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { + spawn_fetch_successor_room_preview( + room_list_service.client().clone(), + successor_room, + new_room_id.clone(), + timeline_update_sender, + ); + } else { + error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); + } + } + + if let Some(nupl) = new_room.user_power_levels + && old_room.user_power_levels.is_none_or(|oupl| oupl != nupl) + { + if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { + log!("Updating room {new_room_id} user power levels."); + match timeline_update_sender.send(TimelineUpdate::UserPowerLevels(nupl)) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => error!("Failed to send the UserPowerLevels update to room {new_room_id}"), + } + } else { + error!("BUG: could not find JoinedRoomDetails for room {new_room_id} where power levels changed."); + } + } + } + Ok(()) + } + else { + warning!("UNTESTED SCENARIO: update_room(): removing old room {}, replacing with new room {}", + old_room.room_id, new_room_id, + ); + remove_room(old_room); + add_new_room(new_room, room_list_service, true).await + } +} + + +/// Invoked when the room list service has received an update to remove an existing room. +fn remove_room(room: &RoomListServiceRoomInfo) { + ALL_JOINED_ROOMS.lock().unwrap().remove(&room.room_id); + enqueue_rooms_list_update( + RoomsListUpdate::RemoveRoom { + room_id: room.room_id.clone(), + new_state: room.state, + } + ); +} + + +/// Invoked when the room list service has received an update with a brand new room. +async fn add_new_room( + new_room: &RoomListServiceRoomInfo, + room_list_service: &RoomListService, + subscribe: bool, +) -> Result<()> { + match new_room.state { + RoomState::Knocked => { + log!("Got new Knocked room: {:?} ({})", new_room.display_name, new_room.room_id); + // Note: here we could optionally display Knocked rooms as a separate type of room + // in the rooms list, but it's not really necessary at this point. + return Ok(()); + } + RoomState::Banned => { + log!("Got new Banned room: {:?} ({})", new_room.display_name, new_room.room_id); + // Note: here we could optionally display Banned rooms as a separate type of room + // in the rooms list, but it's not really necessary at this point. + return Ok(()); + } + RoomState::Left => { + log!("Got new Left room: {:?} ({:?})", new_room.display_name, new_room.room_id); + // Note: here we could optionally display Left rooms as a separate type of room + // in the rooms list, but it's not really necessary at this point. + return Ok(()); + } + RoomState::Invited => { + let invite_details = new_room.room.invite_details().await.ok(); + let room_name_id = RoomNameId::from((new_room.display_name.clone(), new_room.room_id.clone())); + // Start with a basic text avatar; the avatar image will be fetched asynchronously below. + let room_avatar = avatar_from_room_name(room_name_id.name_for_avatar()); + let inviter_info = if let Some(inviter) = invite_details.and_then(|d| d.inviter) { + Some(InviterInfo { + user_id: inviter.user_id().to_owned(), + display_name: inviter.display_name().map(|n| n.to_string()), + avatar: inviter + .avatar(AVATAR_THUMBNAIL_FORMAT.into()) + .await + .ok() + .flatten() + .map(Into::into), + }) + } else { + None + }; + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(InvitedRoomInfo { + room_name_id: room_name_id.clone(), + inviter_info, + room_avatar, + canonical_alias: new_room.room.canonical_alias(), + alt_aliases: new_room.room.alt_aliases(), + // we don't actually display the latest event for Invited rooms, so don't bother. + latest: None, + invite_state: Default::default(), + is_selected: false, + is_direct: new_room.is_direct, + })); + Cx::post_action(AppStateAction::RoomLoadedSuccessfully { + room_name_id, + is_invite: true, + }); + spawn_fetch_room_avatar(new_room); + return Ok(()); + } + RoomState::Joined => { } // Fall through to adding the joined room below. + } + + // If we didn't already subscribe to this room, do so now. + // This ensures we will properly receive all of its states and latest event. + if subscribe { + room_list_service.subscribe_to_rooms(&[&new_room.room_id]).await; + } + + let timeline = Arc::new( + new_room.room.timeline_builder() + .with_focus(TimelineFocus::Live { + // we show threads as separate timelines in their own RoomScreen + hide_threaded_events: true, + }) + .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) + .build() + .await + .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {}: {e}", new_room.room_id))?, + ); + let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); + + let (request_sender, request_receiver) = watch::channel(Vec::new()); + let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler( + new_room.room.clone(), + timeline.clone(), + timeline_update_sender.clone(), + request_receiver, + None, + )); + + // We need to add the room to the `ALL_JOINED_ROOMS` list before we can send + // an `AddJoinedRoom` update to the RoomsList widget, because that widget might + // immediately issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`. + log!("Adding new joined room {}, name: {:?}", new_room.room_id, new_room.display_name); + ALL_JOINED_ROOMS.lock().unwrap().insert( + new_room.room_id.clone(), + JoinedRoomDetails { + room_id: new_room.room_id.clone(), + main_timeline: PerTimelineDetails { + timeline, + timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)), + timeline_update_sender, + timeline_subscriber_handler_task, + }, + thread_timelines: HashMap::new(), + pending_thread_timelines: HashSet::new(), + typing_notice_subscriber: None, + pinned_events_subscriber: None, + }, + ); + + let latest = get_latest_event_details( + &new_room.room.latest_event().await, + room_list_service.client(), + ).await; + let room_name_id = RoomNameId::from((new_room.display_name.clone(), new_room.room_id.clone())); + // Start with a basic text avatar; the avatar image will be fetched asynchronously below. + let room_avatar = avatar_from_room_name(room_name_id.name_for_avatar()); + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddJoinedRoom(JoinedRoomInfo { + latest, + tags: new_room.tags.clone().unwrap_or_default(), + num_unread_messages: new_room.num_unread_messages, + num_unread_mentions: new_room.num_unread_mentions, + is_marked_unread: new_room.is_marked_unread, + room_avatar, + room_name_id: room_name_id.clone(), + canonical_alias: new_room.room.canonical_alias(), + alt_aliases: new_room.room.alt_aliases(), + has_been_paginated: false, + is_selected: false, + is_direct: new_room.is_direct, + is_tombstoned: new_room.is_tombstoned, + })); + + Cx::post_action(AppStateAction::RoomLoadedSuccessfully { + room_name_id, + is_invite: false, + }); + spawn_fetch_room_avatar(new_room); + Ok(()) +} + +#[allow(unused)] +async fn current_ignore_user_list(client: &Client) -> Option> { + use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent; + let ignored_users = client.account() + .account_data::() + .await + .ok()?? + .deserialize() + .ok()? + .ignored_users + .into_keys() + .collect(); + + Some(ignored_users) +} + +fn handle_ignore_user_list_subscriber(client: Client) { + let mut subscriber = client.subscribe_to_ignore_user_list_changes(); + log!("Initial ignored-user list is: {:?}", subscriber.get()); + Handle::current().spawn(async move { + let mut first_update = true; + while let Some(ignore_list) = subscriber.next().await { + log!("Received an updated ignored-user list: {ignore_list:?}"); + let ignored_users_new = ignore_list + .into_iter() + .filter_map(|u| OwnedUserId::try_from(u).ok()) + .collect::>(); + + // TODO: when we support persistent state, don't forget to update `IGNORED_USERS` upon app boot. + let mut ignored_users_old = IGNORED_USERS.lock().unwrap(); + let has_changed = *ignored_users_old != ignored_users_new; + *ignored_users_old = ignored_users_new; + + if has_changed && !first_update { + // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. + // Therefore, we need to re-fetch all timelines for all rooms, + // and currently the only way to actually accomplish this is via pagination. + // See: + for joined_room in client.joined_rooms() { + submit_async_request(MatrixRequest::PaginateTimeline { + timeline_kind: TimelineKind::MainRoom { + room_id: joined_room.room_id().to_owned(), + }, + num_events: 50, + direction: PaginationDirection::Backwards, + }); + } + } + + first_update = false; + } + }); +} + +/// Asynchronously loads and restores the app state from persistent storage for the given user. +/// +/// If the loaded dock state contains open rooms and dock items, this function emits an action +/// to instruct the UI to restore the app state for the main home view (all rooms). +/// If loading fails, it shows a popup notification with the error message. +fn handle_load_app_state(user_id: OwnedUserId) { + Handle::current().spawn(async move { + match load_app_state(&user_id).await { + Ok(app_state) => { + if !app_state.saved_dock_state_home.open_rooms.is_empty() + && !app_state.saved_dock_state_home.dock_items.is_empty() + { + log!("Loaded room panel state from app data directory. Restoring now..."); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(app_state)); + } + } + Err(_e) => { + log!("Failed to restore dock layout from persistent state: {_e}"); + enqueue_popup_notification( + "Could not restore the previous dock layout.", + PopupKind::Error, + None, + ); + } + } + }); +} + +/// Returns `true` if the given sync service error is due to an invalid/expired access token. +fn is_invalid_token_error(e: &sync_service::Error) -> bool { + use matrix_sdk::ruma::api::client::error::ErrorKind; + let sdk_error = match e { + sync_service::Error::RoomList( + matrix_sdk_ui::room_list_service::Error::SlidingSync(err) + ) => err, + sync_service::Error::EncryptionSync( + encryption_sync_service::Error::SlidingSync(err) + ) => err, + _ => return false, + }; + matches!( + sdk_error.client_api_error_kind(), + Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) + ) +} + +/// Subscribes to session change notifications from the Matrix client. +/// +/// When the homeserver rejects the access token with a 401 `M_UNKNOWN_TOKEN` error +/// (e.g., the token was revoked or expired), this emits a [`LoginAction::LoginFailure`] +/// so the user is prompted to log in again. +fn handle_session_changes(client: Client) { + let mut receiver = client.subscribe_to_session_changes(); + Handle::current().spawn(async move { + loop { + match receiver.recv().await { + Ok(SessionChange::UnknownToken { soft_logout }) => { + let msg = if soft_logout { + "Your login session has expired.\n\nPlease log in again." + } else { + "Your login token is no longer valid.\n\nPlease log in again." + }; + error!("Session token is no longer valid (soft_logout: {soft_logout}). Prompting re-login."); + Cx::post_action(LoginAction::LoginFailure(msg.to_string())); + } + Ok(SessionChange::TokensRefreshed) => {} + Err(broadcast::error::RecvError::Lagged(n)) => { + warning!("Session change receiver lagged, missed {n} messages."); + } + Err(broadcast::error::RecvError::Closed) => { + break; + } + } + } + }); +} + +fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { + log!("Initial sync service state is {:?}", subscriber.get()); + Handle::current().spawn(async move { + while let Some(state) = subscriber.next().await { + log!("Received a sync service state update: {state:?}"); + match state { + sync_service::State::Error(e) => { + if is_invalid_token_error(&e) { + // The access token is invalid; `handle_session_changes` will have + // already posted a LoginAction::LoginFailure, so just log here. + error!("Sync service stopped due to invalid/expired access token: {e}."); + } else { + log!("Restarting sync service due to error: {e}."); + if let Some(ss) = get_sync_service() { + ss.start().await; + } else { + enqueue_popup_notification( + "Unable to restart the Matrix sync service.\n\nPlease quit and restart Robrix.", + PopupKind::Error, + None, + ); + } + } + } + other => Cx::post_action(RoomsListHeaderAction::StateUpdate(other)), + } + } + }); +} + +fn handle_sync_indicator_subscriber(sync_service: &SyncService) { + /// Duration for sync indicator delay before showing + const SYNC_INDICATOR_DELAY: Duration = Duration::from_millis(100); + /// Duration for sync indicator delay before hiding + const SYNC_INDICATOR_HIDE_DELAY: Duration = Duration::from_millis(200); + let sync_indicator_stream = sync_service.room_list_service() + .sync_indicator( + SYNC_INDICATOR_DELAY, + SYNC_INDICATOR_HIDE_DELAY + ); + + Handle::current().spawn(async move { + let mut sync_indicator_stream = std::pin::pin!(sync_indicator_stream); + + while let Some(indicator) = sync_indicator_stream.next().await { + let is_syncing = match indicator { + SyncIndicator::Show => true, + SyncIndicator::Hide => false, + }; + Cx::post_action(RoomsListHeaderAction::SetSyncStatus(is_syncing)); + } + }); +} + +fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { + log!("Initial room list loading state is {:?}", loading_state.get()); + Handle::current().spawn(async move { + while let Some(state) = loading_state.next().await { + log!("Received a room list loading state update: {state:?}"); + match state { + RoomListLoadingState::NotLoaded => { + enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); + } + RoomListLoadingState::Loaded { maximum_number_of_rooms } => { + enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { max_rooms: maximum_number_of_rooms }); + // The SDK docs state that we cannot move from the `Loaded` state + // back to the `NotLoaded` state, so we can safely exit this task here. + return; + } + } + } + }); +} + +/// Spawns an async task to fetch the RoomPreview for the given successor room. +/// +/// After the fetch completes, this emites a [`RoomPreviewAction`] +/// containing the fetched room preview or an error if it failed. +fn spawn_fetch_successor_room_preview( + client: Client, + successor_room: Option, + tombstoned_room_id: OwnedRoomId, + timeline_update_sender: crossbeam_channel::Sender, +) { + Handle::current().spawn(async move { + log!("Updating room {tombstoned_room_id} to be tombstoned, {successor_room:?}"); + let srd = if let Some(SuccessorRoom { room_id, reason }) = successor_room { + match fetch_room_preview_with_avatar( + &client, + room_id.deref().into(), + Vec::new(), + ).await { + Ok(room_preview) => SuccessorRoomDetails::Full { room_preview, reason }, + Err(e) => { + log!("Failed to fetch preview of successor room {room_id}, error: {e:?}"); + SuccessorRoomDetails::Basic(SuccessorRoom { room_id, reason }) + } + } + } else { + log!("BUG: room {tombstoned_room_id} was tombstoned but had no successor room!"); + SuccessorRoomDetails::None + }; + + match timeline_update_sender.send(TimelineUpdate::Tombstoned(srd)) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => error!("Failed to send the Tombstoned update to room {tombstoned_room_id}"), + } + }); +} + +/// Fetches the full preview information for the given `room`. +/// Also fetches that room preview's avatar, if it had an avatar URL. +async fn fetch_room_preview_with_avatar( + client: &Client, + room: &RoomOrAliasId, + via: Vec, +) -> Result { + let room_preview = client.get_room_preview(room, via).await?; + // If this room has an avatar URL, fetch it. + let room_avatar = if let Some(avatar_url) = room_preview.avatar_url.clone() { + let media_request = MediaRequestParameters { + source: MediaSource::Plain(avatar_url), + format: AVATAR_THUMBNAIL_FORMAT.into(), + }; + match client.media().get_media_content(&media_request, true).await { + Ok(avatar_content) => { + log!("Fetched avatar for room preview {:?} ({})", room_preview.name, room_preview.room_id); + FetchedRoomAvatar::Image(avatar_content.into()) + } + Err(e) => { + log!("Failed to fetch avatar for room preview {:?} ({}), error: {e:?}", + room_preview.name, room_preview.room_id + ); + avatar_from_room_name(room_preview.name.as_deref()) + } + } + } else { + // The successor room did not have an avatar URL + avatar_from_room_name(room_preview.name.as_deref()) + }; + Ok(FetchedRoomPreview::from(room_preview, room_avatar)) +} + +/// Fetches key details about the given thread root event. +/// +/// Returns a tuple of: +/// 1. the number of replies in the thread (excluding the root event itself), +/// 2. the latest reply event, if it could be fetched. +async fn fetch_thread_summary_details( + room: &Room, + thread_root_event_id: &EventId, +) -> (u32, Option) { + let mut num_replies = 0; + let mut latest_reply_event = None; + + if let Ok(thread_root_event) = room.load_or_fetch_event(thread_root_event_id, None).await + && let Some(thread_summary) = thread_root_event.thread_summary.summary() + { + num_replies = thread_summary.num_replies; + if let Some(latest_reply_event_id) = thread_summary.latest_reply.as_ref() + && let Ok(latest_reply) = room.load_or_fetch_event(latest_reply_event_id, None).await + { + latest_reply_event = Some(latest_reply); + } + } + + // Always compute the reply count directly from the fetched thread relations, + // for some reason we can't rely on the SDK-provided thread_summary to be accurate + // (it's almost always totally wrong or out-of-date...). + let count_replies_future = count_thread_replies(room, thread_root_event_id); + + // Fetch the latest reply event and count the thread replies in parallel. + let (fetched_latest_reply_opt, reply_count_opt) = if latest_reply_event.is_none() { + tokio::join!( + fetch_latest_thread_reply_event(room, thread_root_event_id), + count_replies_future, + ) + } else { + (None, count_replies_future.await) + }; + + if let Some(event) = fetched_latest_reply_opt { + latest_reply_event = Some(event); + } + if let Some(count) = reply_count_opt { + num_replies = count; + } + (num_replies, latest_reply_event) +} + +/// Fetches the latest reply event in the thread rooted at `thread_root_event_id`. +async fn fetch_latest_thread_reply_event( + room: &Room, + thread_root_event_id: &EventId, +) -> Option { + let options = RelationsOptions { + dir: Direction::Backward, + limit: Some(uint!(1)), + include_relations: IncludeRelations::RelationsOfType(RelationType::Thread), + ..Default::default() + }; + + room.relations(thread_root_event_id.to_owned(), options) + .await + .ok() + .and_then(|relations| relations.chunk.into_iter().next()) +} + +/// Counts all replies in the given thread by paginating `/relations` in batches. +async fn count_thread_replies( + room: &Room, + thread_root_event_id: &EventId, +) -> Option { + let mut total_replies: u32 = 0; + let mut next_batch_token = None; + + loop { + let options = RelationsOptions { + from: next_batch_token.clone(), + dir: Direction::Backward, + limit: Some(uint!(100)), + include_relations: IncludeRelations::RelationsOfType(RelationType::Thread), + ..Default::default() + }; + + let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; + if relations.chunk.is_empty() { + break; + } + total_replies = total_replies.saturating_add(relations.chunk.len() as u32); + + next_batch_token = relations.next_batch_token; + if next_batch_token.is_none() { + break; + } + } + + Some(total_replies) +} + +/// Returns an HTML-formatted text preview of the given latest thread reply event. +async fn text_preview_of_latest_thread_reply( + room: &Room, + latest_reply_event: &matrix_sdk::deserialized_responses::TimelineEvent, +) -> Option { + let raw = latest_reply_event.raw(); + let sender_id = raw.get_field::("sender").ok().flatten()?; + let sender_room_member = match room.get_member_no_sync(&sender_id).await { + Ok(Some(rm)) => Some(rm), + _ => room.get_member(&sender_id).await.ok().flatten(), + }; + let sender_name = sender_room_member.as_ref() + .and_then(|rm| rm.display_name()) + .unwrap_or(sender_id.as_str()); + let text_preview = text_preview_of_raw_timeline_event(raw, sender_name).unwrap_or_else(|| { + let event_type = raw.get_field::("type").ok().flatten(); + TextPreview::from(( + event_type.unwrap_or_else(|| "unknown event type".to_string()), + BeforeText::UsernameWithColon, + )) + }); + let preview_str = text_preview.format_with(sender_name, true); + match utils::replace_linebreaks_separators(&preview_str, true) { + Cow::Borrowed(_) => Some(preview_str), + Cow::Owned(replaced) => Some(replaced), + } +} + + +/// Returns the timestamp and an HTML-formatted text preview of the given `latest_event`. +/// +/// If the sender profile of the event is not yet available, this function will +/// generate a preview using the sender's user ID instead of their display name. +async fn get_latest_event_details( + latest_event_value: &LatestEventValue, + client: &Client, +) -> Option<(MilliSecondsSinceUnixEpoch, String)> { + macro_rules! get_sender_username { + ($profile:expr, $sender:expr, $is_own:expr) => {{ + let sender_username_opt = if let TimelineDetails::Ready(profile) = $profile { + profile.display_name.clone() + } else if $is_own { + client.account().get_display_name().await.ok().flatten() + } else { + None + }; + sender_username_opt.unwrap_or_else(|| $sender.to_string()) + }}; + } + + match latest_event_value { + LatestEventValue::None => None, + LatestEventValue::Remote { timestamp, sender, is_own, profile, content } => { + let sender_username = get_sender_username!(profile, sender, *is_own); + let latest_message_text = text_preview_of_timeline_item( + content, + sender, + &sender_username, + ).format_with(&sender_username, true); + Some((*timestamp, latest_message_text)) + } + LatestEventValue::Local { timestamp, sender, profile, content, state: _ } => { + // TODO: use the `state` enum to augment the preview text with more details. + // Example: "Sending... {msg}" or + // "Failed to send {msg}" + let is_own = current_user_id().is_some_and(|id| &id == sender); + let sender_username = get_sender_username!(profile, sender, is_own); + let latest_message_text = text_preview_of_timeline_item( + content, + sender, + &sender_username, + ).format_with(&sender_username, true); + Some((*timestamp, latest_message_text)) + } + } +} + +/// Handles the given updated latest event for the given room. +/// +/// This function sends a `RoomsListUpdate::UpdateLatestEvent` +/// to update the latest event in the RoomsListEntry for the given room. +async fn update_latest_event(room: &Room) { + if let Some((timestamp, latest_message_text)) = get_latest_event_details( + &room.latest_event().await, + &room.client(), + ).await { + enqueue_rooms_list_update(RoomsListUpdate::UpdateLatestEvent { + room_id: room.room_id().to_owned(), + timestamp, + latest_message_text, + }); + } +} + +/// A request to search backwards for a specific event in a room's timeline. +pub struct BackwardsPaginateUntilEventRequest { + pub room_id: OwnedRoomId, + pub target_event_id: OwnedEventId, + /// The index in the timeline where a backwards search should begin. + pub starting_index: usize, + /// The number of items in the timeline at the time of the request, + /// which is used to detect if the timeline has changed since the request was made, + /// meaning that the `starting_index` can no longer be relied upon. + pub current_tl_len: usize, +} + +/// Whether to enable verbose logging of all timeline diff updates. +const LOG_TIMELINE_DIFFS: bool = cfg!(feature = "log_timeline_diffs"); +/// Whether to enable verbose logging of all room list service diff updates. +const LOG_ROOM_LIST_DIFFS: bool = cfg!(feature = "log_room_list_diffs"); + +/// A per-timeline async task that listens for timeline updates and sends them to the UI thread. +/// +/// One instance of this async task is spawned for each room the client knows about, +/// and also one for each thread that the user opens in a thread view. +async fn timeline_subscriber_handler( + room: Room, + timeline: Arc, + timeline_update_sender: crossbeam_channel::Sender, + mut request_receiver: watch::Receiver>, + thread_root_event_id: Option, +) { + + /// An inner function that searches the given new timeline items for a target event. + /// + /// If the target event is found, it is removed from the `target_event_id_opt` and returned, + /// along with the index/position of that event in the given iterator of new items. + fn find_target_event<'a>( + target_event_id_opt: &mut Option, + mut new_items_iter: impl Iterator>, + ) -> Option<(usize, OwnedEventId)> { + let found_index = target_event_id_opt + .as_ref() + .and_then(|target_event_id| new_items_iter + .position(|new_item| new_item + .as_event() + .is_some_and(|new_ev| new_ev.event_id() == Some(target_event_id)) + ) + ); + + if let Some(index) = found_index { + target_event_id_opt.take().map(|ev| (index, ev)) + } else { + None + } + } + + + let room_id = room.room_id().to_owned(); + log!("Starting timeline subscriber for room {room_id}, thread {thread_root_event_id:?}..."); + let (mut timeline_items, mut subscriber) = timeline.subscribe().await; + log!("Received initial timeline update of {} items for room {room_id}, thread {thread_root_event_id:?}.", timeline_items.len()); + + timeline_update_sender.send(TimelineUpdate::FirstUpdate { + initial_items: timeline_items.clone(), + }).unwrap_or_else( + |_e| panic!("Error: timeline update sender couldn't send first update ({} items) to room {room_id}, thread {thread_root_event_id:?}...!", timeline_items.len()) + ); + + // the event ID to search for while loading previous items into the timeline. + let mut target_event_id = None; + // the timeline index and event ID of the target event, if it has been found. + let mut found_target_event_id: Option<(usize, OwnedEventId)> = None; + + loop { tokio::select! { + // we should check for new requests before handling new timeline updates, + // because the request might influence how we handle a timeline update. + biased; + + // Handle updates to the current backwards pagination requests. + Ok(()) = request_receiver.changed() => { + let prev_target_event_id = target_event_id.clone(); + let new_request_details = request_receiver + .borrow_and_update() + .iter() + .find_map(|req| req.room_id + .eq(&room_id) + .then(|| (req.target_event_id.clone(), req.starting_index, req.current_tl_len)) + ); + + target_event_id = new_request_details.as_ref().map(|(ev, ..)| ev.clone()); + + // If we received a new request, start searching backwards for the target event. + if let Some((new_target_event_id, starting_index, current_tl_len)) = new_request_details { + if prev_target_event_id.as_ref() != Some(&new_target_event_id) { + let starting_index = if current_tl_len == timeline_items.len() { + starting_index + } else { + // The timeline has changed since the request was made, so we can't rely on the `starting_index`. + // Instead, we have no choice but to start from the end of the timeline. + timeline_items.len() + }; + // log!("Received new request to search for event {new_target_event_id} in room {room_id}, thread {thread_root_event_id:?} starting from index {starting_index} (tl len {}).", timeline_items.len()); + // Search backwards for the target event in the timeline, starting from the given index. + if let Some(target_event_tl_index) = timeline_items + .focus() + .narrow(..starting_index) + .into_iter() + .rev() + .position(|i| i.as_event() + .and_then(|e| e.event_id()) + .is_some_and(|ev_id| ev_id == new_target_event_id) + ) + .map(|i| starting_index.saturating_sub(i).saturating_sub(1)) + { + // log!("Found existing target event {new_target_event_id} in room {room_id}, thread {thread_root_event_id:?} at index {target_event_tl_index}."); + + // Nice! We found the target event in the current timeline items, + // so there's no need to actually proceed with backwards pagination; + // thus, we can clear the locally-tracked target event ID. + target_event_id = None; + found_target_event_id = None; + timeline_update_sender.send( + TimelineUpdate::TargetEventFound { + target_event_id: new_target_event_id.clone(), + index: target_event_tl_index, + } + ).unwrap_or_else( + |_e| panic!("Error: timeline update sender couldn't send TargetEventFound({new_target_event_id}, {target_event_tl_index}) to room {room_id}, thread {thread_root_event_id:?}!") + ); + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); + } + else { + log!("Target event not in timeline. Starting backwards pagination \ + in room {room_id}, thread {thread_root_event_id:?} to find target event \ + {new_target_event_id} starting from index {starting_index}.", + ); + // If we didn't find the target event in the current timeline items, + // we need to start loading previous items into the timeline. + submit_async_request(MatrixRequest::PaginateTimeline { + timeline_kind: if let Some(thread_root_event_id) = thread_root_event_id.clone() { + TimelineKind::Thread { + room_id: room_id.clone(), + thread_root_event_id, + } + } else { + TimelineKind::MainRoom { + room_id: room_id.clone(), + } + }, + num_events: 50, + direction: PaginationDirection::Backwards, + }); + } + } + } + } + + // Handle updates to the actual timeline content. + batch_opt = subscriber.next() => { + let Some(batch) = batch_opt else { break }; + let mut num_updates = 0; + let mut index_of_first_change = usize::MAX; + let mut index_of_last_change = usize::MIN; + // whether to clear the entire cache of drawn items + let mut clear_cache = false; + // whether the changes include items being appended to the end of the timeline + let mut is_append = false; + for diff in batch { + num_updates += 1; + match diff { + VectorDiff::Append { values } => { + let _values_len = values.len(); + index_of_first_change = min(index_of_first_change, timeline_items.len()); + timeline_items.extend(values); + index_of_last_change = max(index_of_last_change, timeline_items.len()); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Append {_values_len}. Changes: {index_of_first_change}..{index_of_last_change}"); } + is_append = true; + } + VectorDiff::Clear => { + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Clear"); } + clear_cache = true; + timeline_items.clear(); + } + VectorDiff::PushFront { value } => { + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PushFront"); } + if let Some((index, _ev)) = found_target_event_id.as_mut() { + *index += 1; // account for this new `value` being prepended. + } else { + found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)); + } + + clear_cache = true; + timeline_items.push_front(value); + } + VectorDiff::PushBack { value } => { + index_of_first_change = min(index_of_first_change, timeline_items.len()); + timeline_items.push_back(value); + index_of_last_change = max(index_of_last_change, timeline_items.len()); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PushBack. Changes: {index_of_first_change}..{index_of_last_change}"); } + is_append = true; + } + VectorDiff::PopFront => { + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopFront"); } + clear_cache = true; + timeline_items.pop_front(); + if let Some((i, _ev)) = found_target_event_id.as_mut() { + *i = i.saturating_sub(1); // account for the first item being removed. + } + // This doesn't affect whether we should reobtain the latest event. + } + VectorDiff::PopBack => { + timeline_items.pop_back(); + index_of_first_change = min(index_of_first_change, timeline_items.len()); + index_of_last_change = usize::MAX; + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopBack. Changes: {index_of_first_change}..{index_of_last_change}"); } + } + VectorDiff::Insert { index, value } => { + if index == 0 { + clear_cache = true; + } else { + index_of_first_change = min(index_of_first_change, index); + index_of_last_change = usize::MAX; + } + if index >= timeline_items.len() { + is_append = true; + } + + if let Some((i, _ev)) = found_target_event_id.as_mut() { + // account for this new `value` being inserted before the previously-found target event's index. + if index <= *i { + *i += 1; + } + } else { + found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) + .map(|(i, ev)| (i + index, ev)); + } + + timeline_items.insert(index, value); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Insert at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } + } + VectorDiff::Set { index, value } => { + index_of_first_change = min(index_of_first_change, index); + index_of_last_change = max(index_of_last_change, index.saturating_add(1)); + timeline_items.set(index, value); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Set at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } + } + VectorDiff::Remove { index } => { + if index == 0 { + clear_cache = true; + } else { + index_of_first_change = min(index_of_first_change, index.saturating_sub(1)); + index_of_last_change = usize::MAX; + } + if let Some((i, _ev)) = found_target_event_id.as_mut() { + // account for an item being removed before the previously-found target event's index. + if index <= *i { + *i = i.saturating_sub(1); + } + } + timeline_items.remove(index); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Remove at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } + } + VectorDiff::Truncate { length } => { + if length == 0 { + clear_cache = true; + } else { + index_of_first_change = min(index_of_first_change, length.saturating_sub(1)); + index_of_last_change = usize::MAX; + } + timeline_items.truncate(length); + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Truncate to length {length}. Changes: {index_of_first_change}..{index_of_last_change}"); } + } + VectorDiff::Reset { values } => { + if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Reset, new length {}", values.len()); } + clear_cache = true; // we must assume all items have changed. + timeline_items = values; + } + } + } + + + if num_updates > 0 { + // Handle the case where back pagination inserts items at the beginning of the timeline + // (meaning the entire timeline needs to be re-drawn), + // but there is a virtual event at index 0 (e.g., a day divider). + // When that happens, we want the RoomScreen to treat this as if *all* events changed. + if index_of_first_change == 1 && timeline_items.front().and_then(|item| item.as_virtual()).is_some() { + index_of_first_change = 0; + clear_cache = true; + } + + let changed_indices = index_of_first_change..index_of_last_change; + + if LOG_TIMELINE_DIFFS { + log!("timeline_subscriber: applied {num_updates} updates for room {room_id}, thread {thread_root_event_id:?}, timeline now has {} items. is_append? {is_append}, clear_cache? {clear_cache}. Changes: {changed_indices:?}.", timeline_items.len()); + } + timeline_update_sender.send(TimelineUpdate::NewItems { + new_items: timeline_items.clone(), + changed_indices, + clear_cache, + is_append, + }).expect("Error: timeline update sender couldn't send update with new items!"); + + // We must send this update *after* the actual NewItems update, + // otherwise the UI thread (RoomScreen) won't be able to correctly locate the target event. + if let Some((index, found_event_id)) = found_target_event_id.take() { + target_event_id = None; + timeline_update_sender.send( + TimelineUpdate::TargetEventFound { + target_event_id: found_event_id.clone(), + index, + } + ).unwrap_or_else( + |_e| panic!("Error: timeline update sender couldn't send TargetEventFound({found_event_id}, {index}) to room {room_id}, thread {thread_root_event_id:?}!") + ); + } + + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); + } + } + + else => { + break; + } + } } + + error!("Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}."); +} + +/// Spawn a new async task to fetch the room's new avatar. +fn spawn_fetch_room_avatar(room: &RoomListServiceRoomInfo) { + let room_id = room.room_id.clone(); + let room_name_id = RoomNameId::from((room.display_name.clone(), room.room_id.clone())); + let inner_room = room.room.clone(); + Handle::current().spawn(async move { + let room_avatar = room_avatar(&inner_room, &room_name_id).await; + rooms_list::enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomAvatar { + room_id, + room_avatar, + }); + }); +} + +/// Fetches and returns the avatar image for the given room (if one exists), +/// otherwise returns a text avatar string of the first character of the room name. +async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvatar { + match room.avatar(AVATAR_THUMBNAIL_FORMAT.into()).await { + Ok(Some(avatar)) => FetchedRoomAvatar::Image(avatar.into()), + _ => { + if let Ok(room_members) = room.members(RoomMemberships::ACTIVE).await { + if room_members.len() == 2 { + if let Some(non_account_member) = room_members.iter().find(|m| !m.is_account_user()) { + if let Ok(Some(avatar)) = non_account_member.avatar(AVATAR_THUMBNAIL_FORMAT.into()).await { + return FetchedRoomAvatar::Image(avatar.into()); + } + } + } + } + utils::avatar_from_room_name(room_name_id.name_for_avatar()) + } + } +} + +/// Spawn an async task to login to the given Matrix homeserver using the given SSO identity provider ID. +/// +/// This function will post a `LoginAction::SsoPending(true)` to the main thread, and another +/// `LoginAction::SsoPending(false)` once the async task has either successfully logged in or +/// failed to do so. +/// +/// If the login attempt is successful, the resulting `Client` and `ClientSession` will be sent +/// to the login screen using the `login_sender`. +async fn spawn_sso_server( + brand: String, + homeserver_url: String, + identity_provider_id: String, + login_sender: Sender, +) { + Cx::post_action(LoginAction::SsoPending(true)); + // Post a status update to inform the user that we're waiting for the client to be built. + Cx::post_action(LoginAction::Status { + title: "Initializing client...".into(), + status: "Please wait while Matrix builds and configures the client object for login.".into(), + }); + + // Wait for the notification that the client has been built + DEFAULT_SSO_CLIENT_NOTIFIER.notified().await; + + // Try to use the DEFAULT_SSO_CLIENT, if it was successfully built. + // We do not clone it because a Client cannot be re-used again + // once it has been used for a login attempt, so this forces us to create a new one + // if that occurs. + let client_and_session_opt = DEFAULT_SSO_CLIENT.lock().unwrap().take(); + + Handle::current().spawn(async move { + // Try to use the DEFAULT_SSO_CLIENT that we proactively created + // during initialization (to speed up opening the SSO browser window). + let mut client_and_session = client_and_session_opt; + + // If the DEFAULT_SSO_CLIENT is none (meaning it failed to build), + // or if the homeserver_url is *not* empty and isn't the default, + // we cannot use the DEFAULT_SSO_CLIENT, so we must build a new one. + let mut build_client_error = None; + if client_and_session.is_none() || ( + !homeserver_url.is_empty() + && homeserver_url != "matrix.org" + && Url::parse(&homeserver_url) != Url::parse("https://matrix-client.matrix.org/") + && Url::parse(&homeserver_url) != Url::parse("https://matrix.org/") + ) { + match build_client( + &Cli { + homeserver: homeserver_url.is_empty().not().then_some(homeserver_url), + ..Default::default() + }, + app_data_dir(), + ).await { + Ok(success) => client_and_session = Some(success), + Err(e) => build_client_error = Some(e), + } + } + + let Some((client, client_session)) = client_and_session else { + Cx::post_action(LoginAction::LoginFailure( + if let Some(err) = build_client_error { + format!("Could not create client object. Please try to login again.\n\nError: {err}") + } else { + String::from("Could not create client object. Please try to login again.") + } + )); + // This ensures that the called to `DEFAULT_SSO_CLIENT_NOTIFIER.notified()` + // at the top of this function will not block upon the next login attempt. + DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); + Cx::post_action(LoginAction::SsoPending(false)); + return; + }; + + let mut is_logged_in = false; + Cx::post_action(LoginAction::Status { + title: "Opening your browser...".into(), + status: "Please finish logging in using your browser, and then come back to Robrix.".into(), + }); + match client + .matrix_auth() + .login_sso(|sso_url: String| async move { + let url = Url::parse(&sso_url)?; + for (key, value) in url.query_pairs() { + if key == "redirectUrl" { + let redirect_url = Url::parse(&value)?; + Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); + break + } + } + Uri::new(&sso_url).open().map_err(|err| + Error::Io(io::Error::other(format!("Unable to open SSO login url. Error: {:?}", err))) + ) + }) + .identity_provider_id(&identity_provider_id) + .initial_device_display_name(&format!("robrix-sso-{brand}")) + .await + .inspect(|_| { + if let Some(client) = get_client() { + if client.matrix_auth().logged_in() { + is_logged_in = true; + log!("Already logged in, ignore login with sso"); + } + } + }) { + Ok(identity_provider_res) => { + if !is_logged_in { + if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session)).await { + error!("Error sending login request to login_sender: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(String::from( + "BUG: failed to send login request to matrix worker thread." + ))); + } + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: format!( + "Logged in as {:?}.\n → Loading rooms...", + &identity_provider_res.user_id + ), + }); + } + } + Err(e) => { + if !is_logged_in { + error!("SSO Login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(format!("SSO login failed: {e}"))); + } + } + } + + // This ensures that the called to `DEFAULT_SSO_CLIENT_NOTIFIER.notified()` + // at the top of this function will not block upon the next login attempt. + DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); + Cx::post_action(LoginAction::SsoPending(false)); + }); +} + + +bitflags! { + /// The powers that a user has in a given room. + #[derive(Copy, Clone, PartialEq, Eq)] + pub struct UserPowerLevels: u64 { + const Ban = 1 << 0; + const Invite = 1 << 1; + const Kick = 1 << 2; + const Redact = 1 << 3; + const NotifyRoom = 1 << 4; + // ------------------------------------- + // -- Copied from TimelineEventType ---- + // -- Unused powers are commented out -- + // ------------------------------------- + // const CallAnswer = 1 << 5; + // const CallInvite = 1 << 6; + // const CallHangup = 1 << 7; + // const CallCandidates = 1 << 8; + // const CallNegotiate = 1 << 9; + // const CallReject = 1 << 10; + // const CallSdpStreamMetadataChanged = 1 << 11; + // const CallSelectAnswer = 1 << 12; + // const KeyVerificationReady = 1 << 13; + // const KeyVerificationStart = 1 << 14; + // const KeyVerificationCancel = 1 << 15; + // const KeyVerificationAccept = 1 << 16; + // const KeyVerificationKey = 1 << 17; + // const KeyVerificationMac = 1 << 18; + // const KeyVerificationDone = 1 << 19; + const Location = 1 << 20; + const Message = 1 << 21; + // const PollStart = 1 << 22; + // const UnstablePollStart = 1 << 23; + // const PollResponse = 1 << 24; + // const UnstablePollResponse = 1 << 25; + // const PollEnd = 1 << 26; + // const UnstablePollEnd = 1 << 27; + // const Beacon = 1 << 28; + const Reaction = 1 << 29; + // const RoomEncrypted = 1 << 30; + const RoomMessage = 1 << 31; + const RoomRedaction = 1 << 32; + const Sticker = 1 << 33; + // const CallNotify = 1 << 34; + // const PolicyRuleRoom = 1 << 35; + // const PolicyRuleServer = 1 << 36; + // const PolicyRuleUser = 1 << 37; + // const RoomAliases = 1 << 38; + // const RoomAvatar = 1 << 39; + // const RoomCanonicalAlias = 1 << 40; + // const RoomCreate = 1 << 41; + // const RoomEncryption = 1 << 42; + // const RoomGuestAccess = 1 << 43; + // const RoomHistoryVisibility = 1 << 44; + // const RoomJoinRules = 1 << 45; + // const RoomMember = 1 << 46; + // const RoomName = 1 << 47; + const RoomPinnedEvents = 1 << 48; + // const RoomPowerLevels = 1 << 49; + // const RoomServerAcl = 1 << 50; + // const RoomThirdPartyInvite = 1 << 51; + // const RoomTombstone = 1 << 52; + // const RoomTopic = 1 << 53; + // const SpaceChild = 1 << 54; + // const SpaceParent = 1 << 55; + // const BeaconInfo = 1 << 56; + // const CallMember = 1 << 57; + // const MemberHints = 1 << 58; + } +} +impl UserPowerLevels { + pub fn from(power_levels: &RoomPowerLevels, user_id: &UserId) -> Self { + let mut retval = UserPowerLevels::empty(); + let user_power = power_levels.for_user(user_id); + retval.set(UserPowerLevels::Ban, user_power >= power_levels.ban); + retval.set(UserPowerLevels::Invite, user_power >= power_levels.invite); + retval.set(UserPowerLevels::Kick, user_power >= power_levels.kick); + retval.set(UserPowerLevels::Redact, user_power >= power_levels.redact); + retval.set(UserPowerLevels::NotifyRoom, user_power >= power_levels.notifications.room); + retval.set(UserPowerLevels::Location, user_power >= power_levels.for_message(MessageLikeEventType::Location)); + retval.set(UserPowerLevels::Message, user_power >= power_levels.for_message(MessageLikeEventType::Message)); + retval.set(UserPowerLevels::Reaction, user_power >= power_levels.for_message(MessageLikeEventType::Reaction)); + retval.set(UserPowerLevels::RoomMessage, user_power >= power_levels.for_message(MessageLikeEventType::RoomMessage)); + retval.set(UserPowerLevels::RoomRedaction, user_power >= power_levels.for_message(MessageLikeEventType::RoomRedaction)); + retval.set(UserPowerLevels::Sticker, user_power >= power_levels.for_message(MessageLikeEventType::Sticker)); + retval.set(UserPowerLevels::RoomPinnedEvents, user_power >= power_levels.for_state(StateEventType::RoomPinnedEvents)); + retval + } + + pub async fn from_room(room: &Room, user_id: &UserId) -> Option { + let room_power_levels = room.power_levels().await.ok()?; + Some(UserPowerLevels::from(&room_power_levels, user_id)) + } + + pub fn can_ban(self) -> bool { + self.contains(UserPowerLevels::Ban) + } + + pub fn can_unban(self) -> bool { + self.can_ban() && self.can_kick() + } + + pub fn can_invite(self) -> bool { + self.contains(UserPowerLevels::Invite) + } + + pub fn can_kick(self) -> bool { + self.contains(UserPowerLevels::Kick) + } + + pub fn can_redact(self) -> bool { + self.contains(UserPowerLevels::Redact) + } + + pub fn can_notify_room(self) -> bool { + self.contains(UserPowerLevels::NotifyRoom) + } + + pub fn can_redact_own(self) -> bool { + self.contains(UserPowerLevels::RoomRedaction) + } + + pub fn can_redact_others(self) -> bool { + self.can_redact_own() && self.contains(UserPowerLevels::Redact) + } + + pub fn can_send_location(self) -> bool { + self.contains(UserPowerLevels::Location) + } + + pub fn can_send_message(self) -> bool { + self.contains(UserPowerLevels::RoomMessage) + || self.contains(UserPowerLevels::Message) + } + + pub fn can_send_reaction(self) -> bool { + self.contains(UserPowerLevels::Reaction) + } + + pub fn can_send_sticker(self) -> bool { + self.contains(UserPowerLevels::Sticker) + } + + #[doc(alias("unpin"))] + pub fn can_pin(self) -> bool { + self.contains(UserPowerLevels::RoomPinnedEvents) + } +} + + +/// Shuts down the current Tokio runtime completely and takes ownership to ensure proper cleanup. +pub fn shutdown_background_tasks() { + if let Some(runtime) = TOKIO_RUNTIME.lock().unwrap().take() { + runtime.shutdown_background(); + } +} + +pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { + // Clear resources normally, allowing them to be properly dropped + // This prevents memory leaks when users logout and login again without closing the app + CLIENT.lock().unwrap().take(); + SYNC_SERVICE.lock().unwrap().take(); + REQUEST_SENDER.lock().unwrap().take(); + IGNORED_USERS.lock().unwrap().clear(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); + + let on_clear_appstate = Arc::new(Notify::new()); + Cx::post_action(LogoutAction::ClearAppState { on_clear_appstate: on_clear_appstate.clone() }); + + match tokio::time::timeout(config.app_state_cleanup_timeout, on_clear_appstate.notified()).await { + Ok(_) => { + log!("Received signal that UI-side app state was cleaned successfully"); + Ok(()) + } + Err(_) => Err(anyhow!("Timed out waiting for UI-side app state cleanup")), + } +} From e09e8d71e05f455438ff43705983621625939bdd Mon Sep 17 00:00:00 2001 From: Kevin Boos <1139460+kevinaboos@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:41:14 -0700 Subject: [PATCH 11/18] Delete src/sliding_sync.rs.orig --- src/sliding_sync.rs.orig | 4273 -------------------------------------- 1 file changed, 4273 deletions(-) delete mode 100644 src/sliding_sync.rs.orig diff --git a/src/sliding_sync.rs.orig b/src/sliding_sync.rs.orig deleted file mode 100644 index d280a212..00000000 --- a/src/sliding_sync.rs.orig +++ /dev/null @@ -1,4273 +0,0 @@ -use anyhow::{anyhow, bail, Result}; -use bitflags::bitflags; -use clap::Parser; -use eyeball::Subscriber; -use eyeball_im::VectorDiff; -use futures_util::{future::join_all, pin_mut, StreamExt}; -use imbl::Vector; -use makepad_widgets::{error, log, warning, Cx, SignalToUI}; -use matrix_sdk_base::crypto::{DecryptionSettings, TrustRequirement}; -use matrix_sdk::{ - config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ - api::{Direction, client::{profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType}}, events::{ - relation::RelationType, - room::{ - message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource - }, MessageLikeEventType, StateEventType - }, matrix_uri::MatrixId, EventId, MatrixToUri, MatrixUri, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomOrAliasId, UserId, uint - }, sliding_sync::VersionBuilder, Client, ClientBuildError, Error, OwnedServerName, Room, RoomDisplayName, RoomMemberships, RoomState, SessionChange, SuccessorRoom -}; -use matrix_sdk_ui::{ - RoomListService, Timeline, encryption_sync_service, room_list_service::{RoomListItem, RoomListLoadingState, SyncIndicator, filters}, sync_service::{self, SyncService}, timeline::{LatestEventValue, RoomExt, TimelineEventItemId, TimelineFocus, TimelineItem, TimelineReadReceiptTracking, TimelineDetails} -}; -use robius_open::Uri; -use ruma::{OwnedRoomOrAliasId, RoomId, events::tag::Tags}; -use tokio::{ - runtime::Handle, - sync::{broadcast, mpsc::{Sender, UnboundedReceiver, UnboundedSender}, watch, Notify}, task::JoinHandle, time::error::Elapsed, -}; -use url::Url; -use std::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefault, DefaultHasher}, iter::Peekable, ops::{Deref, DerefMut, Not}, path:: Path, sync::{Arc, LazyLock, Mutex}, time::Duration}; -use std::io; -use hashbrown::{HashMap, HashSet}; -use crate::{ - app::AppStateAction, app_data_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{ - add_room::KnockResultAction, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, tombstone_footer::SuccessorRoomDetails - }, login::login_screen::LoginAction, logout::{logout_confirm_modal::LogoutAction, logout_state_machine::{LogoutConfig, is_logout_in_progress, logout_with_state_machine}}, media_cache::{MediaCacheEntry, MediaCacheEntryRef}, persistence::{self, ClientSessionPersisted, load_app_state}, profile::{ - user_profile::UserProfile, - user_profile_cache::{UserProfileUpdate, enqueue_user_profile_update}, - }, room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{ - avatar::AvatarState, html_or_plaintext::MatrixLinkPillState, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification} - }, space_service_sync::space_service_loop, utils::{self, AVATAR_THUMBNAIL_FORMAT, RoomNameId, VecDiff, avatar_from_room_name}, verification::add_verification_event_handlers_and_sync_client -}; - -#[derive(Parser, Default)] -struct Cli { - /// The user ID to login with. - #[clap(value_parser)] - user_id: String, - - /// The password that should be used for the login. - #[clap(value_parser)] - password: String, - - /// The homeserver to connect to. - #[clap(value_parser)] - homeserver: Option, - - /// Set the proxy that should be used for the connection. - #[clap(short, long)] - proxy: Option, - - /// Force login screen. - #[clap(short, long, action)] - login_screen: bool, - - /// Enable verbose logging output. - #[clap(short, long, action)] - verbose: bool, -} - -impl std::fmt::Debug for Cli { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Cli") - .field("user_id", &self.user_id) - .field("password", &"") - .field("homeserver", &self.homeserver) - .field("proxy", &self.proxy) - .field("login_screen", &self.login_screen) - .field("verbose", &self.verbose) - .finish() - } -} - -impl From for Cli { - fn from(login: LoginByPassword) -> Self { - Self { - user_id: login.user_id, - password: login.password, - homeserver: login.homeserver, - proxy: None, - login_screen: false, - verbose: false, - } - } -} - - -/// Build a new client. -async fn build_client( - cli: &Cli, - data_dir: &Path, -) -> Result<(Client, ClientSessionPersisted), ClientBuildError> { - // Generate a unique subfolder name for the client database, - // which allows multiple clients to run simultaneously. - let now = chrono::Local::now(); - let db_subfolder_name: String = format!("db_{}", now.format("%F_%H_%M_%S_%f")); - let db_path = data_dir.join(db_subfolder_name); - - // Generate a random passphrase. - let passphrase: String = { - use rand::{Rng, thread_rng}; - thread_rng() - .sample_iter(rand::distributions::Alphanumeric) - .take(32) - .map(char::from) - .collect() - }; - - let homeserver_url = cli.homeserver.as_deref() - .unwrap_or("https://matrix-client.matrix.org/"); - // .unwrap_or("https://matrix.org/"); - - let mut builder = Client::builder() - .server_name_or_homeserver_url(homeserver_url) - // Use a sqlite database to persist the client's encryption setup. - .sqlite_store(&db_path, Some(&passphrase)) - .with_threading_support(matrix_sdk::ThreadingSupport::Enabled { - with_subscriptions: true, - }) - // The sliding sync proxy has now been deprecated in favor of native sliding sync. - .sliding_sync_version_builder(VersionBuilder::DiscoverNative) - .with_decryption_settings(DecryptionSettings { - sender_device_trust_requirement: TrustRequirement::Untrusted, - }) - .with_encryption_settings(EncryptionSettings { - auto_enable_cross_signing: true, - backup_download_strategy: matrix_sdk::encryption::BackupDownloadStrategy::OneShot, - auto_enable_backups: true, - }) - .with_enable_share_history_on_invite(true) - .handle_refresh_tokens(); - - if let Some(proxy) = cli.proxy.as_ref() { - builder = builder.proxy(proxy.clone()); - } - - // Use a 60 second timeout for all requests to the homeserver. - // Yes, this is a long timeout, but the standard matrix homeserver is often very slow. - builder = builder.request_config( - RequestConfig::new() - .timeout(std::time::Duration::from_secs(60)) - ); - - let client = builder.build().await?; - let homeserver_url = client.homeserver().to_string(); - Ok(( - client, - ClientSessionPersisted { - homeserver: homeserver_url, - db_path, - passphrase, - }, - )) -} - -/// Logs in to the given Matrix homeserver using the given username and password. -/// -/// This function is used by the login screen to log in to the Matrix server. -/// -/// Upon success, this function returns the logged-in client and an optional sync token. -async fn login( - cli: &Cli, - login_request: LoginRequest, -) -> Result<(Client, Option)> { - match login_request { - LoginRequest::LoginByCli | LoginRequest::LoginByPassword(_) => { - let cli = if let LoginRequest::LoginByPassword(login_by_password) = login_request { - &Cli::from(login_by_password) - } else { - cli - }; - let (client, client_session) = build_client(cli, app_data_dir()).await?; - Cx::post_action(LoginAction::Status { - title: "Authenticating".into(), - status: format!("Logging in as {}...", cli.user_id), - }); - // Attempt to login using the CLI-provided username & password. - let login_result = client - .matrix_auth() - .login_username(&cli.user_id, &cli.password) - .initial_device_display_name("robrix-un-pw") - .send() - .await?; - if client.matrix_auth().logged_in() { - log!("Logged in successfully."); - let status = format!("Logged in as {}.\n → Loading rooms...", cli.user_id); - // enqueue_popup_notification(status.clone()); - enqueue_rooms_list_update(RoomsListUpdate::Status { status }); - if let Err(e) = persistence::save_session(&client, client_session).await { - let err_msg = format!("Failed to save session state to storage: {e}"); - error!("{err_msg}"); - enqueue_popup_notification(err_msg, PopupKind::Error, None); - } - Ok((client, None)) - } else { - let err_msg = format!("Failed to login as {}: {:?}", cli.user_id, login_result); - enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); - bail!(err_msg); - } - } - - LoginRequest::LoginBySSOSuccess(client, client_session) => { - if let Err(e) = persistence::save_session(&client, client_session).await { - error!("Failed to save session state to storage: {e:?}"); - } - Ok((client, None)) - } - LoginRequest::HomeserverLoginTypesQuery(_) => { - bail!("LoginRequest::HomeserverLoginTypesQuery not handled earlier"); - } - } -} - - -/// Which direction to paginate in. -/// -/// * `Forwards` will retrieve later events (towards the end of the timeline), -/// which only works if the timeline is *focused* on a specific event. -/// * `Backwards`: the more typical choice, in which earlier events are retrieved -/// (towards the start of the timeline), which works in both live mode and focused mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PaginationDirection { - Forwards, - Backwards, -} -impl std::fmt::Display for PaginationDirection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Forwards => write!(f, "forwards"), - Self::Backwards => write!(f, "backwards"), - } - } -} - -/// The function signature for the callback that gets invoked when media is fetched. -pub type OnMediaFetchedFn = fn( - &Mutex, - MediaRequestParameters, - matrix_sdk::Result>, - Option>, -); - -/// Error types for URL preview operations. -#[derive(Debug)] -pub enum UrlPreviewError { - /// HTTP request failed. - Request(reqwest::Error), - /// JSON parsing failed. - Json(serde_json::Error), - /// Client not available. - ClientNotAvailable, - /// Access token not available. - AccessTokenNotAvailable, - /// HTTP error status. - HttpStatus(u16), - /// URL parsing error. - UrlParse(url::ParseError), -} - -impl std::fmt::Display for UrlPreviewError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UrlPreviewError::Request(e) => write!(f, "HTTP request failed: {}", e), - UrlPreviewError::Json(e) => write!(f, "JSON parsing failed: {}", e), - UrlPreviewError::ClientNotAvailable => write!(f, "Matrix client not available"), - UrlPreviewError::AccessTokenNotAvailable => write!(f, "Access token not available"), - UrlPreviewError::HttpStatus(status) => write!(f, "HTTP {} error", status), - UrlPreviewError::UrlParse(e) => write!(f, "URL parsing failed: {}", e), - } - } -} - -impl std::error::Error for UrlPreviewError {} - -/// The function signature for the callback that gets invoked when link preview data is fetched. -pub type OnLinkPreviewFetchedFn = fn( - String, - Arc>, - Result, - Option>, -); - - -/// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. -#[derive(Clone, Debug)] -pub enum MatrixLinkAction { - MatrixToUri(MatrixToUri), - MatrixUri(MatrixUri), - Error(String), -} - -/// Actions emitted when account data (e.g., avatar, display name) changes. -#[derive(Clone, Debug)] -pub enum AccountDataAction { - /// The user's avatar was successfully updated or removed. - AvatarChanged(Option), - /// Failed to update or remove the user's avatar. - AvatarChangeFailed(String), - /// The user's display name was successfully updated or removed. - DisplayNameChanged(Option), - /// Failed to update the user's display name. - DisplayNameChangeFailed(String), -} - -/// Actions emitted in response to a [`MatrixRequest::OpenOrCreateDirectMessage`]. -#[derive(Debug)] -pub enum DirectMessageRoomAction { - /// A direct message room already existed with the given user. - FoundExisting { - user_id: OwnedUserId, - room_name_id: RoomNameId, - }, - /// A direct message room didn't exist, and we didn't attempt to create a new one. - DidNotExist { - user_profile: UserProfile, - }, - /// A direct message room didn't exist, but we successfully created a new one. - NewlyCreated { - user_profile: UserProfile, - room_name_id: RoomNameId, - }, - /// A direct message room didn't exist, and we failed to create a new one. - FailedToCreate { - user_profile: UserProfile, - error: matrix_sdk::Error, - }, -} - -/// Either a main room timeline or a thread-focused timeline. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum TimelineKind { - MainRoom { - room_id: OwnedRoomId, - }, - Thread { - room_id: OwnedRoomId, - thread_root_event_id: OwnedEventId, - }, -} -impl TimelineKind { - pub fn room_id(&self) -> &OwnedRoomId { - match self { - TimelineKind::MainRoom { room_id } => room_id, - TimelineKind::Thread { room_id, .. } => room_id, - } - } - - pub fn thread_root_event_id(&self) -> Option<&OwnedEventId> { - match self { - TimelineKind::MainRoom { .. } => None, - TimelineKind::Thread { thread_root_event_id, .. } => Some(thread_root_event_id), - } - } -} -impl std::fmt::Display for TimelineKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TimelineKind::MainRoom { room_id } => write!(f, "MainRoom({})", room_id), - TimelineKind::Thread { room_id, thread_root_event_id } => { - write!(f, "Thread({}, {})", room_id, thread_root_event_id) - } - } - } -} - -/// The set of requests for async work that can be made to the worker thread. -#[allow(clippy::large_enum_variant)] -pub enum MatrixRequest { - /// Request from the login screen to log in with the given credentials. - Login(LoginRequest), - /// Request to logout. - Logout { - is_desktop: bool, - }, - /// Request to paginate the older (or newer) events of a room or thread timeline. - PaginateTimeline { - timeline_kind: TimelineKind, - /// The maximum number of timeline events to fetch in each pagination batch. - num_events: u16, - direction: PaginationDirection, - }, - /// Request to edit the content of an event in the given room's timeline. - EditMessage { - timeline_kind: TimelineKind, - timeline_event_item_id: TimelineEventItemId, - edited_content: EditedContent, - }, - /// Request to fetch the full details of the given event in the given room's timeline. - FetchDetailsForEvent { - timeline_kind: TimelineKind, - event_id: OwnedEventId, - }, - /// Request to fetch the latest thread-reply preview and latest reply count - /// for the given thread root. - FetchThreadSummaryDetails { - timeline_kind: TimelineKind, - thread_root_event_id: OwnedEventId, - timeline_item_index: usize, - }, - /// Request to fetch profile information for all members of a room. - /// - /// This can be *very* slow depending on the number of members in the room. - /// - /// Even though it operates on a room itself, this accepts a `TimelineKind` - /// in order to be able to send the fetched room member list to a specific timeline UI. - SyncRoomMemberList { - timeline_kind: TimelineKind, - }, - /// Request to create a thread timeline focused on the given thread root event in the given room. - CreateThreadTimeline { - room_id: OwnedRoomId, - thread_root_event_id: OwnedEventId, - }, - /// Request to knock on (request an invite to) the given room. - Knock { - room_or_alias_id: OwnedRoomOrAliasId, - reason: Option, - #[doc(alias("via"))] - server_names: Vec, - }, - /// Request to invite the given user to the given room. - InviteUser { - room_id: OwnedRoomId, - user_id: OwnedUserId, - }, - /// Request to join the given room. - JoinRoom { - room_id: OwnedRoomId, - }, - /// Request to leave the given room. - LeaveRoom { - room_id: OwnedRoomId, - }, - /// Request to get the actual list of members in a room. - /// - /// This returns the list of members that can be displayed in the UI. - /// - /// Even though it operates on a room itself, this accepts a `TimelineKind` - /// in order to be able to send the fetched room member list to a specific timeline UI. - GetRoomMembers { - timeline_kind: TimelineKind, - memberships: RoomMemberships, - /// * If `true` (not recommended), only the local cache will be accessed. - /// * If `false` (recommended), details will be fetched from the server. - local_only: bool, - }, - /// Request to fetch the preview (basic info) for the given room, - /// either one that is joined locally or one that is unknown. - /// - /// Emits a [`RoomPreviewAction::Fetched`] when the fetch operation has completed. - GetRoomPreview { - room_or_alias_id: OwnedRoomOrAliasId, - via: Vec, - }, - /// Request to fetch the full details (the room preview) of a tombstoned room. - GetSuccessorRoomDetails { - tombstoned_room_id: OwnedRoomId, - }, - /// Request to create or open a direct message room with the given user. - /// - /// If there is no existing DM room with the given user, this will create a new DM room - /// if `allow_create` is `true`; otherwise it will emit an action indicating that - /// no DM room existed, upon which the UI will prompt the user to confirm that they want - /// to proceed with creating a new DM room. - #[doc(alias("dm"))] - OpenOrCreateDirectMessage { - user_profile: UserProfile, - allow_create: bool, - }, - /// Request to fetch profile information for the given user ID. - GetUserProfile { - user_id: OwnedUserId, - /// * If `Some`, the user is known to be a member of a room, so this will - /// fetch the user's profile from that room's membership info. - /// * If `None`, the user's profile info will be fetched from the server - /// in a room-agnostic manner, and no room membership info will be returned. - room_id: Option, - /// * If `true` (not recommended), only the local cache will be accessed. - /// * If `false` (recommended), details will be fetched from the server. - local_only: bool, - }, - /// Request to fetch the number of unread messages in the given room. - GetNumberUnreadMessages { - timeline_kind: TimelineKind, - }, - /// Request to set the unread flag for the given room. - SetUnreadFlag { - room_id: OwnedRoomId, - /// If `true`, marks the room as unread. - /// If `false`, marks the room as read. - mark_as_unread: bool, - }, - /// Request to set the favorite flag for the given room. - SetIsFavorite { - room_id: OwnedRoomId, - is_favorite: bool, - }, - /// Request to set the low priority flag for the given room. - SetIsLowPriority { - room_id: OwnedRoomId, - is_low_priority: bool, - }, - /// Request to generate a Matrix link (permalink) for a room or event. - GenerateMatrixLink { - /// The ID of the room to generate a link for. - room_id: OwnedRoomId, - /// * If `Some`, the link will point to this specific event within the room. - /// * If `None`, the link will point to the room itself. - event_id: Option, - /// * If `true`, the `matrix:` URI scheme will be used to create a [`MatrixUri`]. - /// * If `false` (default), the `https://matrix.to` scheme will be used to create a [`MatrixToUri`]. - use_matrix_scheme: bool, - /// * If `true` (default is false), the link will include an action hint to join the room. - join_on_click: bool, - }, - /// Request to ignore/block or unignore/unblock a user. - IgnoreUser { - /// Whether to ignore (`true`) or unignore (`false`) the user. - ignore: bool, - /// The room membership info of the user to (un)ignore. - room_member: RoomMember, - /// The room ID of the room where the user is a member, - /// which is only needed because it isn't present in the `RoomMember` object. - room_id: OwnedRoomId, - }, - /// Request to set or remove the avatar of the current user's account. - SetAvatar { - /// * If `Some`, the avatar will be set to the given MXC URI. - /// * If `None`, the avatar will be removed. - avatar_url: Option, - }, - /// Request to set or remove the display name of the current user's account. - SetDisplayName { - /// * If `Some`, the display name will be set to the given value. - /// * If `None`, the display name will be removed. - new_display_name: Option, - }, - /// Request to resolve a room alias into a room ID and the servers that know about that room. - ResolveRoomAlias(OwnedRoomAliasId), - /// Request to fetch an Avatar image from the server. - /// Upon completion of the async media request, the `on_fetched` function - /// will be invoked with the content of an `AvatarUpdate`. - FetchAvatar { - mxc_uri: OwnedMxcUri, - on_fetched: fn(AvatarUpdate), - }, - /// Request to fetch media from the server. - /// Upon completion of the async media request, the `on_fetched` function - /// will be invoked with four arguments: the `destination`, the `media_request`, - /// the result of the media fetch, and the `update_sender`. - FetchMedia { - media_request: MediaRequestParameters, - on_fetched: OnMediaFetchedFn, - destination: MediaCacheEntryRef, - update_sender: Option>, - }, - /// Request to send a message to the given room. - SendMessage { - timeline_kind: TimelineKind, - message: RoomMessageEventContent, - replied_to: Option, - #[cfg(feature = "tsp")] - sign_with_tsp: bool, - }, - /// Sends a notice to the given room that the current user is or is not typing. - /// - /// This request does not return a response or notify the UI thread, and - /// furthermore, there is no need to send a follow-up request to stop typing - /// (though you certainly can do so). - SendTypingNotice { - room_id: OwnedRoomId, - typing: bool, - }, - /// Spawn an async task to login to the given Matrix homeserver using the given SSO identity provider ID. - /// - /// While an SSO request is in flight, the login screen will temporarily prevent the user - /// from submitting another redundant request, until this request has succeeded or failed. - SpawnSSOServer{ - brand: String, - homeserver_url: String, - identity_provider_id: String, - }, - /// Subscribe to typing notices for the given room. - /// - /// This is only valid for the main room timeline, not for thread-focused timelines. - /// - /// This request does not immediately return a response or notify the UI thread, - /// but it will send updates to the UI via the timeline's update sender. - SubscribeToTypingNotices { - room_id: OwnedRoomId, - /// Whether to subscribe or unsubscribe. - subscribe: bool, - }, - /// Subscribe to changes in the read receipts of our own user. - /// - /// This request does not immediately return a response or notify the UI thread, - /// but it will send updates to the UI via the timeline's update sender. - SubscribeToOwnUserReadReceiptsChanged { - timeline_kind: TimelineKind, - /// Whether to subscribe or unsubscribe. - subscribe: bool, - }, - /// Subscribe to changes in the set of pinned events for the given room. - /// - /// This is only valid for the main room timeline, not for thread-focused timelines. - SubscribeToPinnedEvents { - room_id: OwnedRoomId, - /// Whether to subscribe or unsubscribe. - subscribe: bool, - }, - /// Sends a read receipt for the given event to the given room or thread timeline. - ReadReceipt { - timeline_kind: TimelineKind, - event_id: OwnedEventId, - receipt_type: ReceiptType, - }, - /// Sends a request to obtain the power levels for this room. - /// - /// The response is delivered back to the main UI thread via [`TimelineUpdate::UserPowerLevels`]. - /// - /// Even though it operates on a room itself, this accepts a `TimelineKind` - /// in order to be able to send the fetched room member list to a specific timeline UI. - GetRoomPowerLevels { - timeline_kind: TimelineKind, - }, - /// Toggles the given reaction to the given event in the given room. - ToggleReaction { - timeline_kind: TimelineKind, - timeline_event_id: TimelineEventItemId, - reaction: String, - }, - /// Redacts (deletes) the given event in the given room. - #[doc(alias("delete"))] - RedactMessage { - timeline_kind: TimelineKind, - timeline_event_id: TimelineEventItemId, - reason: Option, - }, - /// Pin or unpin the given event in the given room. - #[doc(alias("unpin"))] - PinEvent { - timeline_kind: TimelineKind, - event_id: OwnedEventId, - pin: bool, - }, - /// Sends a request to obtain the room's pill link info for the given Matrix ID. - /// - /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. - GetMatrixRoomLinkPillInfo { - matrix_id: MatrixId, - via: Vec - }, - /// Request to fetch URL preview from the Matrix homeserver. - GetUrlPreview { - url: String, - on_fetched: OnLinkPreviewFetchedFn, - destination: Arc>, - update_sender: Option>, - }, -} - -/// Submits a request to the worker thread to be executed asynchronously. -pub fn submit_async_request(req: MatrixRequest) { - if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() { - sender.send(req) - .expect("BUG: matrix worker task receiver has died!"); - } -} - -/// Details of a login request that get submitted within [`MatrixRequest::Login`]. -pub enum LoginRequest{ - LoginByPassword(LoginByPassword), - LoginBySSOSuccess(Client, ClientSessionPersisted), - LoginByCli, - HomeserverLoginTypesQuery(String), - -} -/// Information needed to log in to a Matrix homeserver. -pub struct LoginByPassword { - pub user_id: String, - pub password: String, - pub homeserver: Option, -} - - -/// The entry point for the worker task that runs Matrix-related operations. -/// -/// All this task does is wait for [`MatrixRequests`] from the main UI thread -/// and then executes them within an async runtime context. -async fn matrix_worker_task( - mut request_receiver: UnboundedReceiver, - login_sender: Sender, -) -> Result<()> { - log!("Started matrix_worker_task."); - // The async tasks that are spawned to subscribe to changes in our own user's read receipts for each timeline. - let mut subscribers_own_user_read_receipts: HashMap> = HashMap::new(); - // The async tasks that are spawned to subscribe to changes in the pinned events for each room. - let mut subscribers_pinned_events: HashMap> = HashMap::new(); - - while let Some(request) = request_receiver.recv().await { - match request { - MatrixRequest::Login(login_request) => { - if let Err(e) = login_sender.send(login_request).await { - error!("Error sending login request to login_sender: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(String::from( - "BUG: failed to send login request to login worker task." - ))); - } - } - - MatrixRequest::Logout { is_desktop } => { - log!("Received MatrixRequest::Logout, is_desktop: {}", is_desktop); - let _logout_task = Handle::current().spawn(async move { - log!("Starting logout task"); - // Use the state machine implementation - match logout_with_state_machine(is_desktop).await { - Ok(()) => { - log!("Logout completed successfully via state machine"); - }, - Err(e) => { - error!("Logout failed: {e:?}"); - } - } - }); - } - - MatrixRequest::PaginateTimeline {timeline_kind, num_events, direction} => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("Skipping pagination request for unknown {timeline_kind}"); - continue; - }; - - // Spawn a new async task that will make the actual pagination request. - let _paginate_task = Handle::current().spawn(async move { - log!("Starting {direction} pagination request for {timeline_kind}..."); - sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap(); - SignalToUI::set_ui_signal(); - - let res = if direction == PaginationDirection::Forwards { - timeline.paginate_forwards(num_events).await - } else { - timeline.paginate_backwards(num_events).await - }; - - match res { - Ok(fully_paginated) => { - log!("Completed {direction} pagination request for {timeline_kind}, hit {} of timeline? {}", - if direction == PaginationDirection::Forwards { "end" } else { "start" }, - if fully_paginated { "yes" } else { "no" }, - ); - sender.send(TimelineUpdate::PaginationIdle { - fully_paginated, - direction, - }).unwrap(); - SignalToUI::set_ui_signal(); - } - Err(error) => { - error!("Error sending {direction} pagination request for {timeline_kind}: {error:?}"); - sender.send(TimelineUpdate::PaginationError { - error, - direction, - }).unwrap(); - SignalToUI::set_ui_signal(); - } - } - }); - } - - MatrixRequest::EditMessage { timeline_kind, timeline_event_item_id, edited_content } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for edit request"); - continue; - }; - - // Spawn a new async task that will make the actual edit request. - let _edit_task = Handle::current().spawn(async move { - log!("Sending request to edit message {timeline_event_item_id:?} in {timeline_kind}..."); - let result = timeline.edit(&timeline_event_item_id, edited_content).await; - match result { - Ok(_) => log!("Successfully edited message {timeline_event_item_id:?} in {timeline_kind}."), - Err(ref e) => error!("Error editing message {timeline_event_item_id:?} in {timeline_kind}: {e:?}"), - } - sender.send(TimelineUpdate::MessageEdited { - timeline_event_item_id, - result, - }).unwrap(); - SignalToUI::set_ui_signal(); - }); - } - - MatrixRequest::FetchDetailsForEvent { timeline_kind, event_id } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for fetch details for event request"); - continue; - }; - - let _fetch_task = Handle::current().spawn(async move { - // log!("Sending request to fetch details for event {event_id} in {timeline_kind}..."); - let result = timeline.fetch_details_for_event(&event_id).await; - match &result { - Ok(_) => { - // log!("Successfully fetched details for event {event_id} in {timeline_kind}."); - } - Err(_e) => { - // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); - } - } - if sender.send(TimelineUpdate::EventDetailsFetched { event_id, result }).is_err() { - error!("Failed to send fetched event details to UI for {timeline_kind}"); - } - SignalToUI::set_ui_signal(); - }); - } - - MatrixRequest::FetchThreadSummaryDetails { - timeline_kind, - thread_root_event_id, - timeline_item_index, - } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for fetch thread summary details request"); - continue; - }; - - let _fetch_task = Handle::current().spawn(async move { - let (num_replies, latest_reply_event) = fetch_thread_summary_details( - timeline.room(), - &thread_root_event_id, - ).await; - let latest_reply_preview_text = match latest_reply_event.as_ref() { - Some(event) => text_preview_of_latest_thread_reply(timeline.room(), event).await, - None => None, - }; - - if sender.send(TimelineUpdate::ThreadSummaryDetailsFetched { - thread_root_event_id, - timeline_item_index, - num_replies, - latest_reply_preview_text, - }).is_err() { - error!("Failed to send fetched thread summary details to UI for {timeline_kind}"); - } - SignalToUI::set_ui_signal(); - }); - } - - MatrixRequest::SyncRoomMemberList { timeline_kind } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for sync members list request"); - continue; - }; - - let _fetch_task = Handle::current().spawn(async move { - log!("Sending sync room members request for {timeline_kind}..."); - timeline.fetch_members().await; - log!("Completed sync room members request for {timeline_kind}."); - sender.send(TimelineUpdate::RoomMembersSynced).unwrap(); - SignalToUI::set_ui_signal(); - }); - } - - MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { - let main_room_timeline = { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { - error!("BUG: room info not found for create thread timeline request, room {room_id}"); - continue; - }; - if room_info.thread_timelines.contains_key(&thread_root_event_id) { - continue; - } - let newly_pending = room_info.pending_thread_timelines.insert(thread_root_event_id.clone()); - if !newly_pending { - continue; - } - room_info.main_timeline.timeline.clone() - }; - - let _create_thread_timeline_task = Handle::current().spawn(async move { - log!("Creating thread-focused timeline for room {room_id}, thread {thread_root_event_id}..."); - let build_result = main_room_timeline.room() - .timeline_builder() - .with_focus(TimelineFocus::Thread { - root_event_id: thread_root_event_id.clone(), - }) - .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) - .build() - .await; - - match build_result { - Ok(thread_timeline) => { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { - return; - }; - log!("Successfully created thread-focused timeline for room {room_id}, thread {thread_root_event_id}."); - let thread_timeline = Arc::new(thread_timeline); - let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); - let (request_sender, request_receiver) = watch::channel(Vec::new()); - let timeline_subscriber_handler_task = Handle::current().spawn( - timeline_subscriber_handler( - main_room_timeline.room().clone(), - thread_timeline.clone(), - timeline_update_sender.clone(), - request_receiver, - Some(thread_root_event_id.clone()), - ) - ); - room_info - .pending_thread_timelines - .remove(&thread_root_event_id); - room_info.thread_timelines.insert( - thread_root_event_id.clone(), - PerTimelineDetails { - timeline: thread_timeline, - timeline_update_sender, - timeline_singleton_endpoints: Some(( - timeline_update_receiver, - request_sender, - )), - timeline_subscriber_handler_task, - }, - ); - SignalToUI::set_ui_signal(); - } - Err(error) => { - error!("Failed to create thread-focused timeline for room {room_id}, thread {thread_root_event_id}: {error}"); - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - if let Some(room_info) = all_joined_rooms.get_mut(&room_id) { - room_info - .pending_thread_timelines - .remove(&thread_root_event_id); - } - enqueue_popup_notification( - format!("Failed to create thread-focused timeline. Please retry opening the thread again later.\n\nError: {error}"), - PopupKind::Error, - None, - ); - } - } - }); - } - - MatrixRequest::Knock { room_or_alias_id, reason, server_names } => { - let Some(client) = get_client() else { continue }; - let _knock_room_task = Handle::current().spawn(async move { - log!("Sending request to knock on room {room_or_alias_id}..."); - match client.knock(room_or_alias_id.clone(), reason, server_names).await { - Ok(room) => { - let _ = room.display_name().await; // populate this room's display name cache - Cx::post_action(KnockResultAction::Knocked { - room_or_alias_id, - room, - }); - } - Err(error) => Cx::post_action(KnockResultAction::Failed { - room_or_alias_id, - error, - }), - } - }); - } - - MatrixRequest::InviteUser { room_id, user_id } => { - let Some(client) = get_client() else { continue }; - let _invite_task = Handle::current().spawn(async move { - // We use `client.get_room()` here because the room might also be a space, - // not just a joined room. - if let Some(room) = client.get_room(&room_id) { - log!("Sending request to invite user {user_id} to room {room_id}..."); - match room.invite_user_by_id(&user_id).await { - Ok(_) => Cx::post_action(InviteResultAction::Sent { - room_id, - user_id, - }), - Err(error) => Cx::post_action(InviteResultAction::Failed { - room_id, - user_id, - error, - }), - } - } - else { - error!("Room/Space not found for invite user request {room_id}, {user_id}"); - Cx::post_action(InviteResultAction::Failed { - room_id, - user_id, - error: matrix_sdk::Error::UnknownError("Room/Space not found in client's known list.".into()), - }) - } - }); - } - - MatrixRequest::JoinRoom { room_id } => { - let Some(client) = get_client() else { continue }; - let _join_room_task = Handle::current().spawn(async move { - log!("Sending request to join room {room_id}..."); - let result_action = if let Some(room) = client.get_room(&room_id) { - match room.join().await { - Ok(()) => { - log!("Successfully joined known room {room_id}."); - JoinRoomResultAction::Joined { room_id } - } - Err(e) => { - error!("Error joining known room {room_id}: {e:?}"); - JoinRoomResultAction::Failed { room_id, error: e } - } - } - } - else { - match client.join_room_by_id(&room_id).await { - Ok(_room) => { - log!("Successfully joined new unknown room {room_id}."); - JoinRoomResultAction::Joined { room_id } - } - Err(e) => { - error!("Error joining new unknown room {room_id}: {e:?}"); - JoinRoomResultAction::Failed { room_id, error: e } - } - } - }; - Cx::post_action(result_action); - }); - } - - MatrixRequest::LeaveRoom { room_id } => { - let Some(client) = get_client() else { continue }; - let _leave_room_task = Handle::current().spawn(async move { - log!("Sending request to leave room {room_id}..."); - let result_action = if let Some(room) = client.get_room(&room_id) { - match room.leave().await { - Ok(()) => { - log!("Successfully left room {room_id}."); - LeaveRoomResultAction::Left { room_id } - } - Err(e) => { - error!("Error leaving room {room_id}: {e:?}"); - LeaveRoomResultAction::Failed { room_id, error: e } - } - } - } else { - error!("BUG: client could not get room with ID {room_id}"); - LeaveRoomResultAction::Failed { - room_id, - error: matrix_sdk::Error::UnknownError("Client couldn't locate room to leave it.".into()), - } - }; - Cx::post_action(result_action); - }); - } - - MatrixRequest::GetRoomMembers { timeline_kind, memberships, local_only } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for get room members request"); - continue; - }; - - let _get_members_task = Handle::current().spawn(async move { - let send_update = |members: Vec, source: &str| { - log!("{} {} members for {timeline_kind}", source, members.len()); - sender.send(TimelineUpdate::RoomMembersListFetched { members }).unwrap(); - SignalToUI::set_ui_signal(); - }; - - let room = timeline.room(); - if local_only { - if let Ok(members) = room.members_no_sync(memberships).await { - send_update(members, "Got"); - } - } else { - if let Ok(members) = room.members(memberships).await { - send_update(members, "Successfully fetched"); - } - } - }); - } - - MatrixRequest::GetRoomPreview { room_or_alias_id, via } => { - let Some(client) = get_client() else { continue }; - let _fetch_task = Handle::current().spawn(async move { - let res = fetch_room_preview_with_avatar(&client, &room_or_alias_id, via).await; - Cx::post_action(RoomPreviewAction::Fetched(res)); - }); - } - - MatrixRequest::GetSuccessorRoomDetails { tombstoned_room_id } => { - let Some(client) = get_client() else { continue }; - let (sender, successor_room) = { - let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - let Some(room_info) = all_joined_rooms.get(&tombstoned_room_id) else { - error!("BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request"); - continue; - }; - ( - room_info.main_timeline.timeline_update_sender.clone(), - room_info.main_timeline.timeline.room().successor_room(), - ) - }; - spawn_fetch_successor_room_preview( - client, - successor_room, - tombstoned_room_id, - sender, - ); - } - - MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create } => { - let Some(client) = get_client() else { continue }; - let _create_dm_task = Handle::current().spawn(async move { - if let Some(room) = client.get_dm_room(&user_profile.user_id) { - log!("Found existing DM room: {}", room.room_id()); - Cx::post_action(DirectMessageRoomAction::FoundExisting { - user_id: user_profile.user_id, - room_name_id: RoomNameId::from_room(&room).await, - }); - return; - } - if !allow_create { - Cx::post_action(DirectMessageRoomAction::DidNotExist { user_profile }); - return; - } - log!("Creating new DM room with {user_profile:?}..."); - match client.create_dm(&user_profile.user_id).await { - Ok(room) => { - log!("Successfully created DM room: {}", room.room_id()); - Cx::post_action(DirectMessageRoomAction::NewlyCreated { - user_profile, - room_name_id: RoomNameId::from_room(&room).await, - }); - }, - Err(error) => { - error!("Failed to create DM with {user_profile:?}: {error}"); - Cx::post_action(DirectMessageRoomAction::FailedToCreate { - user_profile, - error, - }); - } - } - }); - } - - MatrixRequest::GetUserProfile { user_id, room_id, local_only } => { - let Some(client) = get_client() else { continue }; - let _fetch_task = Handle::current().spawn(async move { - // log!("Sending get user profile request: user: {user_id}, \ - // room: {room_id:?}, local_only: {local_only}...", - // ); - - let mut update = None; - - if let Some(room_id) = room_id.as_ref() { - if let Some(room) = client.get_room(room_id) { - let member = if local_only { - room.get_member_no_sync(&user_id).await - } else { - room.get_member(&user_id).await - }; - if let Ok(Some(room_member)) = member { - update = Some(UserProfileUpdate::Full { - new_profile: UserProfile { - username: room_member.display_name().map(|u| u.to_owned()), - user_id: user_id.clone(), - avatar_state: AvatarState::Known(room_member.avatar_url().map(|u| u.to_owned())), - }, - room_id: room_id.to_owned(), - room_member, - }); - } else { - log!("User profile request: user {user_id} was not a member of room {room_id}"); - } - } else { - log!("User profile request: client could not get room with ID {room_id}"); - } - } - - if !local_only { - if update.is_none() { - if let Ok(response) = client.account().fetch_user_profile_of(&user_id).await { - update = Some(UserProfileUpdate::UserProfileOnly( - UserProfile { - username: response.get_static::().ok().flatten(), - user_id: user_id.clone(), - avatar_state: response.get_static::() - .ok() - .map_or(AvatarState::Unknown, AvatarState::Known), - } - )); - } else { - log!("User profile request: client could not get user with ID {user_id}"); - } - } - - match update.as_mut() { - Some(UserProfileUpdate::Full { new_profile: UserProfile { username, .. }, .. }) if username.is_none() => { - if let Ok(response) = client.account().fetch_user_profile_of(&user_id).await { - *username = response.get_static::().ok().flatten(); - } - } - _ => { } - } - } - - if let Some(upd) = update { - // log!("Successfully completed get user profile request: user: {user_id}, room: {room_id:?}, local_only: {local_only}."); - enqueue_user_profile_update(upd); - } else { - log!("Failed to get user profile: user: {user_id}, room: {room_id:?}, local_only: {local_only}."); - } - }); - } - - MatrixRequest::GetNumberUnreadMessages { timeline_kind } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("Skipping get number of unread messages request for {timeline_kind}"); - continue; - }; - - let _get_unreads_task = Handle::current().spawn(async move { - match sender.send(TimelineUpdate::NewUnreadMessagesCount( - UnreadMessageCount::Known(timeline.room().num_unread_messages()) - )) { - Ok(_) => SignalToUI::set_ui_signal(), - Err(e) => log!("Failed to send timeline update: {e:?} for GetNumberUnreadMessages request for {timeline_kind}"), - } - if let TimelineKind::MainRoom { room_id } = timeline_kind { - enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { - room_id, - is_marked_unread: timeline.room().is_marked_unread(), - unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), - unread_mentions: timeline.room().num_unread_mentions(), - }); - } - }); - } - - MatrixRequest::SetUnreadFlag { room_id, mark_as_unread } => { - let Some(main_timeline) = get_room_timeline(&room_id) else { - log!("BUG: skipping set unread flag request for not-yet-known room {room_id}"); - continue; - }; - let _set_unread_task = Handle::current().spawn(async move { - let result = main_timeline.room().set_unread_flag(mark_as_unread).await; - match result { - Ok(_) => log!("Set unread flag to {} for room {}", mark_as_unread, room_id), - Err(e) => error!("Failed to set unread flag to {} for room {}: {:?}", mark_as_unread, room_id, e), - } - }); - } - - MatrixRequest::SetIsFavorite { room_id, is_favorite } => { - let Some(main_timeline) = get_room_timeline(&room_id) else { - log!("BUG: skipping set favorite flag request for not-yet-known room {room_id}"); - continue; - }; - let _set_favorite_task = Handle::current().spawn(async move { - let result = main_timeline.room().set_is_favourite(is_favorite, None).await; - match result { - Ok(_) => log!("Set favorite to {} for room {}", is_favorite, room_id), - Err(e) => error!("Failed to set favorite to {} for room {}: {:?}", is_favorite, room_id, e), - } - }); - } - - MatrixRequest::SetIsLowPriority { room_id, is_low_priority } => { - let Some(main_timeline) = get_room_timeline(&room_id) else { - log!("BUG: skipping set low priority flag request for not-yet-known room {room_id}"); - continue; - }; - let _set_lp_task = Handle::current().spawn(async move { - let result = main_timeline.room().set_is_low_priority(is_low_priority, None).await; - match result { - Ok(_) => log!("Set low priority to {} for room {}", is_low_priority, room_id), - Err(e) => error!("Failed to set low priority to {} for room {}: {:?}", is_low_priority, room_id, e), - } - }); - } - - MatrixRequest::SetAvatar { avatar_url } => { - let Some(client) = get_client() else { continue }; - let _set_avatar_task = Handle::current().spawn(async move { - let is_removing = avatar_url.is_none(); - log!("Sending request to {} avatar...", if is_removing { "remove" } else { "set" }); - let result = client.account().set_avatar_url(avatar_url.as_deref()).await; - match result { - Ok(_) => { - log!("Successfully {} avatar.", if is_removing { "removed" } else { "set" }); - Cx::post_action(AccountDataAction::AvatarChanged(avatar_url)); - } - Err(e) => { - let err_msg = format!("Failed to {} avatar: {e}", if is_removing { "remove" } else { "set" }); - Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); - } - } - }); - } - - MatrixRequest::SetDisplayName { new_display_name } => { - let Some(client) = get_client() else { continue }; - let _set_display_name_task = Handle::current().spawn(async move { - let is_removing = new_display_name.is_none(); - log!("Sending request to {} display name{}...", - if is_removing { "remove" } else { "set" }, - new_display_name.as_ref().map(|n| format!(" to '{n}'")).unwrap_or_default() - ); - let result = client.account().set_display_name(new_display_name.as_deref()).await; - match result { - Ok(_) => { - log!("Successfully {} display name.", if is_removing { "removed" } else { "set" }); - Cx::post_action(AccountDataAction::DisplayNameChanged(new_display_name)); - } - Err(e) => { - let err_msg = format!("Failed to {} display name: {e}", if is_removing { "remove" } else { "set" }); - Cx::post_action(AccountDataAction::DisplayNameChangeFailed(err_msg)); - } - } - }); - } - - MatrixRequest::GenerateMatrixLink { room_id, event_id, use_matrix_scheme, join_on_click } => { - let Some(client) = get_client() else { continue }; - let _gen_link_task = Handle::current().spawn(async move { - if let Some(room) = client.get_room(&room_id) { - let result = if use_matrix_scheme { - if let Some(event_id) = event_id { - room.matrix_event_permalink(event_id).await - .map(MatrixLinkAction::MatrixUri) - } else { - room.matrix_permalink(join_on_click).await - .map(MatrixLinkAction::MatrixUri) - } - } else { - if let Some(event_id) = event_id { - room.matrix_to_event_permalink(event_id).await - .map(MatrixLinkAction::MatrixToUri) - } else { - room.matrix_to_permalink().await - .map(MatrixLinkAction::MatrixToUri) - } - }; - - match result { - Ok(action) => Cx::post_action(action), - Err(e) => Cx::post_action(MatrixLinkAction::Error(e.to_string())), - } - } else { - Cx::post_action(MatrixLinkAction::Error(format!("Room {room_id} not found"))); - } - }); - } - - MatrixRequest::IgnoreUser { ignore, room_member, room_id } => { - let Some(client) = get_client() else { continue }; - let _ignore_task = Handle::current().spawn(async move { - let user_id = room_member.user_id(); - log!("Sending request to {}ignore user: {user_id}...", if ignore { "" } else { "un" }); - let ignore_result = if ignore { - room_member.ignore().await - } else { - room_member.unignore().await - }; - - log!("{} user {user_id} {}", - if ignore { "Ignoring" } else { "Unignoring" }, - if ignore_result.is_ok() { "succeeded." } else { "failed." }, - ); - - if ignore_result.is_err() { - return; - } - - // We need to re-acquire the `RoomMember` object now that its state - // has changed, i.e., the user has been (un)ignored. - // We then need to send an update to replace the cached `RoomMember` - // with the now-stale ignored state. - if let Some(room) = client.get_room(&room_id) { - if let Ok(Some(new_room_member)) = room.get_member(user_id).await { - log!("Enqueueing user profile update for user {user_id}, who went from {}ignored to {}ignored.", - if room_member.is_ignored() { "" } else { "un" }, - if new_room_member.is_ignored() { "" } else { "un" }, - ); - enqueue_user_profile_update(UserProfileUpdate::RoomMemberOnly { - room_id: room_id.clone(), - room_member: new_room_member, - }); - } - } - - // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. - // Therefore, we need to re-fetch all timelines for all rooms, - // and currently the only way to actually accomplish this is via pagination. - // See: - // - // Note that here we only proactively re-paginate the *current* room - // (the one being viewed by the user when this ignore request was issued), - // and all other rooms will be re-paginated in `handle_ignore_user_list_subscriber()`.` - submit_async_request(MatrixRequest::PaginateTimeline { - timeline_kind: TimelineKind::MainRoom { room_id }, - num_events: 50, - direction: PaginationDirection::Backwards, - }); - }); - } - - MatrixRequest::SendTypingNotice { room_id, typing } => { - let Some(main_room_timeline) = get_room_timeline(&room_id) else { - log!("BUG: skipping send typing notice request for not-yet-known room {room_id}"); - continue; - }; - let _typing_task = Handle::current().spawn(async move { - if let Err(e) = main_room_timeline.room().typing_notice(typing).await { - error!("Failed to send typing notice to room {room_id}: {e:?}"); - } - }); - } - - MatrixRequest::SubscribeToTypingNotices { room_id, subscribe } => { - let (main_timeline, timeline_update_sender, mut typing_notice_receiver) = { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - let Some(jrd) = all_joined_rooms.get_mut(&room_id) else { - log!("BUG: room info not found for subscribe to typing notices request, room {room_id}"); - continue; - }; - let (main_timeline, receiver) = if subscribe { - if jrd.typing_notice_subscriber.is_some() { - warning!("Note: room {room_id} is already subscribed to typing notices."); - continue; - } else { - let main_timeline = jrd.main_timeline.timeline.clone(); - let (drop_guard, receiver) = main_timeline.room().subscribe_to_typing_notifications(); - jrd.typing_notice_subscriber = Some(drop_guard); - (main_timeline, receiver) - } - } else { - jrd.typing_notice_subscriber.take(); - continue; - }; - // Here: we don't have an existing subscriber running, so we fall through and start one. - (main_timeline, jrd.main_timeline.timeline_update_sender.clone(), receiver) - }; - - let _typing_notices_task = Handle::current().spawn(async move { - while let Ok(user_ids) = typing_notice_receiver.recv().await { - // log!("Received typing notifications for room {room_id}: {user_ids:?}"); - let mut users = Vec::with_capacity(user_ids.len()); - for user_id in user_ids { - users.push( - main_timeline.room() - .get_member_no_sync(&user_id) - .await - .ok() - .flatten() - .and_then(|m| m.display_name().map(|d| d.to_owned())) - .unwrap_or_else(|| user_id.to_string()) - ); - } - if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { - error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); - } - SignalToUI::set_ui_signal(); - } - // log!("Note: typing notifications recv loop has ended for room {}", room_id); - }); - } - - MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { - if !subscribe { - if let Some(task_handler) = subscribers_own_user_read_receipts.remove(&timeline_kind) { - task_handler.abort(); - } - continue; - } - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}"); - continue; - }; - - let timeline_kind_clone = timeline_kind.clone(); - let subscribe_own_read_receipt_task = Handle::current().spawn(async move { - let update_receiver = timeline.subscribe_own_user_read_receipts_changed().await; - pin_mut!(update_receiver); - if let Some(client_user_id) = current_user_id() { - if let Some((event_id, receipt)) = timeline.latest_user_read_receipt(&client_user_id).await { - log!("Received own user read receipt for {timeline_kind}: {receipt:?}, event ID: {event_id:?}"); - if sender.send(TimelineUpdate::OwnUserReadReceipt(receipt)).is_err() { - error!("Failed to send own user read receipt to UI."); - } - } - - while update_receiver.next().await.is_some() { - if let Some((_, receipt)) = timeline.latest_user_read_receipt(&client_user_id).await { - if sender.send(TimelineUpdate::OwnUserReadReceipt(receipt)).is_err() { - error!("Failed to send own user read receipt to UI."); - } - // When read receipts change (from other devices), update unread count - let unread_count = timeline.room().num_unread_messages(); - let unread_mentions = timeline.room().num_unread_mentions(); - if sender.send(TimelineUpdate::NewUnreadMessagesCount( - UnreadMessageCount::Known(unread_count) - )).is_err() { - error!("Failed to send unread message count update to UI."); - } - if let TimelineKind::MainRoom { room_id } = &timeline_kind { - // Update the rooms list with new unread counts - enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { - room_id: room_id.clone(), - is_marked_unread: timeline.room().is_marked_unread(), - unread_messages: UnreadMessageCount::Known(unread_count), - unread_mentions, - }); - } - } - } - } - }); - subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); - } - - MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { - if !subscribe { - if let Some(task_handler) = subscribers_pinned_events.remove(&room_id) { - task_handler.abort(); - } - continue; - } - let kind = TimelineKind::MainRoom { room_id: room_id.clone() }; - let Some((main_timeline, sender)) = get_timeline_and_sender(&kind) else { - log!("BUG: skipping subscribe to pinned events request for unknown room {room_id}"); - continue; - }; - let subscribe_pinned_events_task = Handle::current().spawn(async move { - // Send an initial update, as the stream may not update immediately. - let pinned_events = main_timeline.room().pinned_event_ids().unwrap_or_default(); - match sender.send(TimelineUpdate::PinnedEvents(pinned_events)) { - Ok(()) => SignalToUI::set_ui_signal(), - Err(_) => log!("Failed to send initial pinned events update to UI."), - } - let update_receiver = main_timeline.room().pinned_event_ids_stream(); - pin_mut!(update_receiver); - while let Some(pinned_events) = update_receiver.next().await { - match sender.send(TimelineUpdate::PinnedEvents(pinned_events)) { - Ok(()) => SignalToUI::set_ui_signal(), - Err(e) => log!("Failed to send pinned events update: {e:?}"), - } - } - }); - subscribers_pinned_events.insert(room_id, subscribe_pinned_events_task); - } - - MatrixRequest::SpawnSSOServer { brand, homeserver_url, identity_provider_id} => { - spawn_sso_server(brand, homeserver_url, identity_provider_id, login_sender.clone()).await; - } - - MatrixRequest::ResolveRoomAlias(room_alias) => { - let Some(client) = get_client() else { continue }; - let _resolve_task = Handle::current().spawn(async move { - log!("Sending resolve room alias request for {room_alias}..."); - let res = client.resolve_room_alias(&room_alias).await; - log!("Resolved room alias {room_alias} to: {res:?}"); - todo!("Send the resolved room alias back to the UI thread somehow."); - }); - } - - MatrixRequest::FetchAvatar { mxc_uri, on_fetched } => { - let Some(client) = get_client() else { continue }; - Handle::current().spawn(async move { - // log!("Sending fetch avatar request for {mxc_uri:?}..."); - let media_request = MediaRequestParameters { - source: MediaSource::Plain(mxc_uri.clone()), - format: AVATAR_THUMBNAIL_FORMAT.into(), - }; - let res = client.media().get_media_content(&media_request, true).await; - // log!("Fetched avatar for {mxc_uri:?}, succeeded? {}", res.is_ok()); - on_fetched(AvatarUpdate { mxc_uri, avatar_data: res.map(|v| v.into()) }); - }); - } - - MatrixRequest::FetchMedia { media_request, on_fetched, destination, update_sender } => { - let Some(client) = get_client() else { continue }; - - let _fetch_task = Handle::current().spawn(async move { - // log!("Sending fetch media request for {media_request:?}..."); - let res = client.media().get_media_content(&media_request, true).await; - on_fetched(&destination, media_request, res, update_sender); - }); - } - - MatrixRequest::SendMessage { - timeline_kind, - message, - replied_to, - #[cfg(feature = "tsp")] - sign_with_tsp, - } => { - // TODO: use this timeline `_sender` once we support sending-message status/operations in the UI. - let Some((timeline, _sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for send message request"); - continue; - }; - - // Spawn a new async task that will send the actual message. - let _send_message_task = Handle::current().spawn(async move { - log!("Sending message to {timeline_kind}: {message:?}..."); - let message = { - #[cfg(not(feature = "tsp"))] { - message - } - - #[cfg(feature = "tsp")] { - let mut message = message; - if sign_with_tsp { - log!("Signing message with TSP..."); - match serde_json::to_vec(&message) { - Ok(message_bytes) => { - log!("Serialized message to bytes, length {}", message_bytes.len()); - match crate::tsp::sign_anycast_with_default_vid(&message_bytes) { - Ok(signed_msg) => { - log!("Successfully signed message with TSP, length {}", signed_msg.len()); - use matrix_sdk::ruma::serde::Base64; - message.tsp_signature = Some(Base64::new(signed_msg)); - } - Err(e) => { - error!("Failed to sign message with TSP: {e:?}"); - enqueue_popup_notification( - format!("Failed to sign message with TSP: {e}"), - PopupKind::Error, - None, - ); - return; - } - } - } - Err(e) => { - error!("Failed to serialize message to bytes for TSP signing: {e:?}"); - enqueue_popup_notification( - format!("Failed to serialize message for TSP signing: {e}"), - PopupKind::Error, - None, - ); - return; - } - } - } - message - } - }; - - if let Some(replied_to_info) = replied_to { - let reply_content = match timeline - .room() - .make_reply_event(message.into(), replied_to_info) - .await - { - Ok(content) => content, - Err(_e) => { - error!("Failed to build reply content to send to {timeline_kind}: {_e:?}"); - enqueue_popup_notification( - format!("Failed to send reply: {_e}"), - PopupKind::Error, - None, - ); - return; - } - }; - match timeline.send(reply_content.into()).await { - Ok(_send_handle) => log!("Sent reply message to {timeline_kind}."), - Err(_e) => { - error!("Failed to send reply message to {timeline_kind}: {_e:?}"); - enqueue_popup_notification(format!("Failed to send reply: {_e}"), PopupKind::Error, None); - } - } - } else { - match timeline.send(message.into()).await { - Ok(_send_handle) => log!("Sent message to {timeline_kind}."), - Err(_e) => { - error!("Failed to send message to {timeline_kind}: {_e:?}"); - enqueue_popup_notification(format!("Failed to send message: {_e}"), PopupKind::Error, None); - } - } - } - SignalToUI::set_ui_signal(); - }); - } - - MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { - let Some(timeline) = get_timeline(&timeline_kind) else { - log!("BUG: {timeline_kind} not found when sending read receipt, {event_id}"); - continue; - }; - - let _send_rr_task = Handle::current().spawn(async move { - match timeline.send_single_receipt(receipt_type.clone(), event_id.clone()).await { - Ok(sent) => log!("{} {receipt_type} read receipt to {timeline_kind} for event {event_id}", if sent { "Sent" } else { "Already sent" }), - Err(_e) => error!("Failed to send {receipt_type} read receipt to {timeline_kind} for event {event_id}; error: {_e:?}"), - } - if let TimelineKind::MainRoom { room_id } = timeline_kind { - // Also update the number of unread messages in the room. - enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { - room_id, - is_marked_unread: timeline.room().is_marked_unread(), - unread_messages: UnreadMessageCount::Known(timeline.room().num_unread_messages()), - unread_mentions: timeline.room().num_unread_mentions() - }); - } - }); - }, - - MatrixRequest::GetRoomPowerLevels { timeline_kind } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for room power levels request"); - continue; - }; - - let Some(user_id) = current_user_id() else { continue }; - - let _power_levels_task = Handle::current().spawn(async move { - match timeline.room().power_levels().await { - Ok(power_levels) => { - log!("Successfully fetched power levels for {timeline_kind}."); - if sender.send(TimelineUpdate::UserPowerLevels( - UserPowerLevels::from(&power_levels, &user_id), - )).is_err() { - error!("Failed to send room power levels to UI.") - } - SignalToUI::set_ui_signal(); - } - Err(e) => { - error!("Failed to fetch power levels for {timeline_kind}: {e:?}"); - } - } - }); - }, - - MatrixRequest::ToggleReaction { timeline_kind, timeline_event_id, reaction } => { - let Some(timeline) = get_timeline(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for toggle reaction request"); - continue; - }; - - let _toggle_reaction_task = Handle::current().spawn(async move { - log!("Sending toggle reaction {reaction:?} to {timeline_kind}: ..."); - match timeline.toggle_reaction(&timeline_event_id, &reaction).await { - Ok(_send_handle) => { - log!("Sent toggle reaction {reaction:?} to {timeline_kind}."); - SignalToUI::set_ui_signal(); - }, - Err(_e) => error!("Failed to send toggle reaction to {timeline_kind}; error: {_e:?}"), - } - }); - }, - - MatrixRequest::RedactMessage { timeline_kind, timeline_event_id, reason } => { - let Some(timeline) = get_timeline(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for redact message request"); - continue; - }; - - let _redact_task = Handle::current().spawn(async move { - match timeline.redact(&timeline_event_id, reason.as_deref()).await { - Ok(()) => log!("Successfully redacted message in {timeline_kind}."), - Err(e) => { - error!("Failed to redact message in {timeline_kind}; error: {e:?}"); - enqueue_popup_notification( - format!("Failed to redact message. Error: {e}"), - PopupKind::Error, - None, - ); - } - } - }); - }, - - MatrixRequest::PinEvent { timeline_kind, event_id, pin } => { - let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { - log!("BUG: {timeline_kind} not found for pin event request"); - continue; - }; - - let _pin_task = Handle::current().spawn(async move { - let result = if pin { - timeline.pin_event(&event_id).await - } else { - timeline.unpin_event(&event_id).await - }; - match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { - Ok(_) => SignalToUI::set_ui_signal(), - Err(_) => log!("Failed to send UI update for pin event."), - } - }); - } - - MatrixRequest::GetMatrixRoomLinkPillInfo { matrix_id, via } => { - let Some(client) = get_client() else { continue }; - let _fetch_matrix_link_pill_info_task = Handle::current().spawn(async move { - let room_or_alias_id: Option<&RoomOrAliasId> = match &matrix_id { - MatrixId::Room(room_id) => Some((&**room_id).into()), - MatrixId::RoomAlias(room_alias_id) => Some((&**room_alias_id).into()), - MatrixId::Event(room_or_alias_id, _event_id) => Some(room_or_alias_id), - _ => { - log!("MatrixLinkRoomPillInfoRequest: Unsupported MatrixId type: {matrix_id:?}"); - return; - } - }; - if let Some(room_or_alias_id) = room_or_alias_id { - match client.get_room_preview(room_or_alias_id, via).await { - Ok(preview) => Cx::post_action(MatrixLinkPillState::Loaded { - matrix_id: matrix_id.clone(), - name: preview.name.unwrap_or_else(|| room_or_alias_id.to_string()), - avatar_url: preview.avatar_url - }), - Err(_e) => { - log!("Failed to get room link pill info for {room_or_alias_id:?}: {_e:?}"); - } - }; - } - }); - } - - MatrixRequest::GetUrlPreview { url, on_fetched, destination, update_sender } => { - // const MAX_LOG_RESPONSE_BODY_LENGTH: usize = 1000; - // log!("Starting URL preview fetch for: {}", url); - let _fetch_url_preview_task = Handle::current().spawn(async move { - let result: Result = async { - // log!("Getting Matrix client for URL preview: {}", url); - let client = get_client().ok_or_else(|| { - // error!("Matrix client not available for URL preview: {}", url); - UrlPreviewError::ClientNotAvailable - })?; - - let token = client.access_token().ok_or_else(|| { - // error!("Access token not available for URL preview: {}", url); - UrlPreviewError::AccessTokenNotAvailable - })?; - // Official Doc: https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediapreview_url - // Element desktop is using /_matrix/media/v3/preview_url - let endpoint_url = client.homeserver().join("/_matrix/client/v1/media/preview_url") - .map_err(UrlPreviewError::UrlParse)?; - // log!("Fetching URL preview from endpoint: {} for URL: {}", endpoint_url, url); - - let response = client - .http_client() - .get(endpoint_url.clone()) - .bearer_auth(token) - .query(&[("url", url.as_str())]) - .header("Content-Type", "application/json") - .send() - .await - .map_err(|e| { - // error!("HTTP request failed for URL preview {}: {}", url, e); - UrlPreviewError::Request(e) - })?; - - let status = response.status(); - // log!("URL preview response status for {}: {}", url, status); - - if !status.is_success() && status.as_u16() != 429 { - // error!("URL preview request failed with status {} for URL: {}", status, url); - return Err(UrlPreviewError::HttpStatus(status.as_u16())); - } - - let text = response.text().await.map_err(|e| { - // error!("Failed to read response text for URL preview {}: {}", url, e); - UrlPreviewError::Request(e) - })?; - - // log!("URL preview response body length for {}: {} bytes", url, text.len()); - // if text.len() > MAX_LOG_RESPONSE_BODY_LENGTH { - // log!("URL preview response body preview for {}: {}...", url, &text[..MAX_LOG_RESPONSE_BODY_LENGTH]); - // } else { - // log!("URL preview response body for {}: {}", url, text); - // } - // This request is rate limited, retry after a duration we get from the server. - if status.as_u16() == 429 { - let link_preview_429_res = serde_json::from_str::(&text) - .map_err(|e| { - // error!("Failed to parse as LinkPreviewRateLimitResponse for URL preview {}: {}", url, e); - UrlPreviewError::Json(e) - }); - match link_preview_429_res { - Ok(link_preview_429_res) => { - if let Some(retry_after) = link_preview_429_res.retry_after_ms { - tokio::time::sleep(Duration::from_millis(retry_after.into())).await; - submit_async_request(MatrixRequest::GetUrlPreview{ - url: url.clone(), - on_fetched, - destination: destination.clone(), - update_sender: update_sender.clone(), - }); - - } - } - Err(_e) => { - // error!("Failed to parse as LinkPreviewRateLimitResponse for URL preview {}: {}", url, _e); - } - } - return Err(UrlPreviewError::HttpStatus(429)); - } - serde_json::from_str::(&text) - .or_else(|_first_error| { - // log!("Failed to parse as LinkPreviewData, trying LinkPreviewDataNonNumeric for URL: {}", url); - serde_json::from_str::(&text) - .map(|non_numeric| non_numeric.into()) - }) - .map_err(|e| { - // error!("Failed to parse JSON response for URL preview {}: {}", url, e); - // error!("Response body that failed to parse: {}", text); - UrlPreviewError::Json(e) - }) - }.await; - - // match &result { - // Ok(preview_data) => { - // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", - // url, preview_data.title, preview_data.site_name); - // } - // Err(e) => { - // error!("URL preview fetch failed for {}: {}", url, e); - // } - // } - - on_fetched(url, destination, result, update_sender); - SignalToUI::set_ui_signal(); - }); - } - } - } - - error!("matrix_worker_task task ended unexpectedly"); - bail!("matrix_worker_task task ended unexpectedly") -} - - -/// The single global Tokio runtime that is used by all async tasks. -static TOKIO_RUNTIME: Mutex> = Mutex::new(None); - -/// The sender used by [`submit_async_request`] to send requests to the async worker thread. -/// Currently there is only one, but it can be cloned if we need more concurrent senders. -static REQUEST_SENDER: Mutex>> = Mutex::new(None); - -/// A client object that is proactively created during initialization -/// in order to speed up the client-building process when the user logs in. -static DEFAULT_SSO_CLIENT: Mutex> = Mutex::new(None); - -/// Used to notify the SSO login task that the async creation of the `DEFAULT_SSO_CLIENT` has finished. -static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = LazyLock::new(|| Arc::new(Notify::new())); - -/// Blocks the current thread until the given future completes. -/// -/// ## Warning -/// This should be used with caution, especially on the main UI thread, -/// as blocking a thread prevents it from handling other events or running other tasks. -pub fn block_on_async_with_timeout( - timeout: Option, - async_future: impl Future, -) -> Result { - let rt = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| - tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") - ).handle().clone(); - - if let Some(timeout) = timeout { - rt.block_on(async { - tokio::time::timeout(timeout, async_future).await - }) - } else { - Ok(rt.block_on(async_future)) - } -} - - -/// The primary initialization routine for starting the Matrix client sync -/// and the async tokio runtime. -/// -/// Returns a handle to the Tokio runtime that is used to run async background tasks. -pub fn start_matrix_tokio() -> Result { - // Create a Tokio runtime, and save it in a static variable to ensure it isn't dropped. - let rt_handle = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| { - tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") - }).handle().clone(); - - // Proactively build a Matrix Client in the background so that the SSO Server - // can have a quicker start if needed (as it's rather slow to build this client). - rt_handle.spawn(async move { - match build_client(&Cli::default(), app_data_dir()).await { - Ok(client_and_session) => { - DEFAULT_SSO_CLIENT.lock().unwrap() - .get_or_insert(client_and_session); - } - Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), - }; - DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); - Cx::post_action(LoginAction::SsoPending(false)); - }); - - let rt = rt_handle.clone(); - // Spawn the main async task that drives the Matrix client SDK, which itself will - // start and monitor other related background tasks. - rt_handle.spawn(start_matrix_client_login_and_sync(rt)); - - Ok(rt_handle) -} - - -/// A tokio::watch channel sender for sending requests from the RoomScreen UI widget -/// to the corresponding background async task for that room (its `timeline_subscriber_handler`). -pub type TimelineRequestSender = watch::Sender>; - -/// The return type for [`take_timeline_endpoints()`]. -/// -/// This primarily contains endpoints for channels of communication -/// between the timeline UI (`RoomScreen`] and the background worker tasks. -/// If the relevant room was tombstoned, this also includes info about its successor room. -pub struct TimelineEndpoints { - pub update_sender: crossbeam_channel::Sender, - pub update_receiver: crossbeam_channel::Receiver, - pub request_sender: TimelineRequestSender, - pub successor_room: Option, -} - -/// Info about a timeline for a joined room or a thread in a joined room. -struct PerTimelineDetails { - /// A shared reference to a room's main timeline or thread's timeline of events. - timeline: Arc, - /// A clone-able sender for updates to this timeline. - timeline_update_sender: crossbeam_channel::Sender, - /// A tuple of two separate channel endpoints that can only be taken *once* by the main UI thread. - /// - /// 1. The single receiver that can receive updates from this timeline. - /// * When a new room is joined (or a thread is opened), an unbounded crossbeam channel will be created - /// and its sender given to a background task (the `timeline_subscriber_handler()`) - /// that enqueues timeline updates as it receives timeline vector diffs from the server. - /// * The UI thread can take ownership of this update receiver in order to receive updates - /// to this room or thread timeline, but only one receiver can exist at a time. - /// 2. The sender that can send requests to the background timeline subscriber handler, - /// e.g., to watch for a specific event to be prepended to the timeline (via back pagination). - timeline_singleton_endpoints: Option<( - crossbeam_channel::Receiver, - TimelineRequestSender, - )>, - /// The async task that listens for updates for this timeline. - timeline_subscriber_handler_task: JoinHandle<()>, -} - -struct JoinedRoomDetails { - /// The room ID of this joined room. - room_id: OwnedRoomId, - /// Details about the main timeline for this room. - main_timeline: PerTimelineDetails, - /// Thread-focused timelines for this room, keyed by thread root event ID. - thread_timelines: HashMap, - /// The set of thread timelines currently being created, to avoid duplicate in-flight work. - pending_thread_timelines: HashSet, - /// A drop guard for the event handler that represents a subscription to typing notices for this room. - typing_notice_subscriber: Option, - /// A drop guard for the event handler that represents a subscription to pinned events for this room. - pinned_events_subscriber: Option, -} -impl Drop for JoinedRoomDetails { - fn drop(&mut self) { - log!("Dropping JoinedRoomDetails for room {}", self.room_id); - self.main_timeline.timeline_subscriber_handler_task.abort(); - for thread_timeline in self.thread_timelines.values() { - thread_timeline.timeline_subscriber_handler_task.abort(); - } - drop(self.typing_notice_subscriber.take()); - drop(self.pinned_events_subscriber.take()); - } -} - - -/// A const-compatible hasher, used for `static` items containing `HashMap`s or `HashSet`s. -type ConstHasher = BuildHasherDefault; - -/// Information about all joined rooms that our client currently know about. -/// We use a `HashMap` for O(1) lookups, as this is accessed frequently (e.g. every timeline update). -static ALL_JOINED_ROOMS: Mutex> = Mutex::new(HashMap::with_hasher(BuildHasherDefault::new())); - -/// Returns the timeline and timeline update sender for the given joined room/thread timeline. -fn get_per_timeline_details<'a>( - all_joined_rooms: &'a mut HashMap, - kind: &TimelineKind, -) -> Option<&'a mut PerTimelineDetails> { - let room_info = all_joined_rooms.get_mut(kind.room_id())?; - match kind { - TimelineKind::MainRoom { .. } => Some(&mut room_info.main_timeline), - TimelineKind::Thread { thread_root_event_id, .. } => room_info.thread_timelines.get_mut(thread_root_event_id), - } -} - -/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the timeline for the given timeline kind. -fn get_timeline(kind: &TimelineKind) -> Option> { - get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap().deref_mut(), kind) - .map(|details| details.timeline.clone()) -} - -/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the timeline and timeline update sender for the given timeline kind. -fn get_timeline_and_sender(kind: &TimelineKind) -> Option<(Arc, crossbeam_channel::Sender)> { - get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap().deref_mut(), kind) - .map(|details| (details.timeline.clone(), details.timeline_update_sender.clone())) -} - -/// Obtains the lock on `ALL_JOINED_ROOMS` and returns the main timeline for the given room. -fn get_room_timeline(room_id: &RoomId) -> Option> { - ALL_JOINED_ROOMS.lock().unwrap() - .get(room_id) - .map(|jrd| jrd.main_timeline.timeline.clone()) -} - -/// The logged-in Matrix client, which can be freely and cheaply cloned. -static CLIENT: Mutex> = Mutex::new(None); - -pub fn get_client() -> Option { - CLIENT.lock().unwrap().clone() -} - -/// Returns the user ID of the currently logged-in user, if any. -pub fn current_user_id() -> Option { - CLIENT.lock().unwrap().as_ref().and_then(|c| - c.session_meta().map(|m| m.user_id.clone()) - ) -} - -/// The singleton sync service. -static SYNC_SERVICE: Mutex>> = Mutex::new(None); - - -/// Get a reference to the current sync service, if available. -pub fn get_sync_service() -> Option> { - SYNC_SERVICE.lock().ok()?.as_ref().cloned() -} - -/// The list of users that the current user has chosen to ignore. -/// Ideally we shouldn't have to maintain this list ourselves, -/// but the Matrix SDK doesn't currently properly maintain the list of ignored users. -static IGNORED_USERS: Mutex> = Mutex::new(HashSet::with_hasher(BuildHasherDefault::new())); - -/// Returns a deep clone of the current list of ignored users. -pub fn get_ignored_users() -> HashSet { - IGNORED_USERS.lock().unwrap().clone() -} - -/// Returns whether the given user ID is currently being ignored. -pub fn is_user_ignored(user_id: &UserId) -> bool { - IGNORED_USERS.lock().unwrap().contains(user_id) -} - - -/// Returns three channel endpoints related to the timeline for the given joined room or thread. -/// -/// 1. A timeline update sender. -/// 2. The timeline update receiver, which is a singleton, and can only be taken once. -/// 3. A `tokio::watch` sender that can be used to send requests to the timeline subscriber handler. -/// -/// This will only succeed once per room (or once per room thread), -/// as only a single channel receiver can exist. -pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); - let jrd = all_joined_rooms.get_mut(kind.room_id())?; - let details = match kind { - TimelineKind::MainRoom { .. } => &mut jrd.main_timeline, - TimelineKind::Thread { thread_root_event_id, .. } => jrd.thread_timelines.get_mut(thread_root_event_id)?, - }; - let (update_receiver, request_sender) = details.timeline_singleton_endpoints.take()?; - Some(TimelineEndpoints { - update_sender: details.timeline_update_sender.clone(), - update_receiver, - request_sender, - successor_room: details.timeline.room().successor_room(), - }) -} - -const DEFAULT_HOMESERVER: &str = "matrix.org"; - -fn username_to_full_user_id( - username: &str, - homeserver: Option<&str>, -) -> Option { - username - .try_into() - .ok() - .or_else(|| { - let homeserver_url = homeserver.unwrap_or(DEFAULT_HOMESERVER); - let user_id_str = if username.starts_with("@") { - format!("{}:{}", username, homeserver_url) - } else { - format!("@{}:{}", username, homeserver_url) - }; - user_id_str.as_str().try_into().ok() - }) -} - - -/// Info we store about a room received by the room list service. -/// -/// This struct is necessary in order for us to track the previous state -/// of a room received from the room list service, so that we can -/// determine what room data has changed since the last update. -/// We can't just store the `matrix_sdk::Room` object itself, -/// because that is a shallow reference to an inner room object within -/// the room list service. -#[derive(Clone)] -struct RoomListServiceRoomInfo { - room_id: OwnedRoomId, - state: RoomState, - is_direct: bool, - is_marked_unread: bool, - is_tombstoned: bool, - tags: Option, - user_power_levels: Option, - latest_event_timestamp: Option, - num_unread_messages: u64, - num_unread_mentions: u64, - display_name: Option, - room_avatar: Option, - room: matrix_sdk::Room, -} -impl RoomListServiceRoomInfo { - async fn from_room(room: matrix_sdk::Room, current_user_id: &Option) -> Self { - // Parallelize fetching of independent room data. - let (is_direct, tags, display_name, user_power_levels) = tokio::join!( - room.is_direct(), - room.tags(), - room.display_name(), - async { - if let Some(user_id) = current_user_id { - UserPowerLevels::from_room(&room, user_id.deref()).await - } else { - None - } - } - ); - - Self { - room_id: room.room_id().to_owned(), - state: room.state(), - is_direct: is_direct.unwrap_or(false), - is_marked_unread: room.is_marked_unread(), - is_tombstoned: room.is_tombstoned(), - tags: tags.ok().flatten(), - user_power_levels, - latest_event_timestamp: room.latest_event_timestamp(), - num_unread_messages: room.num_unread_messages(), - num_unread_mentions: room.num_unread_mentions(), - display_name: display_name.ok(), - room_avatar: room.avatar_url(), - room, - } - } - async fn from_room_ref(room: &matrix_sdk::Room, current_user_id: &Option) -> Self { - Self::from_room(room.clone(), current_user_id).await - } -} - -/// Performs the Matrix client login or session restore, and starts the main sync service. -/// -/// After starting the sync service, this also starts the main room list service loop -/// and the main space service loop. -async fn start_matrix_client_login_and_sync(rt: Handle) { - // Create a channel for sending requests from the main UI thread to a background worker task. - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); - REQUEST_SENDER.lock().unwrap().replace(sender); - - let (login_sender, mut login_receiver) = tokio::sync::mpsc::channel(1); - - // Spawn the async worker task that handles matrix requests. - // We must do this now such that the matrix worker task can listen for incoming login requests - // from the UI, and forward them to this task (via the login_sender --> login_receiver). - let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); - - let most_recent_user_id = persistence::most_recent_user_id().await; - log!("Most recent user ID: {most_recent_user_id:?}"); - let cli_parse_result = Cli::try_parse(); - let cli_has_valid_username_password = cli_parse_result.as_ref() - .is_ok_and(|cli| !cli.user_id.is_empty() && !cli.password.is_empty()); - log!("CLI parsing succeeded? {}. CLI has valid UN+PW? {}", - cli_parse_result.as_ref().is_ok(), - cli_has_valid_username_password, - ); - let wait_for_login = !cli_has_valid_username_password && ( - most_recent_user_id.is_none() - || std::env::args().any(|arg| arg == "--login-screen" || arg == "--force-login") - ); - log!("Waiting for login? {}", wait_for_login); - - let new_login_opt = if !wait_for_login { - let specified_username = cli_parse_result.as_ref().ok().and_then(|cli| - username_to_full_user_id( - &cli.user_id, - cli.homeserver.as_deref(), - ) - ); - log!("Trying to restore session for user: {:?}", - specified_username.as_ref().or(most_recent_user_id.as_ref()) - ); - match persistence::restore_session(specified_username).await { - Ok(session) => Some(session), - Err(e) => { - let status_err = "Could not restore previous user session.\n\nPlease login again."; - log!("{status_err} Error: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); - - if let Ok(cli) = &cli_parse_result { - log!("Attempting auto-login from CLI arguments as user '{}'...", cli.user_id); - Cx::post_action(LoginAction::CliAutoLogin { - user_id: cli.user_id.clone(), - homeserver: cli.homeserver.clone(), - }); - match login(cli, LoginRequest::LoginByCli).await { - Ok(new_login) => Some(new_login), - Err(e) => { - error!("CLI-based login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure( - format!("Could not login with CLI-provided arguments.\n\nPlease login manually.\n\nError: {e}") - )); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e:?}"), - }); - None - } - } - } else { - None - } - } - } - } else { - None - }; - let cli: Cli = cli_parse_result.unwrap_or(Cli::default()); - // `initial_client_opt` holds the client obtained from the session restore or CLI auto-login. - // On subsequent iterations of the login loop (after a post-auth setup failure), it is `None`, - // which causes the loop to wait for the user to submit a new manual login request. - let mut initial_client_opt = new_login_opt; - - let (client, sync_service, logged_in_user_id) = 'login_loop: loop { - let (client, _sync_token) = match initial_client_opt.take() { - Some(login) => login, - None => { - loop { - log!("Waiting for login request..."); - match login_receiver.recv().await { - Some(login_request) => { - match login(&cli, login_request).await { - Ok((client, sync_token)) => break (client, sync_token), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e}"), - }); - } - } - }, - None => { - error!("BUG: login_receiver hung up unexpectedly"); - let err = String::from("Please restart Robrix.\n\nUnable to listen for login requests."); - Cx::post_action(LoginAction::LoginFailure(err.clone())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err, - }); - return; - } - } - } - } - }; - - // Deallocate the default SSO client after a successful login. - if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { - let _ = client_opt.take(); - } - - let logged_in_user_id: OwnedUserId = client.user_id() - .expect("BUG: Client::user_id() returned None after successful login!") - .to_owned(); - let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); - enqueue_rooms_list_update(RoomsListUpdate::Status { status }); - - // Store this active client in our global Client state so that other tasks can access it. - if let Some(_existing) = CLIENT.lock().unwrap().replace(client.clone()) { - error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); - } - - // Listen for changes to our verification status and incoming verification requests. - add_verification_event_handlers_and_sync_client(client.clone()); - - // Listen for updates to the ignored user list. - handle_ignore_user_list_subscriber(client.clone()); - - // Listen for session changes, e.g., when the access token becomes invalid. - handle_session_changes(client.clone()); - - Cx::post_action(LoginAction::Status { - title: "Connecting".into(), - status: "Setting up sync service...".into(), - }); - let sync_service = match SyncService::builder(client.clone()) - .with_offline_mode() - .build() - .await - { - Ok(ss) => ss, - Err(e) => { - error!("Failed to create SyncService: {e:?}"); - let err_msg = if is_invalid_token_error(&e) { - "Your login token is no longer valid.\n\nPlease log in again.".to_string() - } else { - format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") - }; - Cx::post_action(LoginAction::LoginFailure(err_msg.clone())); - enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg }); - // Clear the stored client so the next login attempt doesn't trigger the - // "unexpectedly replaced an existing client" warning. - let _ = CLIENT.lock().unwrap().take(); - continue 'login_loop; - } - }; - - break 'login_loop (client, sync_service, logged_in_user_id); - }; - - // Signal login success now that SyncService::build() has already succeeded (inside - // 'login_loop), which is the only step that can fail with an invalid/expired token. - // Doing this before sync_service.start() lets the UI transition to the home screen - // without waiting for the sync loop to begin. - Cx::post_action(LoginAction::LoginSuccess); - - // Attempt to load the previously-saved app state. - handle_load_app_state(logged_in_user_id.to_owned()); - handle_sync_indicator_subscriber(&sync_service); - handle_sync_service_state_subscriber(sync_service.state()); - sync_service.start().await; - - let room_list_service = sync_service.room_list_service(); - - if let Some(_existing) = SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)) { - error!("BUG: unexpectedly replaced an existing sync service when initializing the matrix client."); - } - - let mut room_list_service_task = rt.spawn(room_list_service_loop(room_list_service)); - let mut space_service_task = rt.spawn(space_service_loop(client)); - - // Now, this task becomes an infinite loop that monitors the state of the - // three core matrix-related background tasks that we just spawned above. - #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. - loop { - tokio::select! { - result = &mut matrix_worker_task_handle => { - match result { - Ok(Ok(())) => { - // Check if this is due to logout - if is_logout_in_progress() { - log!("matrix worker task ended due to logout"); - } else { - error!("BUG: matrix worker task ended unexpectedly!"); - } - } - Ok(Err(e)) => { - // Check if this is due to logout - if is_logout_in_progress() { - log!("matrix worker task ended with error due to logout: {e:?}"); - } else { - error!("Error: matrix worker task ended:\n\t{e:?}"); - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { - status: e.to_string(), - }); - enqueue_popup_notification( - format!("Rooms list update error: {e}"), - PopupKind::Error, - None, - ); - } - }, - Err(e) => { - error!("BUG: failed to join matrix worker task: {e:?}"); - } - } - break; - } - result = &mut room_list_service_task => { - match result { - Ok(Ok(())) => { - error!("BUG: room list service loop task ended unexpectedly!"); - } - Ok(Err(e)) => { - error!("Error: room list service loop task ended:\n\t{e:?}"); - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { - status: e.to_string(), - }); - enqueue_popup_notification( - format!("Room list service error: {e}"), - PopupKind::Error, - None, - ); - }, - Err(e) => { - error!("BUG: failed to join room list service loop task: {e:?}"); - } - } - break; - } - result = &mut space_service_task => { - match result { - Ok(Ok(())) => { - error!("BUG: space service loop task ended unexpectedly!"); - } - Ok(Err(e)) => { - error!("Error: space service loop task ended:\n\t{e:?}"); - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { - status: e.to_string(), - }); - enqueue_popup_notification( - format!("Space service error: {e}"), - PopupKind::Error, - None, - ); - }, - Err(e) => { - error!("BUG: failed to join space service loop task: {e:?}"); - } - } - break; - } - } - } -} - - -/// The main async task that listens for changes to all rooms. -async fn room_list_service_loop(room_list_service: Arc) -> Result<()> { - let all_rooms_list = room_list_service.all_rooms().await?; - handle_room_list_service_loading_state(all_rooms_list.loading_state()); - - let (room_diff_stream, room_list_dynamic_entries_controller) = - // TODO: paginate room list to avoid loading all rooms at once - all_rooms_list.entries_with_dynamic_adapters(usize::MAX); - - // By default, our rooms list should only show rooms that are: - // 1. not spaces (those are handled by the SpaceService), - // 2. not left (clients don't typically show rooms that the user has already left), - // 3. not outdated (don't show tombstoned rooms whose successor is already joined). - room_list_dynamic_entries_controller.set_filter(Box::new( - filters::new_filter_all(vec![ - Box::new(filters::new_filter_not(Box::new(filters::new_filter_space()))), - Box::new(filters::new_filter_non_left()), - Box::new(filters::new_filter_deduplicate_versions()), - ]) - )); - - let mut all_known_rooms: Vector = Vector::new(); - let current_user_id = current_user_id(); - - pin_mut!(room_diff_stream); - while let Some(batch) = room_diff_stream.next().await { - let mut peekable_diffs = batch.into_iter().peekable(); - while let Some(diff) = peekable_diffs.next() { - let is_reset = matches!(diff, VectorDiff::Reset { .. }); - match diff { - VectorDiff::Append { values: new_rooms } - | VectorDiff::Reset { values: new_rooms } => { - // Append and Reset are identical, except for Reset first clears all rooms. - let _num_new_rooms = new_rooms.len(); - if is_reset { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Reset, old length {}, new length {}", all_known_rooms.len(), new_rooms.len()); } - // Iterate manually so we can know which rooms are being removed. - while let Some(room) = all_known_rooms.pop_back() { - remove_room(&room); - } - // ALL_JOINED_ROOMS should already be empty due to successive calls to `remove_room()`, - // so this is just a sanity check. - ALL_JOINED_ROOMS.lock().unwrap().clear(); - enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); - } else { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Append, old length {}, adding {} new items", all_known_rooms.len(), _num_new_rooms); } - } - - // Parallelize creating each room's RoomListServiceRoomInfo and adding that new room. - // We combine `from_room` and `add_new_room` into a single async task per room. - let new_room_infos: Vec = join_all( - new_rooms.into_iter().map(|room| async { - let room_info = RoomListServiceRoomInfo::from_room(room.into_inner(), ¤t_user_id).await; - if let Err(e) = add_new_room(&room_info, &room_list_service, false).await { - error!("Failed to add new room: {:?} ({}); error: {:?}", room_info.display_name, room_info.room_id, e); - } - room_info - }) - ).await; - - // Send room order update with the new room IDs - let (room_id_refs, room_ids) = { - let mut room_id_refs = Vec::with_capacity(new_room_infos.len()); - let mut room_ids = Vec::with_capacity(new_room_infos.len()); - for r in &new_room_infos { - room_id_refs.push(r.room_id.as_ref()); - room_ids.push(r.room_id.clone()); - } - (room_id_refs, room_ids) - }; - if !room_ids.is_empty() { - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Append { values: room_ids } - )); - room_list_service.subscribe_to_rooms(&room_id_refs).await; - all_known_rooms.extend(new_room_infos); - } - } - VectorDiff::Clear => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Clear"); } - all_known_rooms.clear(); - ALL_JOINED_ROOMS.lock().unwrap().clear(); - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); - enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); - } - VectorDiff::PushFront { value: new_room } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushFront"); } - let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; - let room_id = new_room.room_id.clone(); - add_new_room(&new_room, &room_list_service, true).await?; - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::PushFront { value: room_id } - )); - all_known_rooms.push_front(new_room); - } - VectorDiff::PushBack { value: new_room } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PushBack"); } - let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; - let room_id = new_room.room_id.clone(); - add_new_room(&new_room, &room_list_service, true).await?; - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::PushBack { value: room_id } - )); - all_known_rooms.push_back(new_room); - } - remove_diff @ VectorDiff::PopFront => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PopFront"); } - if let Some(room) = all_known_rooms.pop_front() { - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopFront)); - optimize_remove_then_add_into_update( - remove_diff, - &room, - &mut peekable_diffs, - &mut all_known_rooms, - &room_list_service, - ¤t_user_id, - ).await?; - } - } - remove_diff @ VectorDiff::PopBack => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff PopBack"); } - if let Some(room) = all_known_rooms.pop_back() { - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopBack)); - optimize_remove_then_add_into_update( - remove_diff, - &room, - &mut peekable_diffs, - &mut all_known_rooms, - &room_list_service, - ¤t_user_id, - ).await?; - } - } - VectorDiff::Insert { index, value: new_room } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Insert at {index}"); } - let new_room = RoomListServiceRoomInfo::from_room(new_room.into_inner(), ¤t_user_id).await; - let room_id = new_room.room_id.clone(); - add_new_room(&new_room, &room_list_service, true).await?; - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Insert { index, value: room_id } - )); - all_known_rooms.insert(index, new_room); - } - VectorDiff::Set { index, value: changed_room } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Set at {index}"); } - let changed_room = RoomListServiceRoomInfo::from_room(changed_room.into_inner(), ¤t_user_id).await; - if let Some(old_room) = all_known_rooms.get(index) { - update_room(old_room, &changed_room, &room_list_service).await?; - } else { - error!("BUG: room list diff: Set index {index} was out of bounds."); - } - // Send order update (room ID at this index may have changed) - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Set { index, value: changed_room.room_id.clone() } - )); - all_known_rooms.set(index, changed_room); - } - remove_diff @ VectorDiff::Remove { index } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Remove at {index}"); } - if index < all_known_rooms.len() { - let room = all_known_rooms.remove(index); - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Remove { index })); - optimize_remove_then_add_into_update( - remove_diff, - &room, - &mut peekable_diffs, - &mut all_known_rooms, - &room_list_service, - ¤t_user_id, - ).await?; - } else { - error!("BUG: room_list: diff Remove index {index} out of bounds, len {}", all_known_rooms.len()); - } - } - VectorDiff::Truncate { length } => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Truncate to {length}"); } - // Iterate manually so we can know which rooms are being removed. - while all_known_rooms.len() > length { - if let Some(room) = all_known_rooms.pop_back() { - remove_room(&room); - } - } - all_known_rooms.truncate(length); // sanity check - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Truncate { length } - )); - } - } - } - } - - bail!("room list service sync loop ended unexpectedly") -} - - -/// Attempts to optimize a common RoomListService operation of remove + add. -/// -/// If a `Remove` diff (or `PopBack` or `PopFront`) is immediately followed by -/// an `Insert` diff (or `PushFront` or `PushBack`) for the same room, -/// we can treat it as a simple `Set` operation, in which we call `update_room()`. -/// This is much more efficient than removing the room and then adding it back. -/// -/// This tends to happen frequently in order to change the room's state -/// or to "sort" the room list by changing its positional order. -async fn optimize_remove_then_add_into_update( - remove_diff: VectorDiff, - room: &RoomListServiceRoomInfo, - peekable_diffs: &mut Peekable>>, - all_known_rooms: &mut Vector, - room_list_service: &RoomListService, - current_user_id: &Option, -) -> Result<()> { - let next_diff_was_handled: bool; - match peekable_diffs.peek() { - Some(VectorDiff::Insert { index: insert_index, value: new_room }) - if room.room_id == new_room.room_id() => - { - if LOG_ROOM_LIST_DIFFS { - log!("Optimizing {remove_diff:?} + Insert({insert_index}) into Update for room {}", room.room_id); - } - let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; - update_room(room, &new_room, room_list_service).await?; - // Send order update for the insert - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Insert { index: *insert_index, value: new_room.room_id.clone() } - )); - all_known_rooms.insert(*insert_index, new_room); - next_diff_was_handled = true; - } - Some(VectorDiff::PushFront { value: new_room }) - if room.room_id == new_room.room_id() => - { - if LOG_ROOM_LIST_DIFFS { - log!("Optimizing {remove_diff:?} + PushFront into Update for room {}", room.room_id); - } - let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; - update_room(room, &new_room, room_list_service).await?; - // Send order update for the push front - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::PushFront { value: new_room.room_id.clone() } - )); - all_known_rooms.push_front(new_room); - next_diff_was_handled = true; - } - Some(VectorDiff::PushBack { value: new_room }) - if room.room_id == new_room.room_id() => - { - if LOG_ROOM_LIST_DIFFS { - log!("Optimizing {remove_diff:?} + PushBack into Update for room {}", room.room_id); - } - let new_room = RoomListServiceRoomInfo::from_room_ref(new_room.deref(), current_user_id).await; - update_room(room, &new_room, room_list_service).await?; - // Send order update for the push back - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::PushBack { value: new_room.room_id.clone() } - )); - all_known_rooms.push_back(new_room); - next_diff_was_handled = true; - } - _ => next_diff_was_handled = false, - } - if next_diff_was_handled { - peekable_diffs.next(); // consume the next diff - } else { - remove_room(room); - } - Ok(()) -} - - -/// Invoked when the room list service has received an update that changes an existing room. -async fn update_room( - old_room: &RoomListServiceRoomInfo, - new_room: &RoomListServiceRoomInfo, - room_list_service: &RoomListService, -) -> Result<()> { - let new_room_id = new_room.room_id.clone(); - if old_room.room_id == new_room_id { - // Handle state transitions for a room. - if LOG_ROOM_LIST_DIFFS { - log!("Room {:?} ({new_room_id}) state went from {:?} --> {:?}", new_room.display_name, old_room.state, new_room.state); - } - if old_room.state != new_room.state { - match new_room.state { - RoomState::Banned => { - // TODO: handle rooms that this user has been banned from. - log!("Removing Banned room: {:?} ({new_room_id})", new_room.display_name); - remove_room(new_room); - return Ok(()); - } - RoomState::Left => { - log!("Removing Left room: {:?} ({new_room_id})", new_room.display_name); - // TODO: instead of removing this, we could optionally add it to - // a separate list of left rooms, which would be collapsed by default. - // Upon clicking a left room, we could show a splash page - // that prompts the user to rejoin the room or forget it permanently. - // Currently, we just remove it and do not show left rooms at all. - remove_room(new_room); - return Ok(()); - } - RoomState::Joined => { - log!("update_room(): adding new Joined room: {:?} ({new_room_id})", new_room.display_name); - return add_new_room(new_room, room_list_service, true).await; - } - RoomState::Invited => { - log!("update_room(): adding new Invited room: {:?} ({new_room_id})", new_room.display_name); - return add_new_room(new_room, room_list_service, true).await; - } - RoomState::Knocked => { - // TODO: handle Knocked rooms (e.g., can you re-knock? or cancel a prior knock?) - return Ok(()); - } - } - } - - // First, we check for changes to room data that is relevant to any room, - // including joined, invited, and other rooms. - // This includes the room name and room avatar. - if old_room.room_avatar != new_room.room_avatar { - log!("Updating room avatar for room {}", new_room_id); - spawn_fetch_room_avatar(new_room); - } - if old_room.display_name != new_room.display_name { - log!("Updating room {} name: {:?} --> {:?}", new_room_id, old_room.display_name, new_room.display_name); - - enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomName { - new_room_name: (new_room.display_name.clone(), new_room_id.clone()).into(), - }); - } - - // Then, we check for changes to room data that is only relevant to joined rooms: - // including the latest event, tags, unread counts, is_direct, tombstoned state, power levels, etc. - // Invited or left rooms don't care about these details. - if matches!(new_room.state, RoomState::Joined) { - // For some reason, the latest event API does not reliably catch *all* changes - // to the latest event in a given room, such as redactions. - // Thus, we have to re-obtain the latest event on *every* update, regardless of timestamp. - // - let update_latest = match (old_room.latest_event_timestamp, new_room.room.latest_event_timestamp()) { - (Some(old_ts), Some(new_ts)) => new_ts >= old_ts, - (None, Some(_)) => true, - _ => false, - }; - if update_latest { - update_latest_event(&new_room.room).await; - } - - - if old_room.tags != new_room.tags { - log!("Updating room {} tags from {:?} to {:?}", new_room_id, old_room.tags, new_room.tags); - enqueue_rooms_list_update(RoomsListUpdate::Tags { - room_id: new_room_id.clone(), - new_tags: new_room.tags.clone().unwrap_or_default(), - }); - } - - if old_room.is_marked_unread != new_room.is_marked_unread - || old_room.num_unread_messages != new_room.num_unread_messages - || old_room.num_unread_mentions != new_room.num_unread_mentions - { - log!("Updating room {}, marked unread {} --> {}, unread messages {} --> {}, unread mentions {} --> {}", - new_room_id, - old_room.is_marked_unread, new_room.is_marked_unread, - old_room.num_unread_messages, new_room.num_unread_messages, - old_room.num_unread_mentions, new_room.num_unread_mentions, - ); - enqueue_rooms_list_update(RoomsListUpdate::UpdateNumUnreadMessages { - room_id: new_room_id.clone(), - is_marked_unread: new_room.is_marked_unread, - unread_messages: UnreadMessageCount::Known(new_room.num_unread_messages), - unread_mentions: new_room.num_unread_mentions, - }); - } - - if old_room.is_direct != new_room.is_direct { - log!("Updating room {} is_direct from {} to {}", - new_room_id, - old_room.is_direct, - new_room.is_direct, - ); - enqueue_rooms_list_update(RoomsListUpdate::UpdateIsDirect { - room_id: new_room_id.clone(), - is_direct: new_room.is_direct, - }); - } - - let mut __timeline_update_sender_opt = None; - let mut get_timeline_update_sender = |room_id| { - if __timeline_update_sender_opt.is_none() { - if let Some(jrd) = ALL_JOINED_ROOMS.lock().unwrap().get(room_id) { - __timeline_update_sender_opt = Some(jrd.main_timeline.timeline_update_sender.clone()); - } - } - __timeline_update_sender_opt.clone() - }; - - if !old_room.is_tombstoned && new_room.is_tombstoned { - let successor_room = new_room.room.successor_room(); - log!("Updating room {new_room_id} to be tombstoned, {successor_room:?}"); - enqueue_rooms_list_update(RoomsListUpdate::TombstonedRoom { room_id: new_room_id.clone() }); - if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { - spawn_fetch_successor_room_preview( - room_list_service.client().clone(), - successor_room, - new_room_id.clone(), - timeline_update_sender, - ); - } else { - error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); - } - } - - if let Some(nupl) = new_room.user_power_levels - && old_room.user_power_levels.is_none_or(|oupl| oupl != nupl) - { - if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { - log!("Updating room {new_room_id} user power levels."); - match timeline_update_sender.send(TimelineUpdate::UserPowerLevels(nupl)) { - Ok(_) => SignalToUI::set_ui_signal(), - Err(_) => error!("Failed to send the UserPowerLevels update to room {new_room_id}"), - } - } else { - error!("BUG: could not find JoinedRoomDetails for room {new_room_id} where power levels changed."); - } - } - } - Ok(()) - } - else { - warning!("UNTESTED SCENARIO: update_room(): removing old room {}, replacing with new room {}", - old_room.room_id, new_room_id, - ); - remove_room(old_room); - add_new_room(new_room, room_list_service, true).await - } -} - - -/// Invoked when the room list service has received an update to remove an existing room. -fn remove_room(room: &RoomListServiceRoomInfo) { - ALL_JOINED_ROOMS.lock().unwrap().remove(&room.room_id); - enqueue_rooms_list_update( - RoomsListUpdate::RemoveRoom { - room_id: room.room_id.clone(), - new_state: room.state, - } - ); -} - - -/// Invoked when the room list service has received an update with a brand new room. -async fn add_new_room( - new_room: &RoomListServiceRoomInfo, - room_list_service: &RoomListService, - subscribe: bool, -) -> Result<()> { - match new_room.state { - RoomState::Knocked => { - log!("Got new Knocked room: {:?} ({})", new_room.display_name, new_room.room_id); - // Note: here we could optionally display Knocked rooms as a separate type of room - // in the rooms list, but it's not really necessary at this point. - return Ok(()); - } - RoomState::Banned => { - log!("Got new Banned room: {:?} ({})", new_room.display_name, new_room.room_id); - // Note: here we could optionally display Banned rooms as a separate type of room - // in the rooms list, but it's not really necessary at this point. - return Ok(()); - } - RoomState::Left => { - log!("Got new Left room: {:?} ({:?})", new_room.display_name, new_room.room_id); - // Note: here we could optionally display Left rooms as a separate type of room - // in the rooms list, but it's not really necessary at this point. - return Ok(()); - } - RoomState::Invited => { - let invite_details = new_room.room.invite_details().await.ok(); - let room_name_id = RoomNameId::from((new_room.display_name.clone(), new_room.room_id.clone())); - // Start with a basic text avatar; the avatar image will be fetched asynchronously below. - let room_avatar = avatar_from_room_name(room_name_id.name_for_avatar()); - let inviter_info = if let Some(inviter) = invite_details.and_then(|d| d.inviter) { - Some(InviterInfo { - user_id: inviter.user_id().to_owned(), - display_name: inviter.display_name().map(|n| n.to_string()), - avatar: inviter - .avatar(AVATAR_THUMBNAIL_FORMAT.into()) - .await - .ok() - .flatten() - .map(Into::into), - }) - } else { - None - }; - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(InvitedRoomInfo { - room_name_id: room_name_id.clone(), - inviter_info, - room_avatar, - canonical_alias: new_room.room.canonical_alias(), - alt_aliases: new_room.room.alt_aliases(), - // we don't actually display the latest event for Invited rooms, so don't bother. - latest: None, - invite_state: Default::default(), - is_selected: false, - is_direct: new_room.is_direct, - })); - Cx::post_action(AppStateAction::RoomLoadedSuccessfully { - room_name_id, - is_invite: true, - }); - spawn_fetch_room_avatar(new_room); - return Ok(()); - } - RoomState::Joined => { } // Fall through to adding the joined room below. - } - - // If we didn't already subscribe to this room, do so now. - // This ensures we will properly receive all of its states and latest event. - if subscribe { - room_list_service.subscribe_to_rooms(&[&new_room.room_id]).await; - } - - let timeline = Arc::new( - new_room.room.timeline_builder() - .with_focus(TimelineFocus::Live { - // we show threads as separate timelines in their own RoomScreen - hide_threaded_events: true, - }) - .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) - .build() - .await - .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {}: {e}", new_room.room_id))?, - ); - let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); - - let (request_sender, request_receiver) = watch::channel(Vec::new()); - let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler( - new_room.room.clone(), - timeline.clone(), - timeline_update_sender.clone(), - request_receiver, - None, - )); - - // We need to add the room to the `ALL_JOINED_ROOMS` list before we can send - // an `AddJoinedRoom` update to the RoomsList widget, because that widget might - // immediately issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`. - log!("Adding new joined room {}, name: {:?}", new_room.room_id, new_room.display_name); - ALL_JOINED_ROOMS.lock().unwrap().insert( - new_room.room_id.clone(), - JoinedRoomDetails { - room_id: new_room.room_id.clone(), - main_timeline: PerTimelineDetails { - timeline, - timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)), - timeline_update_sender, - timeline_subscriber_handler_task, - }, - thread_timelines: HashMap::new(), - pending_thread_timelines: HashSet::new(), - typing_notice_subscriber: None, - pinned_events_subscriber: None, - }, - ); - - let latest = get_latest_event_details( - &new_room.room.latest_event().await, - room_list_service.client(), - ).await; - let room_name_id = RoomNameId::from((new_room.display_name.clone(), new_room.room_id.clone())); - // Start with a basic text avatar; the avatar image will be fetched asynchronously below. - let room_avatar = avatar_from_room_name(room_name_id.name_for_avatar()); - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddJoinedRoom(JoinedRoomInfo { - latest, - tags: new_room.tags.clone().unwrap_or_default(), - num_unread_messages: new_room.num_unread_messages, - num_unread_mentions: new_room.num_unread_mentions, - is_marked_unread: new_room.is_marked_unread, - room_avatar, - room_name_id: room_name_id.clone(), - canonical_alias: new_room.room.canonical_alias(), - alt_aliases: new_room.room.alt_aliases(), - has_been_paginated: false, - is_selected: false, - is_direct: new_room.is_direct, - is_tombstoned: new_room.is_tombstoned, - })); - - Cx::post_action(AppStateAction::RoomLoadedSuccessfully { - room_name_id, - is_invite: false, - }); - spawn_fetch_room_avatar(new_room); - Ok(()) -} - -#[allow(unused)] -async fn current_ignore_user_list(client: &Client) -> Option> { - use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent; - let ignored_users = client.account() - .account_data::() - .await - .ok()?? - .deserialize() - .ok()? - .ignored_users - .into_keys() - .collect(); - - Some(ignored_users) -} - -fn handle_ignore_user_list_subscriber(client: Client) { - let mut subscriber = client.subscribe_to_ignore_user_list_changes(); - log!("Initial ignored-user list is: {:?}", subscriber.get()); - Handle::current().spawn(async move { - let mut first_update = true; - while let Some(ignore_list) = subscriber.next().await { - log!("Received an updated ignored-user list: {ignore_list:?}"); - let ignored_users_new = ignore_list - .into_iter() - .filter_map(|u| OwnedUserId::try_from(u).ok()) - .collect::>(); - - // TODO: when we support persistent state, don't forget to update `IGNORED_USERS` upon app boot. - let mut ignored_users_old = IGNORED_USERS.lock().unwrap(); - let has_changed = *ignored_users_old != ignored_users_new; - *ignored_users_old = ignored_users_new; - - if has_changed && !first_update { - // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. - // Therefore, we need to re-fetch all timelines for all rooms, - // and currently the only way to actually accomplish this is via pagination. - // See: - for joined_room in client.joined_rooms() { - submit_async_request(MatrixRequest::PaginateTimeline { - timeline_kind: TimelineKind::MainRoom { - room_id: joined_room.room_id().to_owned(), - }, - num_events: 50, - direction: PaginationDirection::Backwards, - }); - } - } - - first_update = false; - } - }); -} - -/// Asynchronously loads and restores the app state from persistent storage for the given user. -/// -/// If the loaded dock state contains open rooms and dock items, this function emits an action -/// to instruct the UI to restore the app state for the main home view (all rooms). -/// If loading fails, it shows a popup notification with the error message. -fn handle_load_app_state(user_id: OwnedUserId) { - Handle::current().spawn(async move { - match load_app_state(&user_id).await { - Ok(app_state) => { - if !app_state.saved_dock_state_home.open_rooms.is_empty() - && !app_state.saved_dock_state_home.dock_items.is_empty() - { - log!("Loaded room panel state from app data directory. Restoring now..."); - Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(app_state)); - } - } - Err(_e) => { - log!("Failed to restore dock layout from persistent state: {_e}"); - enqueue_popup_notification( - "Could not restore the previous dock layout.", - PopupKind::Error, - None, - ); - } - } - }); -} - -/// Returns `true` if the given sync service error is due to an invalid/expired access token. -fn is_invalid_token_error(e: &sync_service::Error) -> bool { - use matrix_sdk::ruma::api::client::error::ErrorKind; - let sdk_error = match e { - sync_service::Error::RoomList( - matrix_sdk_ui::room_list_service::Error::SlidingSync(err) - ) => err, - sync_service::Error::EncryptionSync( - encryption_sync_service::Error::SlidingSync(err) - ) => err, - _ => return false, - }; - matches!( - sdk_error.client_api_error_kind(), - Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) - ) -} - -/// Subscribes to session change notifications from the Matrix client. -/// -/// When the homeserver rejects the access token with a 401 `M_UNKNOWN_TOKEN` error -/// (e.g., the token was revoked or expired), this emits a [`LoginAction::LoginFailure`] -/// so the user is prompted to log in again. -fn handle_session_changes(client: Client) { - let mut receiver = client.subscribe_to_session_changes(); - Handle::current().spawn(async move { - loop { - match receiver.recv().await { - Ok(SessionChange::UnknownToken { soft_logout }) => { - let msg = if soft_logout { - "Your login session has expired.\n\nPlease log in again." - } else { - "Your login token is no longer valid.\n\nPlease log in again." - }; - error!("Session token is no longer valid (soft_logout: {soft_logout}). Prompting re-login."); - Cx::post_action(LoginAction::LoginFailure(msg.to_string())); - } - Ok(SessionChange::TokensRefreshed) => {} - Err(broadcast::error::RecvError::Lagged(n)) => { - warning!("Session change receiver lagged, missed {n} messages."); - } - Err(broadcast::error::RecvError::Closed) => { - break; - } - } - } - }); -} - -fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { - log!("Initial sync service state is {:?}", subscriber.get()); - Handle::current().spawn(async move { - while let Some(state) = subscriber.next().await { - log!("Received a sync service state update: {state:?}"); - match state { - sync_service::State::Error(e) => { - if is_invalid_token_error(&e) { - // The access token is invalid; `handle_session_changes` will have - // already posted a LoginAction::LoginFailure, so just log here. - error!("Sync service stopped due to invalid/expired access token: {e}."); - } else { - log!("Restarting sync service due to error: {e}."); - if let Some(ss) = get_sync_service() { - ss.start().await; - } else { - enqueue_popup_notification( - "Unable to restart the Matrix sync service.\n\nPlease quit and restart Robrix.", - PopupKind::Error, - None, - ); - } - } - } - other => Cx::post_action(RoomsListHeaderAction::StateUpdate(other)), - } - } - }); -} - -fn handle_sync_indicator_subscriber(sync_service: &SyncService) { - /// Duration for sync indicator delay before showing - const SYNC_INDICATOR_DELAY: Duration = Duration::from_millis(100); - /// Duration for sync indicator delay before hiding - const SYNC_INDICATOR_HIDE_DELAY: Duration = Duration::from_millis(200); - let sync_indicator_stream = sync_service.room_list_service() - .sync_indicator( - SYNC_INDICATOR_DELAY, - SYNC_INDICATOR_HIDE_DELAY - ); - - Handle::current().spawn(async move { - let mut sync_indicator_stream = std::pin::pin!(sync_indicator_stream); - - while let Some(indicator) = sync_indicator_stream.next().await { - let is_syncing = match indicator { - SyncIndicator::Show => true, - SyncIndicator::Hide => false, - }; - Cx::post_action(RoomsListHeaderAction::SetSyncStatus(is_syncing)); - } - }); -} - -fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { - log!("Initial room list loading state is {:?}", loading_state.get()); - Handle::current().spawn(async move { - while let Some(state) = loading_state.next().await { - log!("Received a room list loading state update: {state:?}"); - match state { - RoomListLoadingState::NotLoaded => { - enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); - } - RoomListLoadingState::Loaded { maximum_number_of_rooms } => { - enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { max_rooms: maximum_number_of_rooms }); - // The SDK docs state that we cannot move from the `Loaded` state - // back to the `NotLoaded` state, so we can safely exit this task here. - return; - } - } - } - }); -} - -/// Spawns an async task to fetch the RoomPreview for the given successor room. -/// -/// After the fetch completes, this emites a [`RoomPreviewAction`] -/// containing the fetched room preview or an error if it failed. -fn spawn_fetch_successor_room_preview( - client: Client, - successor_room: Option, - tombstoned_room_id: OwnedRoomId, - timeline_update_sender: crossbeam_channel::Sender, -) { - Handle::current().spawn(async move { - log!("Updating room {tombstoned_room_id} to be tombstoned, {successor_room:?}"); - let srd = if let Some(SuccessorRoom { room_id, reason }) = successor_room { - match fetch_room_preview_with_avatar( - &client, - room_id.deref().into(), - Vec::new(), - ).await { - Ok(room_preview) => SuccessorRoomDetails::Full { room_preview, reason }, - Err(e) => { - log!("Failed to fetch preview of successor room {room_id}, error: {e:?}"); - SuccessorRoomDetails::Basic(SuccessorRoom { room_id, reason }) - } - } - } else { - log!("BUG: room {tombstoned_room_id} was tombstoned but had no successor room!"); - SuccessorRoomDetails::None - }; - - match timeline_update_sender.send(TimelineUpdate::Tombstoned(srd)) { - Ok(_) => SignalToUI::set_ui_signal(), - Err(_) => error!("Failed to send the Tombstoned update to room {tombstoned_room_id}"), - } - }); -} - -/// Fetches the full preview information for the given `room`. -/// Also fetches that room preview's avatar, if it had an avatar URL. -async fn fetch_room_preview_with_avatar( - client: &Client, - room: &RoomOrAliasId, - via: Vec, -) -> Result { - let room_preview = client.get_room_preview(room, via).await?; - // If this room has an avatar URL, fetch it. - let room_avatar = if let Some(avatar_url) = room_preview.avatar_url.clone() { - let media_request = MediaRequestParameters { - source: MediaSource::Plain(avatar_url), - format: AVATAR_THUMBNAIL_FORMAT.into(), - }; - match client.media().get_media_content(&media_request, true).await { - Ok(avatar_content) => { - log!("Fetched avatar for room preview {:?} ({})", room_preview.name, room_preview.room_id); - FetchedRoomAvatar::Image(avatar_content.into()) - } - Err(e) => { - log!("Failed to fetch avatar for room preview {:?} ({}), error: {e:?}", - room_preview.name, room_preview.room_id - ); - avatar_from_room_name(room_preview.name.as_deref()) - } - } - } else { - // The successor room did not have an avatar URL - avatar_from_room_name(room_preview.name.as_deref()) - }; - Ok(FetchedRoomPreview::from(room_preview, room_avatar)) -} - -/// Fetches key details about the given thread root event. -/// -/// Returns a tuple of: -/// 1. the number of replies in the thread (excluding the root event itself), -/// 2. the latest reply event, if it could be fetched. -async fn fetch_thread_summary_details( - room: &Room, - thread_root_event_id: &EventId, -) -> (u32, Option) { - let mut num_replies = 0; - let mut latest_reply_event = None; - - if let Ok(thread_root_event) = room.load_or_fetch_event(thread_root_event_id, None).await - && let Some(thread_summary) = thread_root_event.thread_summary.summary() - { - num_replies = thread_summary.num_replies; - if let Some(latest_reply_event_id) = thread_summary.latest_reply.as_ref() - && let Ok(latest_reply) = room.load_or_fetch_event(latest_reply_event_id, None).await - { - latest_reply_event = Some(latest_reply); - } - } - - // Always compute the reply count directly from the fetched thread relations, - // for some reason we can't rely on the SDK-provided thread_summary to be accurate - // (it's almost always totally wrong or out-of-date...). - let count_replies_future = count_thread_replies(room, thread_root_event_id); - - // Fetch the latest reply event and count the thread replies in parallel. - let (fetched_latest_reply_opt, reply_count_opt) = if latest_reply_event.is_none() { - tokio::join!( - fetch_latest_thread_reply_event(room, thread_root_event_id), - count_replies_future, - ) - } else { - (None, count_replies_future.await) - }; - - if let Some(event) = fetched_latest_reply_opt { - latest_reply_event = Some(event); - } - if let Some(count) = reply_count_opt { - num_replies = count; - } - (num_replies, latest_reply_event) -} - -/// Fetches the latest reply event in the thread rooted at `thread_root_event_id`. -async fn fetch_latest_thread_reply_event( - room: &Room, - thread_root_event_id: &EventId, -) -> Option { - let options = RelationsOptions { - dir: Direction::Backward, - limit: Some(uint!(1)), - include_relations: IncludeRelations::RelationsOfType(RelationType::Thread), - ..Default::default() - }; - - room.relations(thread_root_event_id.to_owned(), options) - .await - .ok() - .and_then(|relations| relations.chunk.into_iter().next()) -} - -/// Counts all replies in the given thread by paginating `/relations` in batches. -async fn count_thread_replies( - room: &Room, - thread_root_event_id: &EventId, -) -> Option { - let mut total_replies: u32 = 0; - let mut next_batch_token = None; - - loop { - let options = RelationsOptions { - from: next_batch_token.clone(), - dir: Direction::Backward, - limit: Some(uint!(100)), - include_relations: IncludeRelations::RelationsOfType(RelationType::Thread), - ..Default::default() - }; - - let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; - if relations.chunk.is_empty() { - break; - } - total_replies = total_replies.saturating_add(relations.chunk.len() as u32); - - next_batch_token = relations.next_batch_token; - if next_batch_token.is_none() { - break; - } - } - - Some(total_replies) -} - -/// Returns an HTML-formatted text preview of the given latest thread reply event. -async fn text_preview_of_latest_thread_reply( - room: &Room, - latest_reply_event: &matrix_sdk::deserialized_responses::TimelineEvent, -) -> Option { - let raw = latest_reply_event.raw(); - let sender_id = raw.get_field::("sender").ok().flatten()?; - let sender_room_member = match room.get_member_no_sync(&sender_id).await { - Ok(Some(rm)) => Some(rm), - _ => room.get_member(&sender_id).await.ok().flatten(), - }; - let sender_name = sender_room_member.as_ref() - .and_then(|rm| rm.display_name()) - .unwrap_or(sender_id.as_str()); - let text_preview = text_preview_of_raw_timeline_event(raw, sender_name).unwrap_or_else(|| { - let event_type = raw.get_field::("type").ok().flatten(); - TextPreview::from(( - event_type.unwrap_or_else(|| "unknown event type".to_string()), - BeforeText::UsernameWithColon, - )) - }); - let preview_str = text_preview.format_with(sender_name, true); - match utils::replace_linebreaks_separators(&preview_str, true) { - Cow::Borrowed(_) => Some(preview_str), - Cow::Owned(replaced) => Some(replaced), - } -} - - -/// Returns the timestamp and an HTML-formatted text preview of the given `latest_event`. -/// -/// If the sender profile of the event is not yet available, this function will -/// generate a preview using the sender's user ID instead of their display name. -async fn get_latest_event_details( - latest_event_value: &LatestEventValue, - client: &Client, -) -> Option<(MilliSecondsSinceUnixEpoch, String)> { - macro_rules! get_sender_username { - ($profile:expr, $sender:expr, $is_own:expr) => {{ - let sender_username_opt = if let TimelineDetails::Ready(profile) = $profile { - profile.display_name.clone() - } else if $is_own { - client.account().get_display_name().await.ok().flatten() - } else { - None - }; - sender_username_opt.unwrap_or_else(|| $sender.to_string()) - }}; - } - - match latest_event_value { - LatestEventValue::None => None, - LatestEventValue::Remote { timestamp, sender, is_own, profile, content } => { - let sender_username = get_sender_username!(profile, sender, *is_own); - let latest_message_text = text_preview_of_timeline_item( - content, - sender, - &sender_username, - ).format_with(&sender_username, true); - Some((*timestamp, latest_message_text)) - } - LatestEventValue::Local { timestamp, sender, profile, content, state: _ } => { - // TODO: use the `state` enum to augment the preview text with more details. - // Example: "Sending... {msg}" or - // "Failed to send {msg}" - let is_own = current_user_id().is_some_and(|id| &id == sender); - let sender_username = get_sender_username!(profile, sender, is_own); - let latest_message_text = text_preview_of_timeline_item( - content, - sender, - &sender_username, - ).format_with(&sender_username, true); - Some((*timestamp, latest_message_text)) - } - } -} - -/// Handles the given updated latest event for the given room. -/// -/// This function sends a `RoomsListUpdate::UpdateLatestEvent` -/// to update the latest event in the RoomsListEntry for the given room. -async fn update_latest_event(room: &Room) { - if let Some((timestamp, latest_message_text)) = get_latest_event_details( - &room.latest_event().await, - &room.client(), - ).await { - enqueue_rooms_list_update(RoomsListUpdate::UpdateLatestEvent { - room_id: room.room_id().to_owned(), - timestamp, - latest_message_text, - }); - } -} - -/// A request to search backwards for a specific event in a room's timeline. -pub struct BackwardsPaginateUntilEventRequest { - pub room_id: OwnedRoomId, - pub target_event_id: OwnedEventId, - /// The index in the timeline where a backwards search should begin. - pub starting_index: usize, - /// The number of items in the timeline at the time of the request, - /// which is used to detect if the timeline has changed since the request was made, - /// meaning that the `starting_index` can no longer be relied upon. - pub current_tl_len: usize, -} - -/// Whether to enable verbose logging of all timeline diff updates. -const LOG_TIMELINE_DIFFS: bool = cfg!(feature = "log_timeline_diffs"); -/// Whether to enable verbose logging of all room list service diff updates. -const LOG_ROOM_LIST_DIFFS: bool = cfg!(feature = "log_room_list_diffs"); - -/// A per-timeline async task that listens for timeline updates and sends them to the UI thread. -/// -/// One instance of this async task is spawned for each room the client knows about, -/// and also one for each thread that the user opens in a thread view. -async fn timeline_subscriber_handler( - room: Room, - timeline: Arc, - timeline_update_sender: crossbeam_channel::Sender, - mut request_receiver: watch::Receiver>, - thread_root_event_id: Option, -) { - - /// An inner function that searches the given new timeline items for a target event. - /// - /// If the target event is found, it is removed from the `target_event_id_opt` and returned, - /// along with the index/position of that event in the given iterator of new items. - fn find_target_event<'a>( - target_event_id_opt: &mut Option, - mut new_items_iter: impl Iterator>, - ) -> Option<(usize, OwnedEventId)> { - let found_index = target_event_id_opt - .as_ref() - .and_then(|target_event_id| new_items_iter - .position(|new_item| new_item - .as_event() - .is_some_and(|new_ev| new_ev.event_id() == Some(target_event_id)) - ) - ); - - if let Some(index) = found_index { - target_event_id_opt.take().map(|ev| (index, ev)) - } else { - None - } - } - - - let room_id = room.room_id().to_owned(); - log!("Starting timeline subscriber for room {room_id}, thread {thread_root_event_id:?}..."); - let (mut timeline_items, mut subscriber) = timeline.subscribe().await; - log!("Received initial timeline update of {} items for room {room_id}, thread {thread_root_event_id:?}.", timeline_items.len()); - - timeline_update_sender.send(TimelineUpdate::FirstUpdate { - initial_items: timeline_items.clone(), - }).unwrap_or_else( - |_e| panic!("Error: timeline update sender couldn't send first update ({} items) to room {room_id}, thread {thread_root_event_id:?}...!", timeline_items.len()) - ); - - // the event ID to search for while loading previous items into the timeline. - let mut target_event_id = None; - // the timeline index and event ID of the target event, if it has been found. - let mut found_target_event_id: Option<(usize, OwnedEventId)> = None; - - loop { tokio::select! { - // we should check for new requests before handling new timeline updates, - // because the request might influence how we handle a timeline update. - biased; - - // Handle updates to the current backwards pagination requests. - Ok(()) = request_receiver.changed() => { - let prev_target_event_id = target_event_id.clone(); - let new_request_details = request_receiver - .borrow_and_update() - .iter() - .find_map(|req| req.room_id - .eq(&room_id) - .then(|| (req.target_event_id.clone(), req.starting_index, req.current_tl_len)) - ); - - target_event_id = new_request_details.as_ref().map(|(ev, ..)| ev.clone()); - - // If we received a new request, start searching backwards for the target event. - if let Some((new_target_event_id, starting_index, current_tl_len)) = new_request_details { - if prev_target_event_id.as_ref() != Some(&new_target_event_id) { - let starting_index = if current_tl_len == timeline_items.len() { - starting_index - } else { - // The timeline has changed since the request was made, so we can't rely on the `starting_index`. - // Instead, we have no choice but to start from the end of the timeline. - timeline_items.len() - }; - // log!("Received new request to search for event {new_target_event_id} in room {room_id}, thread {thread_root_event_id:?} starting from index {starting_index} (tl len {}).", timeline_items.len()); - // Search backwards for the target event in the timeline, starting from the given index. - if let Some(target_event_tl_index) = timeline_items - .focus() - .narrow(..starting_index) - .into_iter() - .rev() - .position(|i| i.as_event() - .and_then(|e| e.event_id()) - .is_some_and(|ev_id| ev_id == new_target_event_id) - ) - .map(|i| starting_index.saturating_sub(i).saturating_sub(1)) - { - // log!("Found existing target event {new_target_event_id} in room {room_id}, thread {thread_root_event_id:?} at index {target_event_tl_index}."); - - // Nice! We found the target event in the current timeline items, - // so there's no need to actually proceed with backwards pagination; - // thus, we can clear the locally-tracked target event ID. - target_event_id = None; - found_target_event_id = None; - timeline_update_sender.send( - TimelineUpdate::TargetEventFound { - target_event_id: new_target_event_id.clone(), - index: target_event_tl_index, - } - ).unwrap_or_else( - |_e| panic!("Error: timeline update sender couldn't send TargetEventFound({new_target_event_id}, {target_event_tl_index}) to room {room_id}, thread {thread_root_event_id:?}!") - ); - // Send a Makepad-level signal to update this room's timeline UI view. - SignalToUI::set_ui_signal(); - } - else { - log!("Target event not in timeline. Starting backwards pagination \ - in room {room_id}, thread {thread_root_event_id:?} to find target event \ - {new_target_event_id} starting from index {starting_index}.", - ); - // If we didn't find the target event in the current timeline items, - // we need to start loading previous items into the timeline. - submit_async_request(MatrixRequest::PaginateTimeline { - timeline_kind: if let Some(thread_root_event_id) = thread_root_event_id.clone() { - TimelineKind::Thread { - room_id: room_id.clone(), - thread_root_event_id, - } - } else { - TimelineKind::MainRoom { - room_id: room_id.clone(), - } - }, - num_events: 50, - direction: PaginationDirection::Backwards, - }); - } - } - } - } - - // Handle updates to the actual timeline content. - batch_opt = subscriber.next() => { - let Some(batch) = batch_opt else { break }; - let mut num_updates = 0; - let mut index_of_first_change = usize::MAX; - let mut index_of_last_change = usize::MIN; - // whether to clear the entire cache of drawn items - let mut clear_cache = false; - // whether the changes include items being appended to the end of the timeline - let mut is_append = false; - for diff in batch { - num_updates += 1; - match diff { - VectorDiff::Append { values } => { - let _values_len = values.len(); - index_of_first_change = min(index_of_first_change, timeline_items.len()); - timeline_items.extend(values); - index_of_last_change = max(index_of_last_change, timeline_items.len()); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Append {_values_len}. Changes: {index_of_first_change}..{index_of_last_change}"); } - is_append = true; - } - VectorDiff::Clear => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Clear"); } - clear_cache = true; - timeline_items.clear(); - } - VectorDiff::PushFront { value } => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PushFront"); } - if let Some((index, _ev)) = found_target_event_id.as_mut() { - *index += 1; // account for this new `value` being prepended. - } else { - found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)); - } - - clear_cache = true; - timeline_items.push_front(value); - } - VectorDiff::PushBack { value } => { - index_of_first_change = min(index_of_first_change, timeline_items.len()); - timeline_items.push_back(value); - index_of_last_change = max(index_of_last_change, timeline_items.len()); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PushBack. Changes: {index_of_first_change}..{index_of_last_change}"); } - is_append = true; - } - VectorDiff::PopFront => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopFront"); } - clear_cache = true; - timeline_items.pop_front(); - if let Some((i, _ev)) = found_target_event_id.as_mut() { - *i = i.saturating_sub(1); // account for the first item being removed. - } - // This doesn't affect whether we should reobtain the latest event. - } - VectorDiff::PopBack => { - timeline_items.pop_back(); - index_of_first_change = min(index_of_first_change, timeline_items.len()); - index_of_last_change = usize::MAX; - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopBack. Changes: {index_of_first_change}..{index_of_last_change}"); } - } - VectorDiff::Insert { index, value } => { - if index == 0 { - clear_cache = true; - } else { - index_of_first_change = min(index_of_first_change, index); - index_of_last_change = usize::MAX; - } - if index >= timeline_items.len() { - is_append = true; - } - - if let Some((i, _ev)) = found_target_event_id.as_mut() { - // account for this new `value` being inserted before the previously-found target event's index. - if index <= *i { - *i += 1; - } - } else { - found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) - .map(|(i, ev)| (i + index, ev)); - } - - timeline_items.insert(index, value); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Insert at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - } - VectorDiff::Set { index, value } => { - index_of_first_change = min(index_of_first_change, index); - index_of_last_change = max(index_of_last_change, index.saturating_add(1)); - timeline_items.set(index, value); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Set at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - } - VectorDiff::Remove { index } => { - if index == 0 { - clear_cache = true; - } else { - index_of_first_change = min(index_of_first_change, index.saturating_sub(1)); - index_of_last_change = usize::MAX; - } - if let Some((i, _ev)) = found_target_event_id.as_mut() { - // account for an item being removed before the previously-found target event's index. - if index <= *i { - *i = i.saturating_sub(1); - } - } - timeline_items.remove(index); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Remove at {index}. Changes: {index_of_first_change}..{index_of_last_change}"); } - } - VectorDiff::Truncate { length } => { - if length == 0 { - clear_cache = true; - } else { - index_of_first_change = min(index_of_first_change, length.saturating_sub(1)); - index_of_last_change = usize::MAX; - } - timeline_items.truncate(length); - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Truncate to length {length}. Changes: {index_of_first_change}..{index_of_last_change}"); } - } - VectorDiff::Reset { values } => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff Reset, new length {}", values.len()); } - clear_cache = true; // we must assume all items have changed. - timeline_items = values; - } - } - } - - - if num_updates > 0 { - // Handle the case where back pagination inserts items at the beginning of the timeline - // (meaning the entire timeline needs to be re-drawn), - // but there is a virtual event at index 0 (e.g., a day divider). - // When that happens, we want the RoomScreen to treat this as if *all* events changed. - if index_of_first_change == 1 && timeline_items.front().and_then(|item| item.as_virtual()).is_some() { - index_of_first_change = 0; - clear_cache = true; - } - - let changed_indices = index_of_first_change..index_of_last_change; - - if LOG_TIMELINE_DIFFS { - log!("timeline_subscriber: applied {num_updates} updates for room {room_id}, thread {thread_root_event_id:?}, timeline now has {} items. is_append? {is_append}, clear_cache? {clear_cache}. Changes: {changed_indices:?}.", timeline_items.len()); - } - timeline_update_sender.send(TimelineUpdate::NewItems { - new_items: timeline_items.clone(), - changed_indices, - clear_cache, - is_append, - }).expect("Error: timeline update sender couldn't send update with new items!"); - - // We must send this update *after* the actual NewItems update, - // otherwise the UI thread (RoomScreen) won't be able to correctly locate the target event. - if let Some((index, found_event_id)) = found_target_event_id.take() { - target_event_id = None; - timeline_update_sender.send( - TimelineUpdate::TargetEventFound { - target_event_id: found_event_id.clone(), - index, - } - ).unwrap_or_else( - |_e| panic!("Error: timeline update sender couldn't send TargetEventFound({found_event_id}, {index}) to room {room_id}, thread {thread_root_event_id:?}!") - ); - } - - // Send a Makepad-level signal to update this room's timeline UI view. - SignalToUI::set_ui_signal(); - } - } - - else => { - break; - } - } } - - error!("Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}."); -} - -/// Spawn a new async task to fetch the room's new avatar. -fn spawn_fetch_room_avatar(room: &RoomListServiceRoomInfo) { - let room_id = room.room_id.clone(); - let room_name_id = RoomNameId::from((room.display_name.clone(), room.room_id.clone())); - let inner_room = room.room.clone(); - Handle::current().spawn(async move { - let room_avatar = room_avatar(&inner_room, &room_name_id).await; - rooms_list::enqueue_rooms_list_update(RoomsListUpdate::UpdateRoomAvatar { - room_id, - room_avatar, - }); - }); -} - -/// Fetches and returns the avatar image for the given room (if one exists), -/// otherwise returns a text avatar string of the first character of the room name. -async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvatar { - match room.avatar(AVATAR_THUMBNAIL_FORMAT.into()).await { - Ok(Some(avatar)) => FetchedRoomAvatar::Image(avatar.into()), - _ => { - if let Ok(room_members) = room.members(RoomMemberships::ACTIVE).await { - if room_members.len() == 2 { - if let Some(non_account_member) = room_members.iter().find(|m| !m.is_account_user()) { - if let Ok(Some(avatar)) = non_account_member.avatar(AVATAR_THUMBNAIL_FORMAT.into()).await { - return FetchedRoomAvatar::Image(avatar.into()); - } - } - } - } - utils::avatar_from_room_name(room_name_id.name_for_avatar()) - } - } -} - -/// Spawn an async task to login to the given Matrix homeserver using the given SSO identity provider ID. -/// -/// This function will post a `LoginAction::SsoPending(true)` to the main thread, and another -/// `LoginAction::SsoPending(false)` once the async task has either successfully logged in or -/// failed to do so. -/// -/// If the login attempt is successful, the resulting `Client` and `ClientSession` will be sent -/// to the login screen using the `login_sender`. -async fn spawn_sso_server( - brand: String, - homeserver_url: String, - identity_provider_id: String, - login_sender: Sender, -) { - Cx::post_action(LoginAction::SsoPending(true)); - // Post a status update to inform the user that we're waiting for the client to be built. - Cx::post_action(LoginAction::Status { - title: "Initializing client...".into(), - status: "Please wait while Matrix builds and configures the client object for login.".into(), - }); - - // Wait for the notification that the client has been built - DEFAULT_SSO_CLIENT_NOTIFIER.notified().await; - - // Try to use the DEFAULT_SSO_CLIENT, if it was successfully built. - // We do not clone it because a Client cannot be re-used again - // once it has been used for a login attempt, so this forces us to create a new one - // if that occurs. - let client_and_session_opt = DEFAULT_SSO_CLIENT.lock().unwrap().take(); - - Handle::current().spawn(async move { - // Try to use the DEFAULT_SSO_CLIENT that we proactively created - // during initialization (to speed up opening the SSO browser window). - let mut client_and_session = client_and_session_opt; - - // If the DEFAULT_SSO_CLIENT is none (meaning it failed to build), - // or if the homeserver_url is *not* empty and isn't the default, - // we cannot use the DEFAULT_SSO_CLIENT, so we must build a new one. - let mut build_client_error = None; - if client_and_session.is_none() || ( - !homeserver_url.is_empty() - && homeserver_url != "matrix.org" - && Url::parse(&homeserver_url) != Url::parse("https://matrix-client.matrix.org/") - && Url::parse(&homeserver_url) != Url::parse("https://matrix.org/") - ) { - match build_client( - &Cli { - homeserver: homeserver_url.is_empty().not().then_some(homeserver_url), - ..Default::default() - }, - app_data_dir(), - ).await { - Ok(success) => client_and_session = Some(success), - Err(e) => build_client_error = Some(e), - } - } - - let Some((client, client_session)) = client_and_session else { - Cx::post_action(LoginAction::LoginFailure( - if let Some(err) = build_client_error { - format!("Could not create client object. Please try to login again.\n\nError: {err}") - } else { - String::from("Could not create client object. Please try to login again.") - } - )); - // This ensures that the called to `DEFAULT_SSO_CLIENT_NOTIFIER.notified()` - // at the top of this function will not block upon the next login attempt. - DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); - Cx::post_action(LoginAction::SsoPending(false)); - return; - }; - - let mut is_logged_in = false; - Cx::post_action(LoginAction::Status { - title: "Opening your browser...".into(), - status: "Please finish logging in using your browser, and then come back to Robrix.".into(), - }); - match client - .matrix_auth() - .login_sso(|sso_url: String| async move { - let url = Url::parse(&sso_url)?; - for (key, value) in url.query_pairs() { - if key == "redirectUrl" { - let redirect_url = Url::parse(&value)?; - Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); - break - } - } - Uri::new(&sso_url).open().map_err(|err| - Error::Io(io::Error::other(format!("Unable to open SSO login url. Error: {:?}", err))) - ) - }) - .identity_provider_id(&identity_provider_id) - .initial_device_display_name(&format!("robrix-sso-{brand}")) - .await - .inspect(|_| { - if let Some(client) = get_client() { - if client.matrix_auth().logged_in() { - is_logged_in = true; - log!("Already logged in, ignore login with sso"); - } - } - }) { - Ok(identity_provider_res) => { - if !is_logged_in { - if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session)).await { - error!("Error sending login request to login_sender: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(String::from( - "BUG: failed to send login request to matrix worker thread." - ))); - } - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!( - "Logged in as {:?}.\n → Loading rooms...", - &identity_provider_res.user_id - ), - }); - } - } - Err(e) => { - if !is_logged_in { - error!("SSO Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("SSO login failed: {e}"))); - } - } - } - - // This ensures that the called to `DEFAULT_SSO_CLIENT_NOTIFIER.notified()` - // at the top of this function will not block upon the next login attempt. - DEFAULT_SSO_CLIENT_NOTIFIER.notify_one(); - Cx::post_action(LoginAction::SsoPending(false)); - }); -} - - -bitflags! { - /// The powers that a user has in a given room. - #[derive(Copy, Clone, PartialEq, Eq)] - pub struct UserPowerLevels: u64 { - const Ban = 1 << 0; - const Invite = 1 << 1; - const Kick = 1 << 2; - const Redact = 1 << 3; - const NotifyRoom = 1 << 4; - // ------------------------------------- - // -- Copied from TimelineEventType ---- - // -- Unused powers are commented out -- - // ------------------------------------- - // const CallAnswer = 1 << 5; - // const CallInvite = 1 << 6; - // const CallHangup = 1 << 7; - // const CallCandidates = 1 << 8; - // const CallNegotiate = 1 << 9; - // const CallReject = 1 << 10; - // const CallSdpStreamMetadataChanged = 1 << 11; - // const CallSelectAnswer = 1 << 12; - // const KeyVerificationReady = 1 << 13; - // const KeyVerificationStart = 1 << 14; - // const KeyVerificationCancel = 1 << 15; - // const KeyVerificationAccept = 1 << 16; - // const KeyVerificationKey = 1 << 17; - // const KeyVerificationMac = 1 << 18; - // const KeyVerificationDone = 1 << 19; - const Location = 1 << 20; - const Message = 1 << 21; - // const PollStart = 1 << 22; - // const UnstablePollStart = 1 << 23; - // const PollResponse = 1 << 24; - // const UnstablePollResponse = 1 << 25; - // const PollEnd = 1 << 26; - // const UnstablePollEnd = 1 << 27; - // const Beacon = 1 << 28; - const Reaction = 1 << 29; - // const RoomEncrypted = 1 << 30; - const RoomMessage = 1 << 31; - const RoomRedaction = 1 << 32; - const Sticker = 1 << 33; - // const CallNotify = 1 << 34; - // const PolicyRuleRoom = 1 << 35; - // const PolicyRuleServer = 1 << 36; - // const PolicyRuleUser = 1 << 37; - // const RoomAliases = 1 << 38; - // const RoomAvatar = 1 << 39; - // const RoomCanonicalAlias = 1 << 40; - // const RoomCreate = 1 << 41; - // const RoomEncryption = 1 << 42; - // const RoomGuestAccess = 1 << 43; - // const RoomHistoryVisibility = 1 << 44; - // const RoomJoinRules = 1 << 45; - // const RoomMember = 1 << 46; - // const RoomName = 1 << 47; - const RoomPinnedEvents = 1 << 48; - // const RoomPowerLevels = 1 << 49; - // const RoomServerAcl = 1 << 50; - // const RoomThirdPartyInvite = 1 << 51; - // const RoomTombstone = 1 << 52; - // const RoomTopic = 1 << 53; - // const SpaceChild = 1 << 54; - // const SpaceParent = 1 << 55; - // const BeaconInfo = 1 << 56; - // const CallMember = 1 << 57; - // const MemberHints = 1 << 58; - } -} -impl UserPowerLevels { - pub fn from(power_levels: &RoomPowerLevels, user_id: &UserId) -> Self { - let mut retval = UserPowerLevels::empty(); - let user_power = power_levels.for_user(user_id); - retval.set(UserPowerLevels::Ban, user_power >= power_levels.ban); - retval.set(UserPowerLevels::Invite, user_power >= power_levels.invite); - retval.set(UserPowerLevels::Kick, user_power >= power_levels.kick); - retval.set(UserPowerLevels::Redact, user_power >= power_levels.redact); - retval.set(UserPowerLevels::NotifyRoom, user_power >= power_levels.notifications.room); - retval.set(UserPowerLevels::Location, user_power >= power_levels.for_message(MessageLikeEventType::Location)); - retval.set(UserPowerLevels::Message, user_power >= power_levels.for_message(MessageLikeEventType::Message)); - retval.set(UserPowerLevels::Reaction, user_power >= power_levels.for_message(MessageLikeEventType::Reaction)); - retval.set(UserPowerLevels::RoomMessage, user_power >= power_levels.for_message(MessageLikeEventType::RoomMessage)); - retval.set(UserPowerLevels::RoomRedaction, user_power >= power_levels.for_message(MessageLikeEventType::RoomRedaction)); - retval.set(UserPowerLevels::Sticker, user_power >= power_levels.for_message(MessageLikeEventType::Sticker)); - retval.set(UserPowerLevels::RoomPinnedEvents, user_power >= power_levels.for_state(StateEventType::RoomPinnedEvents)); - retval - } - - pub async fn from_room(room: &Room, user_id: &UserId) -> Option { - let room_power_levels = room.power_levels().await.ok()?; - Some(UserPowerLevels::from(&room_power_levels, user_id)) - } - - pub fn can_ban(self) -> bool { - self.contains(UserPowerLevels::Ban) - } - - pub fn can_unban(self) -> bool { - self.can_ban() && self.can_kick() - } - - pub fn can_invite(self) -> bool { - self.contains(UserPowerLevels::Invite) - } - - pub fn can_kick(self) -> bool { - self.contains(UserPowerLevels::Kick) - } - - pub fn can_redact(self) -> bool { - self.contains(UserPowerLevels::Redact) - } - - pub fn can_notify_room(self) -> bool { - self.contains(UserPowerLevels::NotifyRoom) - } - - pub fn can_redact_own(self) -> bool { - self.contains(UserPowerLevels::RoomRedaction) - } - - pub fn can_redact_others(self) -> bool { - self.can_redact_own() && self.contains(UserPowerLevels::Redact) - } - - pub fn can_send_location(self) -> bool { - self.contains(UserPowerLevels::Location) - } - - pub fn can_send_message(self) -> bool { - self.contains(UserPowerLevels::RoomMessage) - || self.contains(UserPowerLevels::Message) - } - - pub fn can_send_reaction(self) -> bool { - self.contains(UserPowerLevels::Reaction) - } - - pub fn can_send_sticker(self) -> bool { - self.contains(UserPowerLevels::Sticker) - } - - #[doc(alias("unpin"))] - pub fn can_pin(self) -> bool { - self.contains(UserPowerLevels::RoomPinnedEvents) - } -} - - -/// Shuts down the current Tokio runtime completely and takes ownership to ensure proper cleanup. -pub fn shutdown_background_tasks() { - if let Some(runtime) = TOKIO_RUNTIME.lock().unwrap().take() { - runtime.shutdown_background(); - } -} - -pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { - // Clear resources normally, allowing them to be properly dropped - // This prevents memory leaks when users logout and login again without closing the app - CLIENT.lock().unwrap().take(); - SYNC_SERVICE.lock().unwrap().take(); - REQUEST_SENDER.lock().unwrap().take(); - IGNORED_USERS.lock().unwrap().clear(); - ALL_JOINED_ROOMS.lock().unwrap().clear(); - - let on_clear_appstate = Arc::new(Notify::new()); - Cx::post_action(LogoutAction::ClearAppState { on_clear_appstate: on_clear_appstate.clone() }); - - match tokio::time::timeout(config.app_state_cleanup_timeout, on_clear_appstate.notified()).await { - Ok(_) => { - log!("Received signal that UI-side app state was cleaned successfully"); - Ok(()) - } - Err(_) => Err(anyhow!("Timed out waiting for UI-side app state cleanup")), - } -} From 7847f2569b6eb8205b4e879188b02df590ecf7f6 Mon Sep 17 00:00:00 2001 From: Kevin Boos <1139460+kevinaboos@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:43:51 -0700 Subject: [PATCH 12/18] Delete patch.diff --- patch.diff | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 patch.diff diff --git a/patch.diff b/patch.diff deleted file mode 100644 index 77a3c0cb..00000000 --- a/patch.diff +++ /dev/null @@ -1,28 +0,0 @@ ---- src/sliding_sync.rs -+++ src/sliding_sync.rs -@@ -1452,17 +1452,13 @@ - let _typing_notices_task = Handle::current().spawn(async move { - while let Ok(user_ids) = typing_notice_receiver.recv().await { - // log!("Received typing notifications for room {room_id}: {user_ids:?}"); -- let mut users = Vec::with_capacity(user_ids.len()); -- for user_id in user_ids { -- users.push( -- main_timeline.room() -- .get_member_no_sync(&user_id) -- .await -- .ok() -- .flatten() -- .and_then(|m| m.display_name().map(|d| d.to_owned())) -- .unwrap_or_else(|| user_id.to_string()) -- ); -- } -+ let users = join_all(user_ids.into_iter().map(|user_id| { -+ let room = main_timeline.room().clone(); -+ async move { -+ room.get_member_no_sync(&user_id).await.ok().flatten() -+ .and_then(|m| m.display_name().map(|d| d.to_owned())) -+ .unwrap_or_else(|| user_id.to_string()) -+ } -+ })).await; - if let Err(e) = timeline_update_sender.send(TimelineUpdate::TypingUsers { users }) { - error!("Error: timeline update sender couldn't send the list of typing users: {e:?}"); From 43cc02ed478e3e7c52105757ee7fbf47ebb55cef Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Wed, 25 Mar 2026 14:50:06 -0700 Subject: [PATCH 13/18] properly parallelize fetching the list of typing users' display names --- src/sliding_sync.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 51242f66..1744639a 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1453,9 +1453,10 @@ async fn matrix_worker_task( while let Ok(user_ids) = typing_notice_receiver.recv().await { // log!("Received typing notifications for room {room_id}: {user_ids:?}"); let users = join_all(user_ids.into_iter().map(|user_id| { - let room = main_timeline.room().clone(); + let tl = main_timeline.clone(); async move { - room.get_member_no_sync(&user_id).await.ok().flatten() + tl.room().get_member_no_sync(&user_id).await + .ok().flatten() .and_then(|m| m.display_name().map(|d| d.to_owned())) .unwrap_or_else(|| user_id.to_string()) } From ebbfcfc289ca4bb5acc35fc1ec920082ce898761 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 26 Mar 2026 15:46:13 +0800 Subject: [PATCH 14/18] attachment_upload_2.0 --- Cargo.lock | 599 ++++++++++++++++++++++++++++- Cargo.toml | 7 + resources/icons/add_attachment.svg | 3 + resources/icons/file.svg | 7 + src/app.rs | 64 ++- src/home/mod.rs | 2 + src/home/room_screen.rs | 40 ++ src/home/upload_progress.rs | 277 +++++++++++++ src/image_utils.rs | 104 +++++ src/lib.rs | 1 + src/room/room_input_bar.rs | 215 ++++++++++- src/shared/file_upload_modal.rs | 338 ++++++++++++++++ src/shared/mod.rs | 4 + src/shared/progress_bar.rs | 106 +++++ src/shared/styles.rs | 2 + src/sliding_sync.rs | 109 ++++++ src/utils.rs | 19 + 17 files changed, 1878 insertions(+), 19 deletions(-) create mode 100644 resources/icons/add_attachment.svg create mode 100644 resources/icons/file.svg create mode 100644 src/home/upload_progress.rs create mode 100644 src/image_utils.rs create mode 100644 src/shared/file_upload_modal.rs create mode 100644 src/shared/progress_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 5e87a838..9c1ea240 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,6 +239,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend 0.3.14", + "wayland-client 0.31.13", + "wayland-protocols 0.32.11", + "zbus", +] + [[package]] name = "askar-crypto" version = "0.3.7" @@ -334,6 +356,18 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -359,6 +393,49 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-lock" version = "3.4.1" @@ -370,12 +447,52 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-once-cell" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-rx" version = "0.1.3" @@ -386,6 +503,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -408,6 +543,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -691,6 +832,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bls12_381" version = "0.8.0" @@ -725,6 +879,12 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "bytemuck" version = "1.25.0" @@ -741,6 +901,12 @@ name = "byteorder" version = "1.5.0" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -1459,7 +1625,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -1469,6 +1635,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", + "libc", "objc2", ] @@ -1483,6 +1651,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1592,6 +1769,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1605,7 +1809,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -1710,6 +1914,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ff" version = "0.13.1" @@ -2532,6 +2745,21 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + [[package]] name = "imbl" version = "6.1.0" @@ -2800,7 +3028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -3156,9 +3384,9 @@ dependencies = [ "napi-ohos", "ohos-sys", "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", - "wayland-client", + "wayland-client 0.31.12", "wayland-egl", - "wayland-protocols", + "wayland-protocols 0.32.10", "windows 0.62.2", "windows-core 0.62.2", "windows-targets 0.52.6", @@ -3268,7 +3496,7 @@ name = "makepad-zune-inflate" version = "0.2.0" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ - "simd-adler32", + "simd-adler32 0.3.8 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", ] [[package]] @@ -3680,6 +3908,15 @@ name = "memchr" version = "2.7.6" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -3692,6 +3929,16 @@ version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase 2.8.1", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3705,6 +3952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -3718,6 +3966,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multihash" version = "0.19.3" @@ -3949,6 +4207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", "objc2", "objc2-foundation", ] @@ -4077,6 +4336,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "p256" version = "0.13.2" @@ -4246,6 +4515,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -4273,6 +4553,39 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.1", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "poly1305" version = "0.8.0" @@ -4435,6 +4748,21 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quinn" version = "0.11.9" @@ -4580,6 +4908,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "readlock" version = "0.1.9" @@ -4722,6 +5056,30 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.17.14" @@ -4838,6 +5196,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "htmlize", + "image", "imbl", "imghdr", "indexmap 2.13.0", @@ -4847,11 +5206,14 @@ dependencies = [ "matrix-sdk", "matrix-sdk-base", "matrix-sdk-ui", + "mime", + "mime_guess", "percent-encoding", "quinn", "rand 0.8.5", "rangemap", "reqwest", + "rfd", "robius-directories", "robius-location", "robius-open", @@ -5101,7 +5463,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -5175,7 +5537,7 @@ version = "0.18.0" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", - "bytemuck", + "bytemuck 1.25.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", "makepad-error-log", "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", "ttf-parser", @@ -5448,6 +5810,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_spanned" version = "1.0.3" @@ -5568,6 +5941,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "simd-adler32" version = "0.3.8" @@ -5949,7 +6328,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -6405,6 +6784,17 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.1", +] + [[package]] name = "ulid" version = "1.2.1" @@ -6778,7 +7168,21 @@ dependencies = [ "libc", "scoped-tls", "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-sys", + "wayland-sys 0.31.8", +] + +[[package]] +name = "wayland-backend" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-sys 0.31.10", ] [[package]] @@ -6788,7 +7192,19 @@ source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvement dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", - "wayland-backend", + "wayland-backend 0.3.12", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustix", + "wayland-backend 0.3.14", + "wayland-scanner", ] [[package]] @@ -6796,8 +7212,8 @@ name = "wayland-egl" version = "0.32.9" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ - "wayland-backend", - "wayland-sys", + "wayland-backend 0.3.12", + "wayland-sys 0.31.8", ] [[package]] @@ -6806,8 +7222,31 @@ version = "0.32.10" source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-backend", - "wayland-client", + "wayland-backend 0.3.12", + "wayland-client 0.31.12", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-backend 0.3.14", + "wayland-client 0.31.13", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", ] [[package]] @@ -6819,6 +7258,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wayland-sys" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.84" @@ -6883,7 +7333,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.1", ] [[package]] @@ -7484,6 +7934,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.1", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -7583,3 +8094,59 @@ name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 8bd24357..14d33b36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,9 @@ hashbrown = { version = "0.16", features = ["raw-entry"] } htmlize = "1.0.5" indexmap = "2.6.0" imghdr = "0.7.0" +image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } +mime = "0.3" +mime_guess = "2.0" linkify = "0.10.0" matrix-sdk-base = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main" } matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main", default-features = false, features = [ @@ -103,6 +106,10 @@ reqwest = { version = "0.12", default-features = false, features = [ "macos-system-configuration", ] } +# Desktop-only file dialog (doesn't work on iOS/Android) +[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies] +rfd = "0.15" + [features] default = [] diff --git a/resources/icons/add_attachment.svg b/resources/icons/add_attachment.svg new file mode 100644 index 00000000..523461c6 --- /dev/null +++ b/resources/icons/add_attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/file.svg b/resources/icons/file.svg new file mode 100644 index 00000000..2b0852bf --- /dev/null +++ b/resources/icons/file.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/app.rs b/src/app.rs index b5df23ff..e8ef510a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,14 +4,15 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; +use makepad_widgets::SignalToUI; use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ - event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update} + event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt}, main_desktop_ui::MainDesktopUiAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, new_message_context_menu::NewMessageContextMenuWidgetRefExt, room_context_menu::RoomContextMenuWidgetRefExt, room_screen::{InviteAction, MessageAction, TimelineUpdate, 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::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::user_profile_cache::clear_user_profile_cache, room::BasicRoomDetails, shared::{confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, get_timeline_update_sender, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -148,6 +149,15 @@ script_mod! { } } + // A modal to preview and confirm file uploads. + file_upload_modal := Modal { + content +: { + width: Fit, height: Fit, + align: Align{x: 0.5, y: 0.5}, + file_upload_modal_inner := FileUploadModal {} + } + } + PopupList {} // Tooltips must be shown in front of all other UI elements, @@ -308,6 +318,36 @@ impl MatchEvent for App { continue; } + // Handle file upload modal actions + match action.downcast_ref() { + Some(FilePreviewerAction::Show(file_data)) => { + self.ui.file_upload_modal(cx, ids!(file_upload_modal_inner)) + .set_file_data(cx, file_data.clone()); + self.ui.modal(cx, ids!(file_upload_modal)).open(cx); + continue; + } + Some(FilePreviewerAction::Hide) => { + self.ui.modal(cx, ids!(file_upload_modal)).close(cx); + continue; + } + Some(FilePreviewerAction::UploadConfirmed(file_data)) => { + // Send the file upload request to the currently selected room + if let Some(selected_room) = &self.app_state.selected_room { + if let Some(timeline_kind) = selected_room.timeline_kind() { + if let Some(sender) = get_timeline_update_sender(&timeline_kind) { + let _ = sender.send(TimelineUpdate::FileUploadConfirmed(file_data.clone())); + SignalToUI::set_ui_signal(); + } + } + } + continue; + } + Some(FilePreviewerAction::Cancelled) => { + continue; + } + _ => {} + } + // Handle an action requesting to open the new message context menu. if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); @@ -925,6 +965,26 @@ impl SelectedRoom { SelectedRoom::Thread { room_name_id, .. } => format!("[Thread] {room_name_id}"), } } + + /// Returns the `TimelineKind` for this selected room. + /// + /// Returns `None` for `InvitedRoom` and `Space` variants, as they don't have timelines. + pub fn timeline_kind(&self) -> Option { + match self { + SelectedRoom::JoinedRoom { room_name_id } => { + Some(crate::sliding_sync::TimelineKind::MainRoom { + room_id: room_name_id.room_id().clone(), + }) + } + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + Some(crate::sliding_sync::TimelineKind::Thread { + room_id: room_name_id.room_id().clone(), + thread_root_event_id: thread_root_event_id.clone(), + }) + } + SelectedRoom::InvitedRoom { .. } | SelectedRoom::Space { .. } => None, + } + } } impl PartialEq for SelectedRoom { diff --git a/src/home/mod.rs b/src/home/mod.rs index 23a1de96..b96ee099 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -29,6 +29,7 @@ pub mod new_message_context_menu; pub mod room_context_menu; pub mod link_preview; pub mod room_image_viewer; +pub mod upload_progress; pub fn script_mod(vm: &mut ScriptVm) { search_messages::script_mod(vm); @@ -58,6 +59,7 @@ pub fn script_mod(vm: &mut ScriptVm) { main_desktop_ui::script_mod(vm); spaces_bar::script_mod(vm); navigation_tab_bar::script_mod(vm); + upload_progress::script_mod(vm); // Keep HomeScreen last, it references many widgets registered above. home_screen::script_mod(vm); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced..af6a817a 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1586,6 +1586,32 @@ impl RoomScreen { tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} + TimelineUpdate::FileUploadConfirmed(file_data) => { + // Show upload progress view and start the upload + self.view.room_input_bar(cx, ids!(room_input_bar)) + .show_upload_progress(cx, &file_data.name); + + // Submit the upload request + submit_async_request(MatrixRequest::SendAttachment { + timeline_kind: tl.kind.clone(), + file_data, + replied_to: None, // TODO: support replying with attachments + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + } + TimelineUpdate::FileUploadUpdate { current, total } => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .set_upload_progress(cx, current, total); + } + TimelineUpdate::FileUploadAbortHandle(handle) => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .set_upload_abort_handle(handle); + } + TimelineUpdate::FileUploadError { error, file_data } => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .show_upload_error(cx, &error, file_data); + } } } @@ -2738,6 +2764,20 @@ pub enum TimelineUpdate { Tombstoned(SuccessorRoomDetails), /// A notice that link preview data for a URL has been fetched and is now available. LinkPreviewFetched, + /// User confirmed a file upload via the file upload modal. + FileUploadConfirmed(crate::shared::file_upload_modal::FileData), + /// Progress update for an ongoing file upload. + FileUploadUpdate { + current: u64, + total: u64, + }, + /// The abort handle for an in-progress file upload. + FileUploadAbortHandle(tokio::task::AbortHandle), + /// An error occurred during file upload. + FileUploadError { + error: String, + file_data: crate::shared::file_upload_modal::FileData, + }, } thread_local! { diff --git a/src/home/upload_progress.rs b/src/home/upload_progress.rs new file mode 100644 index 00000000..6d59b906 --- /dev/null +++ b/src/home/upload_progress.rs @@ -0,0 +1,277 @@ +//! A widget that displays upload progress with a progress bar, status label, +//! and cancel/retry buttons. + +use makepad_widgets::*; +use tokio::task::AbortHandle; + +use crate::shared::file_upload_modal::FileData; +use crate::shared::progress_bar::ProgressBarWidgetRefExt; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.UploadProgressView = set_type_default() do #(UploadProgressView::register_widget(vm)) { + visible: false, + width: Fill, + height: Fit, + flow: Down, + padding: 10, + spacing: 8, + + show_bg: true, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + border_radius: 4.0 + } + + // Header with file name and cancel button + header := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + uploading_label := Label { + width: Fit, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (COLOR_TEXT) + } + text: "Uploading: " + } + + file_name_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (COLOR_TEXT) + } + text: "" + } + + cancel_button := RobrixNeutralIconButton { + width: 24, height: 24, + padding: 4, + draw_icon +: { svg: (ICON_CLOSE) } + icon_walk: Walk{width: 14, height: 14} + text: "" + } + } + + // Progress bar + progress_bar := ProgressBar { + width: Fill, + height: 6, + } + + // Status/error area + status_view := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + status_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + + retry_button := RobrixPositiveIconButton { + visible: false, + padding: Inset{top: 4, bottom: 4, left: 8, right: 8} + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9 }, + } + text: "Retry" + } + } + } +} + +/// The current state of the upload view. +#[derive(Clone, Debug, Default)] +pub enum UploadViewState { + /// Normal state - upload in progress or ready. + #[default] + Normal, + /// Error state - upload failed. + Error { + message: String, + file_data: FileData, + }, +} + +/// Actions emitted by the UploadProgressView. +#[derive(Clone, Debug, Default)] +pub enum UploadProgressViewAction { + /// No action. + #[default] + None, + /// User cancelled the upload. + Cancelled, + /// User requested retry of a failed upload. + Retry(FileData), +} + +/// A widget showing upload progress with cancel/retry functionality. +#[derive(Script, ScriptHook, Widget)] +pub struct UploadProgressView { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// Handle to abort the current upload task. + #[rust] abort_handle: Option, + /// Current progress value (0.0 to 1.0). + #[rust] progress: f32, + /// Current state of the upload view. + #[rust] state: UploadViewState, +} + +impl Widget for UploadProgressView { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Event::Actions(actions) = event { + // Handle cancel button + if self.button(cx, ids!(cancel_button)).clicked(actions) { + if let Some(handle) = self.abort_handle.take() { + handle.abort(); + } + cx.action(UploadProgressViewAction::Cancelled); + self.hide(cx); + } + + // Handle retry button + if self.button(cx, ids!(retry_button)).clicked(actions) { + if let UploadViewState::Error { file_data, .. } = &self.state { + let file_data = file_data.clone(); + cx.action(UploadProgressViewAction::Retry(file_data)); + self.hide(cx); + } + } + } + + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl UploadProgressView { + /// Shows the upload progress view with the given file name. + pub fn show(&mut self, cx: &mut Cx, file_name: &str) { + self.set_visible(cx, true); + self.state = UploadViewState::Normal; + self.progress = 0.0; + + self.label(cx, ids!(file_name_label)).set_text(cx, file_name); + self.label(cx, ids!(status_label)).set_text(cx, "Starting upload..."); + self.button(cx, ids!(retry_button)).set_visible(cx, false); + self.button(cx, ids!(cancel_button)).set_visible(cx, true); + + // Reset progress bar + self.child_by_path(ids!(progress_bar)).as_progress_bar().set_progress(cx, 0.0); + + self.redraw(cx); + } + + /// Hides the upload progress view. + pub fn hide(&mut self, cx: &mut Cx) { + self.set_visible(cx, false); + self.abort_handle = None; + self.state = UploadViewState::Normal; + self.redraw(cx); + } + + /// Updates the progress value. + pub fn set_progress(&mut self, cx: &mut Cx, current: u64, total: u64) { + self.progress = if total > 0 { + (current as f32 / total as f32).clamp(0.0, 1.0) + } else { + 0.0 + }; + + self.child_by_path(ids!(progress_bar)).as_progress_bar() + .set_progress(cx, self.progress); + + // Update status label + let percent = (self.progress * 100.0) as u32; + let status = format!( + "Uploading... {}% ({} / {})", + percent, + crate::utils::format_file_size(current), + crate::utils::format_file_size(total) + ); + self.label(cx, ids!(status_label)).set_text(cx, &status); + + self.redraw(cx); + } + + /// Sets the abort handle for the current upload task. + pub fn set_abort_handle(&mut self, handle: AbortHandle) { + self.abort_handle = Some(handle); + } + + /// Shows an error state with the given message. + pub fn show_error(&mut self, cx: &mut Cx, error: &str, file_data: FileData) { + self.state = UploadViewState::Error { + message: error.to_string(), + file_data, + }; + + // Update UI for error state + self.label(cx, ids!(status_label)) + .set_text(cx, &format!("Error: {}", error)); + self.button(cx, ids!(retry_button)).set_visible(cx, true); + self.button(cx, ids!(cancel_button)).set_visible(cx, true); + + // Set progress bar to error color - no longer apply color change via script_apply_eval + // The progress bar will use the default color for now + + self.redraw(cx); + } +} + +impl UploadProgressViewRef { + /// Shows the upload progress view with the given file name. + pub fn show(&self, cx: &mut Cx, file_name: &str) { + if let Some(mut inner) = self.borrow_mut() { + inner.show(cx, file_name); + } + } + + /// Hides the upload progress view. + pub fn hide(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.hide(cx); + } + } + + /// Updates the progress value. + pub fn set_progress(&self, cx: &mut Cx, current: u64, total: u64) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_progress(cx, current, total); + } + } + + /// Sets the abort handle for the current upload task. + pub fn set_abort_handle(&self, handle: AbortHandle) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_abort_handle(handle); + } + } + + /// Shows an error state with the given message. + pub fn show_error(&self, cx: &mut Cx, error: &str, file_data: FileData) { + if let Some(mut inner) = self.borrow_mut() { + inner.show_error(cx, error, file_data); + } + } +} diff --git a/src/image_utils.rs b/src/image_utils.rs new file mode 100644 index 00000000..3b866dcb --- /dev/null +++ b/src/image_utils.rs @@ -0,0 +1,104 @@ +//! Image processing utilities for thumbnail generation and image manipulation. + +use std::io::Cursor; + +/// The maximum dimension (width or height) for generated thumbnails. +pub const THUMBNAIL_MAX_DIMENSION: u32 = 800; + +/// Generates a thumbnail from the given image data. +/// +/// The thumbnail is scaled to fit within `THUMBNAIL_MAX_DIMENSION` while preserving aspect ratio. +/// Returns the thumbnail as JPEG-encoded bytes, along with the thumbnail's dimensions. +/// +/// # Arguments +/// * `image_data` - The raw bytes of the source image (PNG, JPEG, etc.) +/// +/// # Returns +/// * `Ok((jpeg_bytes, width, height))` - The thumbnail data and dimensions +/// * `Err(String)` - Error message if thumbnail generation fails +pub fn generate_thumbnail(image_data: &[u8]) -> Result<(Vec, u32, u32), String> { + use image::{ImageFormat, ImageReader}; + + // Load the image from bytes + let img = ImageReader::new(Cursor::new(image_data)) + .with_guessed_format() + .map_err(|e| format!("Failed to guess image format: {e}"))? + .decode() + .map_err(|e| format!("Failed to decode image: {e}"))?; + + let (orig_width, orig_height) = (img.width(), img.height()); + + // Calculate thumbnail dimensions while preserving aspect ratio + let (thumb_width, thumb_height) = if orig_width > THUMBNAIL_MAX_DIMENSION + || orig_height > THUMBNAIL_MAX_DIMENSION + { + let ratio = f64::from(orig_width) / f64::from(orig_height); + if orig_width > orig_height { + let new_width = THUMBNAIL_MAX_DIMENSION; + let new_height = (f64::from(new_width) / ratio) as u32; + (new_width, new_height) + } else { + let new_height = THUMBNAIL_MAX_DIMENSION; + let new_width = (f64::from(new_height) * ratio) as u32; + (new_width, new_height) + } + } else { + (orig_width, orig_height) + }; + + // Resize the image using a high-quality filter + let thumbnail = img.resize( + thumb_width, + thumb_height, + image::imageops::FilterType::Lanczos3, + ); + + // Encode as JPEG + let mut jpeg_bytes = Vec::new(); + thumbnail + .write_to(&mut Cursor::new(&mut jpeg_bytes), ImageFormat::Jpeg) + .map_err(|e| format!("Failed to encode thumbnail as JPEG: {e}"))?; + + Ok((jpeg_bytes, thumb_width, thumb_height)) +} + +/// Returns the MIME type string for the given image data by inspecting its header bytes. +/// +/// Returns `None` if the image format cannot be determined. +pub fn detect_mime_type(data: &[u8]) -> Option<&'static str> { + match imghdr::from_bytes(data) { + Some(imghdr::Type::Png) => Some("image/png"), + Some(imghdr::Type::Jpeg) => Some("image/jpeg"), + Some(imghdr::Type::Gif) => Some("image/gif"), + Some(imghdr::Type::Webp) => Some("image/webp"), + Some(imghdr::Type::Bmp) => Some("image/bmp"), + Some(imghdr::Type::Tiff) => Some("image/tiff"), + _ => None, + } +} + +/// Returns true if the given MIME type represents an image format that can be displayed. +pub fn is_displayable_image(mime_type: &str) -> bool { + matches!( + mime_type, + "image/png" + | "image/jpeg" + | "image/jpg" + | "image/gif" + | "image/webp" + | "image/bmp" + ) +} + +/// Gets the dimensions of an image from its raw bytes. +/// +/// Returns `None` if the image cannot be decoded. +pub fn get_image_dimensions(data: &[u8]) -> Option<(u32, u32)> { + use image::ImageReader; + + ImageReader::new(Cursor::new(data)) + .with_guessed_format() + .ok()? + .into_dimensions() + .ok() +} diff --git a/src/lib.rs b/src/lib.rs index 346c0314..750bee2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ pub mod verification; pub mod utils; pub mod temp_storage; pub mod location; +pub mod image_utils; pub const APP_QUALIFIER: &str = "org"; pub const APP_ORGANIZATION: &str = "robius"; diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 93b8d4a9..8058a5d1 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -20,7 +20,7 @@ use makepad_widgets::*; use matrix_sdk::room::reply::{EnforceThread, Reply}; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId}; -use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, RoomScreenProps, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; +use crate::{home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, RoomScreenProps, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::UploadProgressViewWidgetRefExt}, location::init_location_subscriber, shared::{avatar::AvatarWidgetRefExt, file_upload_modal::{FileData, FileLoadedData, FilePreviewerAction, FilePreviewerMetaData, ThumbnailData}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; script_mod! { use mod.prelude.widgets.* @@ -60,6 +60,9 @@ script_mod! { // Below that, display a preview of the current location that a user is about to send. location_preview := LocationPreview { } + // Upload progress view (shown when a file upload is in progress) + upload_progress_view := UploadProgressView { } + // Below that, display one of multiple possible views: // * the message input bar (buttons and message TextInput). // * a notice that the user can't send messages to this room. @@ -80,6 +83,23 @@ script_mod! { align: Align{y: 1.0}, padding: 6, + // Attachment button for uploading files/images + send_attachment_button := RobrixIconButton { + margin: 4 + spacing: 0, + draw_icon +: { + svg: (ICON_ADD_ATTACHMENT) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + } + icon_walk: Walk{width: 21, height: 21} + text: "", + } + location_button := RobrixIconButton { margin: 4 spacing: 0, @@ -169,6 +189,9 @@ pub struct RoomInputBar { #[rust] was_replying_preview_visible: bool, /// Info about the message event that the user is currently replying to, if any. #[rust] replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, + /// The pending file load operation, if any. Contains the receiver channel + /// for receiving the loaded file data from a background thread. + #[rust] pending_file_load: Option, } impl Widget for RoomInputBar { @@ -203,6 +226,36 @@ impl Widget for RoomInputBar { self.handle_actions(cx, actions, room_screen_props); } + // Handle signal events for pending file loads from background threads + if let Event::Signal = event { + if let Some(receiver) = &self.pending_file_load { + let mut remove_receiver = false; + match receiver.try_recv() { + Ok(Some(loaded_data)) => { + // Convert FileLoadedData to FileData for the modal + let file_data = convert_loaded_data_to_file_data(loaded_data); + Cx::post_action(FilePreviewerAction::Show(file_data)); + remove_receiver = true; + } + Ok(None) => { + // File loading failed, hide modal if shown + remove_receiver = true; + } + Err(std::sync::mpsc::TryRecvError::Empty) => { + // Still waiting for data + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + // Channel disconnected + remove_receiver = true; + } + } + if remove_receiver { + self.pending_file_load = None; + self.redraw(cx); + } + } + } + self.view.handle_event(cx, event, scope); } @@ -230,6 +283,12 @@ impl RoomInputBar { self.redraw(cx); } + // Handle the add attachment button being clicked. + if self.button(cx, ids!(send_attachment_button)).clicked(actions) { + log!("Add attachment button clicked; opening file picker..."); + self.open_file_picker(cx); + } + // Handle the add location button being clicked. if self.button(cx, ids!(location_button)).clicked(actions) { log!("Add location button clicked; requesting current location..."); @@ -532,6 +591,101 @@ impl RoomInputBar { fn is_tsp_signing_enabled(&self, cx: &mut Cx) -> bool { self.view.check_box(cx, ids!(tsp_sign_checkbox)).active(cx) } + + /// Opens the native file picker dialog to select a file for upload. + #[cfg(not(any(target_os = "ios", target_os = "android")))] + fn open_file_picker(&mut self, cx: &mut Cx) { + // Run file dialog on main thread (required for non-windowed environments) + let dialog = rfd::FileDialog::new() + .set_title("Select file to upload") + .add_filter("All files", &["*"]) + .add_filter("Images", &["png", "jpg", "jpeg", "gif", "webp", "bmp"]) + .add_filter("Documents", &["pdf", "doc", "docx", "txt", "rtf"]); + + if let Some(selected_file_path) = dialog.pick_file() { + // Get file metadata + let file_size = match std::fs::metadata(&selected_file_path) { + Ok(metadata) => metadata.len(), + Err(e) => { + makepad_widgets::error!("Failed to read file metadata: {e}"); + enqueue_popup_notification( + format!("Unable to access file: {e}"), + PopupKind::Error, + None, + ); + return; + } + }; + + // Check for empty files + if file_size == 0 { + enqueue_popup_notification("Cannot upload empty file", PopupKind::Error, None); + return; + } + + // Detect the MIME type from the file extension + let mime = mime_guess::from_path(&selected_file_path) + .first_or_octet_stream(); + + // Create channel for receiving loaded file data + let (sender, receiver) = std::sync::mpsc::channel(); + self.pending_file_load = Some(receiver); + + // Spawn background thread to generate thumbnail (for images) + let path_clone = selected_file_path.clone(); + let mime_clone = mime.clone(); + cx.spawn_thread(move || { + // Generate thumbnail for images + let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(&mime_clone.to_string()) { + match std::fs::read(&path_clone) { + Ok(data) => { + match crate::image_utils::generate_thumbnail(&data) { + Ok((thumb_data, width, height)) => ( + Some(ThumbnailData { data: thumb_data, width, height }), + Some((width, height)) + ), + Err(e) => { + makepad_widgets::error!("Failed to generate thumbnail: {e}"); + (None, None) + } + } + } + Err(e) => { + makepad_widgets::error!("Failed to read file for thumbnail: {e}"); + (None, None) + } + } + } else { + (None, None) + }; + + let loaded_data = FileLoadedData { + metadata: FilePreviewerMetaData { + mime: mime_clone, + file_size, + file_path: path_clone, + }, + thumbnail, + dimensions, + }; + + if sender.send(Some(loaded_data)).is_err() { + makepad_widgets::error!("Failed to send file data to UI: receiver dropped"); + } + SignalToUI::set_ui_signal(); + }); + } + } + + /// Shows a "not supported" message on mobile platforms. + #[cfg(any(target_os = "ios", target_os = "android"))] + fn open_file_picker(&mut self, _cx: &mut Cx) { + enqueue_popup_notification( + "File uploads are not yet supported on this platform.", + PopupKind::Error, + None, + ); + } } impl RoomInputBarRef { @@ -664,6 +818,46 @@ impl RoomInputBarRef { // This depends on the `EditingPane` state, so it must be done after Step 3. inner.update_tombstone_footer(cx, timeline_kind.room_id(), tombstone_info); } + + /// Shows the upload progress view for a file upload. + pub fn show_upload_progress(&self, cx: &mut Cx, file_name: &str) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show(cx, file_name); + } + + /// Hides the upload progress view. + pub fn hide_upload_progress(&self, cx: &mut Cx) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .hide(cx); + } + + /// Updates the upload progress. + pub fn set_upload_progress(&self, cx: &mut Cx, current: u64, total: u64) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .set_progress(cx, current, total); + } + + /// Sets the abort handle for the current upload. + pub fn set_upload_abort_handle(&self, handle: tokio::task::AbortHandle) { + let Some(inner) = self.borrow_mut() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .set_abort_handle(handle); + } + + /// Shows an upload error with retry option. + pub fn show_upload_error(&self, cx: &mut Cx, error: &str, file_data: FileData) { + let Some(inner) = self.borrow() else { return }; + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show_error(cx, error, file_data); + } } /// The saved UI state of a `RoomInputBar` widget. @@ -691,3 +885,22 @@ enum ShowEditingPaneBehavior { editing_pane_state: EditingPaneState, }, } + +/// Converts `FileLoadedData` from background thread to `FileData` for the modal. +fn convert_loaded_data_to_file_data(loaded: FileLoadedData) -> FileData { + // Read the file data from the path + let data = std::fs::read(&loaded.metadata.file_path).unwrap_or_default(); + let name = loaded.metadata.file_path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + + FileData { + path: loaded.metadata.file_path, + name, + mime_type: loaded.metadata.mime.to_string(), + data, + size: loaded.metadata.file_size, + thumbnail: loaded.thumbnail, + } +} diff --git a/src/shared/file_upload_modal.rs b/src/shared/file_upload_modal.rs new file mode 100644 index 00000000..851b6c7b --- /dev/null +++ b/src/shared/file_upload_modal.rs @@ -0,0 +1,338 @@ +//! A modal dialog for previewing and confirming file uploads. +//! +//! This modal shows a preview of the file (image thumbnail or file icon) +//! along with file metadata and upload/cancel buttons. + +use makepad_widgets::*; +use std::path::PathBuf; + +use crate::utils::format_file_size; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.FileUploadModal = set_type_default() do #(FileUploadModal::register_widget(vm)) { + ..mod.widgets.RoundedView + + width: 400, + height: Fit, + flow: Down, + padding: 20, + spacing: 15, + + show_bg: true, + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 8.0 + shadow_color: #00000044 + shadow_radius: 10.0 + shadow_offset: vec2(0.0, 2.0) + } + + // Header + header := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 0.0, y: 0.5}, + spacing: 10, + + title := Label { + width: Fill, + draw_text +: { + text_style: TITLE_TEXT { font_size: 14 }, + color: (COLOR_TEXT) + } + text: "Upload File" + } + + close_button := RobrixNeutralIconButton { + width: Fit, + height: Fit, + align: Align{x: 1.0, y: 0.0}, + spacing: 0, + margin: Inset{top: 4.5} // vertically align with the title + padding: 15, + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14} + } + } + + // Preview area + preview_container := View { + width: Fill, + height: 200, + flow: Overlay, + align: Align{x: 0.5, y: 0.5}, + + show_bg: true, + draw_bg.color: (COLOR_SECONDARY) + + // Image preview container (visible when file is an image) + image_preview_container := View { + visible: false, + width: Fill, + height: Fill, + align: Align{x: 0.5, y: 0.5}, + // cannot center align for tall images + image_preview := Image { + width: Fill, + height: Fill, + fit: ImageFit.Smallest, + } + } + + // File icon (visible when file is not an image) + file_icon_container := View { + visible: false, + width: Fill, + height: Fill, + align: Align{x: 0.5, y: 0.5}, + flow: Down, + spacing: 10, + + Icon { + width: Fit, height: Fit, + draw_icon +: { + svg: (ICON_FILE) + color: (COLOR_TEXT) + } + icon_walk: Walk{width: 64, height: 64} + } + + file_type_label := Label { + width: Fit, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + } + } + + // File info + file_info := View { + width: Fill, + height: Fit, + flow: Down, + spacing: 5, + + file_name_label := Label { + width: Fill, + flow: Flow.Right{wrap: true}, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11 }, + color: (COLOR_TEXT) + } + text: "" + } + + file_size_label := Label { + width: Fill, + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10 }, + color: (SMALL_STATE_TEXT_COLOR) + } + text: "" + } + } + + // Buttons + buttons := View { + width: Fill, + height: Fit, + flow: Right, + align: Align{x: 1.0, y: 0.5}, + spacing: 10, + + cancel_button := RobrixNeutralIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + text: "Cancel" + } + + upload_button := RobrixPositiveIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + draw_icon +: { svg: (ICON_UPLOAD) } + icon_walk: Walk{width: 16, height: Fit, margin: Inset{right: 4}} + text: "Upload" + } + } + } +} + +/// Data describing a file to be uploaded. +#[derive(Clone, Debug)] +pub struct FileData { + /// The file path on the local filesystem. + pub path: PathBuf, + /// The file name (without directory path). + pub name: String, + /// The MIME type of the file. + pub mime_type: String, + /// The raw file data. + pub data: Vec, + /// The file size in bytes. + pub size: u64, + /// Optional thumbnail data for images (JPEG bytes). + pub thumbnail: Option, +} + +/// Thumbnail data for image files. +#[derive(Clone, Debug)] +pub struct ThumbnailData { + /// The thumbnail image data (JPEG). + pub data: Vec, + /// Width of the thumbnail. + pub width: u32, + /// Height of the thumbnail. + pub height: u32, +} + +/// Metadata for the file previewer (used in background loading). +#[derive(Debug, Clone)] +pub struct FilePreviewerMetaData { + /// MIME type of the file. + pub mime: mime_guess::Mime, + /// File size in bytes. + pub file_size: u64, + /// Path to the original file. + pub file_path: PathBuf, +} + +/// Data loaded from a file by a background thread. +/// This is sent through a channel and combined with additional data to create `FileData`. +#[derive(Debug, Clone)] +pub struct FileLoadedData { + /// Metadata about the file (path, size, MIME type). + pub metadata: FilePreviewerMetaData, + /// Optional thumbnail for image files. + pub thumbnail: Option, + /// Optional dimensions for image/video files, width and height in pixels. + pub dimensions: Option<(u32, u32)>, +} + +/// Type alias for the receiver that gets loaded file data from a background thread. +pub type FileLoadReceiver = std::sync::mpsc::Receiver>; + +/// Actions emitted by the FileUploadModal. +#[derive(Clone, Debug, Default)] +pub enum FilePreviewerAction { + /// No action. + #[default] + None, + /// Show the file upload modal with the given file data. + Show(FileData), + /// Hide the file upload modal. + Hide, + /// User confirmed the upload. + UploadConfirmed(FileData), + /// User cancelled the upload. + Cancelled, +} + +/// A modal for previewing and confirming file uploads. +#[derive(Script, ScriptHook, Widget)] +pub struct FileUploadModal { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// The current file data being previewed. + #[rust] file_data: Option, +} + +impl Widget for FileUploadModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Event::Actions(actions) = event { + // Handle close button + if self.button(cx, ids!(close_button)).clicked(actions) + || self.button(cx, ids!(cancel_button)).clicked(actions) + { + Cx::post_action(FilePreviewerAction::Cancelled); + Cx::post_action(FilePreviewerAction::Hide); + } + + // Handle upload button + if self.button(cx, ids!(upload_button)).clicked(actions) { + if let Some(file_data) = self.file_data.take() { + Cx::post_action(FilePreviewerAction::UploadConfirmed(file_data)); + Cx::post_action(FilePreviewerAction::Hide); + } + } + } + + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl FileUploadModal { + /// Sets the file data and updates the preview UI. + pub fn set_file_data(&mut self, cx: &mut Cx, file_data: FileData) { + // Update file name label + self.label(cx, ids!(file_name_label)) + .set_text(cx, &file_data.name); + + // Update file size label + self.label(cx, ids!(file_size_label)) + .set_text(cx, &format_file_size(file_data.size)); + + // Determine if this is an image + let is_image = crate::image_utils::is_displayable_image(&file_data.mime_type); + + // Show/hide appropriate preview widgets + let image_preview = self.view.image(cx, ids!(image_preview_container.image_preview)); + let image_preview_container = self.view.view(cx, ids!(image_preview_container)); + let file_icon_container = self.view.view(cx, ids!(file_icon_container)); + + if is_image { + makepad_widgets::log!("FileUploadModal: Loading image preview, data size: {} bytes, mime: {}", file_data.data.len(), file_data.mime_type); + // Hide file icon first + file_icon_container.set_visible(cx, false); + + // Load image data into the preview + if let Err(e) = crate::utils::load_png_or_jpg(&image_preview, cx, &file_data.data) { + makepad_widgets::error!("Failed to load image preview: {:?}", e); + // Fall back to file icon + image_preview_container.set_visible(cx, false); + file_icon_container.set_visible(cx, true); + self.update_file_type_label(cx, &file_data.mime_type); + } else { + makepad_widgets::log!("FileUploadModal: Image loaded successfully"); + // Set container visible after loading + image_preview_container.set_visible(cx, true); + } + } else { + image_preview_container.set_visible(cx, false); + file_icon_container.set_visible(cx, true); + self.update_file_type_label(cx, &file_data.mime_type); + } + + self.file_data = Some(file_data); + self.redraw(cx); + } + + /// Updates the file type label based on MIME type. + fn update_file_type_label(&mut self, cx: &mut Cx, mime_type: &str) { + let type_text = mime_type + .split('/') + .next_back() + .unwrap_or("Unknown") + .to_uppercase(); + self.label(cx, ids!(file_type_label)) + .set_text(cx, &format!("{} File", type_text)); + } +} + +impl FileUploadModalRef { + /// Sets the file data and updates the preview UI. + pub fn set_file_data(&self, cx: &mut Cx, file_data: FileData) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_file_data(cx, file_data); + } + } +} diff --git a/src/shared/mod.rs b/src/shared/mod.rs index a92a81fd..e9a04b02 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -4,12 +4,14 @@ pub mod avatar; pub mod collapsible_header; pub mod expand_arrow; pub mod confirmation_modal; +pub mod file_upload_modal; pub mod helpers; pub mod html_or_plaintext; pub mod icon_button; pub mod jump_to_bottom_button; pub mod mentionable_text_input; pub mod popup_list; +pub mod progress_bar; pub mod room_filter_input_bar; pub mod styles; pub mod text_or_image; @@ -44,4 +46,6 @@ pub fn script_mod(vm: &mut ScriptVm) { restore_status_view::script_mod(vm); confirmation_modal::script_mod(vm); image_viewer::script_mod(vm); + progress_bar::script_mod(vm); + file_upload_modal::script_mod(vm); } diff --git a/src/shared/progress_bar.rs b/src/shared/progress_bar.rs new file mode 100644 index 00000000..36f2aaab --- /dev/null +++ b/src/shared/progress_bar.rs @@ -0,0 +1,106 @@ +//! A progress bar widget with capsule-shaped design for showing upload/download progress. + +use makepad_widgets::*; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.ProgressBar = set_type_default() do #(ProgressBar::register_widget(vm)) { + width: Fill, + height: 8, + show_bg: true, + + draw_bg +: { + progress: instance(0.0) + + // Background color (track) + color: (COLOR_SECONDARY) + // Filled portion color + progress_color: instance((COLOR_ACTIVE_PRIMARY)) + + border_radius: 4.0 + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size); + + // Draw background track (full width, rounded) + sdf.box( + 0.0, + 0.0, + self.rect_size.x, + self.rect_size.y, + self.border_radius + ); + sdf.fill(self.color); + + // Draw progress fill (partial width based on progress, rounded) + let progress_width = self.rect_size.x * self.progress; + if progress_width > 0.0 { + sdf.box( + 0.0, + 0.0, + progress_width, + self.rect_size.y, + self.border_radius + ); + sdf.fill(self.progress_color); + } + + return sdf.result; + } + } + } +} + +/// A capsule-shaped progress bar widget. +#[derive(Script, ScriptHook, Widget)] +pub struct ProgressBar { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + /// Current progress value between 0.0 and 1.0 + #[rust] progress: f32, +} + +impl Widget for ProgressBar { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + // Update the progress uniform before drawing + let progress = self.progress.clamp(0.0, 1.0); + script_apply_eval!(cx, self.view, { + draw_bg.progress: #(progress as f64), + }); + self.view.draw_walk(cx, scope, walk) + } +} + +impl ProgressBar { + /// Sets the progress value (0.0 to 1.0). + pub fn set_progress(&mut self, cx: &mut Cx, value: f32) { + self.progress = value.clamp(0.0, 1.0); + self.redraw(cx); + } + + /// Gets the current progress value. + pub fn progress(&self) -> f32 { + self.progress + } +} + +impl ProgressBarRef { + /// Sets the progress value (0.0 to 1.0). + pub fn set_progress(&self, cx: &mut Cx, value: f32) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_progress(cx, value); + } + } + + /// Gets the current progress value. + pub fn progress(&self) -> f32 { + self.borrow().map(|inner| inner.progress()).unwrap_or(0.0) + } +} diff --git a/src/shared/styles.rs b/src/shared/styles.rs index feb778df..1f042591 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -44,6 +44,8 @@ script_mod! { mod.widgets.ICON_WARNING = crate_resource("self://resources/icons/warning.svg") mod.widgets.ICON_ZOOM_IN = crate_resource("self://resources/icons/zoom_in.svg") mod.widgets.ICON_ZOOM_OUT = crate_resource("self://resources/icons/zoom_out.svg") + mod.widgets.ICON_ADD_ATTACHMENT = crate_resource("self://resources/icons/add_attachment.svg") + mod.widgets.ICON_FILE = crate_resource("self://resources/icons/file.svg") mod.widgets.TITLE_TEXT = theme.font_regular { font_size: (13), diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 30fccc5a..19cc191f 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail, Result}; use bitflags::bitflags; +use mime::Mime; use clap::Parser; use eyeball::Subscriber; use eyeball_im::VectorDiff; @@ -573,6 +574,14 @@ pub enum MatrixRequest { #[cfg(feature = "tsp")] sign_with_tsp: bool, }, + /// Request to send a file attachment to the given room. + SendAttachment { + timeline_kind: TimelineKind, + file_data: crate::shared::file_upload_modal::FileData, + replied_to: Option, + #[cfg(feature = "tsp")] + sign_with_tsp: bool, + }, /// Sends a notice to the given room that the current user is or is not typing. /// /// This request does not return a response or notify the UI thread, and @@ -1692,6 +1701,93 @@ async fn matrix_worker_task( }); } + MatrixRequest::SendAttachment { + timeline_kind, + file_data, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp: _sign_with_tsp, + } => { + let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { + log!("BUG: {timeline_kind} not found for send attachment request"); + continue; + }; + + // Spawn a new async task to send the attachment. + let _send_attachment_task = Handle::current().spawn(async move { + use matrix_sdk::attachment::AttachmentConfig; + use eyeball::SharedObservable; + + log!("Sending attachment to {timeline_kind}: {} ({} bytes)...", + file_data.name, file_data.size); + + // For now, we'll just send the attachment without reply support + // TODO: Add proper reply support for attachments + let _ = replied_to; // Suppress unused warning for now + + // Parse MIME type + let content_type: Mime = file_data.mime_type.parse() + .unwrap_or_else(|_| "application/octet-stream".parse().unwrap()); + + // Create a progress observable to track upload progress + let send_progress: SharedObservable = Default::default(); + let progress_subscriber = send_progress.subscribe(); + + // Spawn a task to handle progress updates + let sender_clone = sender.clone(); + Handle::current().spawn(async move { + let mut subscriber = progress_subscriber; + loop { + let progress = subscriber.get(); + let current: u64 = progress.current as u64; + let total: u64 = progress.total as u64; + if sender_clone.send(TimelineUpdate::FileUploadUpdate { + current, + total, + }).is_err() { + break; + } + SignalToUI::set_ui_signal(); + // Wait for next update + if subscriber.next().await.is_none() { + break; + } + } + }); + + // Use the Room's send_attachment method directly + let room = timeline.room(); + let config = AttachmentConfig::new(); + + let send_future = room.send_attachment( + &file_data.name, + &content_type, + file_data.data.clone(), + config, + ).with_send_progress_observable(send_progress); + + match send_future.await { + Ok(_response) => { + log!("Successfully sent attachment to {timeline_kind}."); + } + Err(e) => { + error!("Failed to send attachment to {timeline_kind}: {e:?}"); + let _ = sender.send(TimelineUpdate::FileUploadError { + error: format!("{e}"), + file_data: file_data.clone(), + }); + enqueue_popup_notification( + format!("Failed to upload file: {e}"), + PopupKind::Error, + None, + ); + } + } + + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { let Some(timeline) = get_timeline(&timeline_kind) else { log!("BUG: {timeline_kind} not found when sending read receipt, {event_id}"); @@ -2178,6 +2274,19 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option }) } +/// Returns a clone of the timeline update sender for the given timeline. +/// +/// This can be called multiple times, as it only clones the sender. +pub fn get_timeline_update_sender(kind: &TimelineKind) -> Option> { + let all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let jrd = all_joined_rooms.get(kind.room_id())?; + let details = match kind { + TimelineKind::MainRoom { .. } => &jrd.main_timeline, + TimelineKind::Thread { thread_root_event_id, .. } => jrd.thread_timelines.get(thread_root_event_id)?, + }; + Some(details.timeline_update_sender.clone()) +} + const DEFAULT_HOMESERVER: &str = "matrix.org"; fn username_to_full_user_id( diff --git a/src/utils.rs b/src/utils.rs index aa3ac814..63605317 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1007,6 +1007,25 @@ impl From<(Option, OwnedRoomId)> for RoomNameId { } } +/// Formats a file size in bytes to a human-readable string. +/// +/// Examples: "1.5 KB", "2.3 MB", "4.0 GB" +pub fn format_file_size(bytes: u64) -> String { + const KB: u64 = 1024; + const MB: u64 = KB * 1024; + const GB: u64 = MB * 1024; + + if bytes >= GB { + format!("{:.1} GB", bytes as f64 / GB as f64) + } else if bytes >= MB { + format!("{:.1} MB", bytes as f64 / MB as f64) + } else if bytes >= KB { + format!("{:.1} KB", bytes as f64 / KB as f64) + } else { + format!("{} B", bytes) + } +} + /// Returns a text avatar string containing the first character of the room name. /// /// Skips the first character if it is a `#` or `!`, the sigils used for Room aliases and Room IDs. From e0ebd48dd09b99976c68529e709b143c6cbeb33c Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 26 Mar 2026 16:41:39 +0800 Subject: [PATCH 15/18] upload_progress --- src/app.rs | 1 + src/home/upload_progress.rs | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index e8ef510a..39b97e42 100644 --- a/src/app.rs +++ b/src/app.rs @@ -670,6 +670,7 @@ impl AppMain for App { crate::home::location_preview::script_mod(vm); crate::home::tombstone_footer::script_mod(vm); crate::home::editing_pane::script_mod(vm); + crate::home::upload_progress::script_mod(vm); crate::room::script_mod(vm); crate::join_leave_room_modal::script_mod(vm); crate::verification_modal::script_mod(vm); diff --git a/src/home/upload_progress.rs b/src/home/upload_progress.rs index 6d59b906..18d1ec82 100644 --- a/src/home/upload_progress.rs +++ b/src/home/upload_progress.rs @@ -84,7 +84,7 @@ script_mod! { } retry_button := RobrixPositiveIconButton { - visible: false, + enabled: false, padding: Inset{top: 4, bottom: 4, left: 8, right: 8} draw_text +: { text_style: REGULAR_TEXT { font_size: 9 }, @@ -173,8 +173,8 @@ impl UploadProgressView { self.label(cx, ids!(file_name_label)).set_text(cx, file_name); self.label(cx, ids!(status_label)).set_text(cx, "Starting upload..."); - self.button(cx, ids!(retry_button)).set_visible(cx, false); - self.button(cx, ids!(cancel_button)).set_visible(cx, true); + self.button(cx, ids!(retry_button)).set_enabled(cx, false); + self.button(cx, ids!(cancel_button)).set_enabled(cx, true); // Reset progress bar self.child_by_path(ids!(progress_bar)).as_progress_bar().set_progress(cx, 0.0); @@ -229,8 +229,8 @@ impl UploadProgressView { // Update UI for error state self.label(cx, ids!(status_label)) .set_text(cx, &format!("Error: {}", error)); - self.button(cx, ids!(retry_button)).set_visible(cx, true); - self.button(cx, ids!(cancel_button)).set_visible(cx, true); + self.button(cx, ids!(retry_button)).set_enabled(cx, true); + self.button(cx, ids!(cancel_button)).set_enabled(cx, true); // Set progress bar to error color - no longer apply color change via script_apply_eval // The progress bar will use the default color for now From 528dd95e62cff4572cb3e1187a735ed5a7e9a40b Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 26 Mar 2026 20:34:59 +0800 Subject: [PATCH 16/18] fix not auto close --- src/home/room_screen.rs | 28 +++++++++++++++------------ src/home/upload_progress.rs | 4 ++-- src/room/room_input_bar.rs | 38 +++++++++++++++++++++++++++++++++++++ src/sliding_sync.rs | 1 + 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index af6a817a..c9c77771 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1587,18 +1587,16 @@ impl RoomScreen { } TimelineUpdate::LinkPreviewFetched => {} TimelineUpdate::FileUploadConfirmed(file_data) => { - // Show upload progress view and start the upload - self.view.room_input_bar(cx, ids!(room_input_bar)) - .show_upload_progress(cx, &file_data.name); - - // Submit the upload request - submit_async_request(MatrixRequest::SendAttachment { - timeline_kind: tl.kind.clone(), - file_data, - replied_to: None, // TODO: support replying with attachments - #[cfg(feature = "tsp")] - sign_with_tsp: false, - }); + let room_input_bar = self.view.room_input_bar(cx, ids!(room_input_bar)); + if let Some(replied_to) = room_input_bar.handle_file_upload_confirmed(cx, &file_data.name) { + submit_async_request(MatrixRequest::SendAttachment { + timeline_kind: tl.kind.clone(), + file_data, + replied_to, + #[cfg(feature = "tsp")] + sign_with_tsp: room_input_bar.is_tsp_signing_enabled(cx), + }); + } } TimelineUpdate::FileUploadUpdate { current, total } => { self.view.room_input_bar(cx, ids!(room_input_bar)) @@ -1612,6 +1610,10 @@ impl RoomScreen { self.view.room_input_bar(cx, ids!(room_input_bar)) .show_upload_error(cx, &error, file_data); } + TimelineUpdate::FileUploadComplete => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .hide_upload_progress(cx); + } } } @@ -2778,6 +2780,8 @@ pub enum TimelineUpdate { error: String, file_data: crate::shared::file_upload_modal::FileData, }, + /// File upload completed successfully. + FileUploadComplete, } thread_local! { diff --git a/src/home/upload_progress.rs b/src/home/upload_progress.rs index 18d1ec82..d59fba8a 100644 --- a/src/home/upload_progress.rs +++ b/src/home/upload_progress.rs @@ -142,7 +142,7 @@ impl Widget for UploadProgressView { if let Some(handle) = self.abort_handle.take() { handle.abort(); } - cx.action(UploadProgressViewAction::Cancelled); + cx.widget_action(self.widget_uid(), UploadProgressViewAction::Cancelled); self.hide(cx); } @@ -150,7 +150,7 @@ impl Widget for UploadProgressView { if self.button(cx, ids!(retry_button)).clicked(actions) { if let UploadViewState::Error { file_data, .. } = &self.state { let file_data = file_data.clone(); - cx.action(UploadProgressViewAction::Retry(file_data)); + cx.widget_action(self.widget_uid(), UploadProgressViewAction::Retry(file_data)); self.hide(cx); } } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 8058a5d1..e0422576 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -858,6 +858,44 @@ impl RoomInputBarRef { .as_upload_progress_view() .show_error(cx, error, file_data); } + + /// Handles a confirmed file upload from the file upload modal. + /// + /// This method: + /// - Shows the upload progress view + /// - Gets and clears any "replying to" state + /// - Returns the reply metadata needed to submit the upload request + pub fn handle_file_upload_confirmed(&self, cx: &mut Cx, file_name: &str) -> Option> { + let mut inner = self.borrow_mut()?; + + // Get the reply metadata if replying to a message + let replied_to = inner + .replying_to + .take() + .and_then(|(event_tl_item, _embedded_event)| { + event_tl_item.event_id().map(|event_id| Reply { + event_id: event_id.to_owned(), + enforce_thread: EnforceThread::MaybeThreaded, + }) + }); + + // Show the upload progress view + inner.child_by_path(ids!(upload_progress_view)) + .as_upload_progress_view() + .show(cx, file_name); + + // Clear the replying-to state + inner.clear_replying_to(cx); + + Some(replied_to) + } + + /// Returns whether TSP signing is enabled. + #[cfg(feature = "tsp")] + pub fn is_tsp_signing_enabled(&self, cx: &mut Cx) -> bool { + let Some(inner) = self.borrow() else { return false }; + inner.is_tsp_signing_enabled(cx) + } } /// The saved UI state of a `RoomInputBar` widget. diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 19cc191f..f3b665af 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1769,6 +1769,7 @@ async fn matrix_worker_task( match send_future.await { Ok(_response) => { log!("Successfully sent attachment to {timeline_kind}."); + let _ = sender.send(TimelineUpdate::FileUploadComplete); } Err(e) => { error!("Failed to send attachment to {timeline_kind}: {e:?}"); From 9f7a907db263ed753e5a095ffd51b8f67a7b43ac Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 26 Mar 2026 21:05:04 +0800 Subject: [PATCH 17/18] fix clippy --- src/room/room_input_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index e0422576..2f3f2bf2 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -636,7 +636,7 @@ impl RoomInputBar { let mime_clone = mime.clone(); cx.spawn_thread(move || { // Generate thumbnail for images - let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(&mime_clone.to_string()) { + let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(&mime_clone.as_ref()) { match std::fs::read(&path_clone) { Ok(data) => { match crate::image_utils::generate_thumbnail(&data) { From cb7d663bf3c7f443101b85b45ffa58f7f631875c Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 27 Mar 2026 09:11:33 +0800 Subject: [PATCH 18/18] fix clippy --- src/room/room_input_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 2f3f2bf2..f1c2844c 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -636,7 +636,7 @@ impl RoomInputBar { let mime_clone = mime.clone(); cx.spawn_thread(move || { // Generate thumbnail for images - let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(&mime_clone.as_ref()) { + let (thumbnail, dimensions) = if crate::image_utils::is_displayable_image(mime_clone.as_ref()) { match std::fs::read(&path_clone) { Ok(data) => { match crate::image_utils::generate_thumbnail(&data) {