From dd8fe418218a2a30c00a814f81d1d1d70fc63e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 24 Mar 2026 03:11:22 +0800 Subject: [PATCH 001/283] Fix stale session restore and in-app signup flow --- src/login/login_screen.rs | 323 +++-- src/persistence/matrix_state.rs | 77 +- src/sliding_sync.rs | 2228 +++++++++++++++++++++---------- 3 files changed, 1769 insertions(+), 859 deletions(-) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 3b3c322a1..dfa25fee7 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,9 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest}; +use crate::sliding_sync::{ + submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount, +}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -60,7 +62,7 @@ script_mod! { show_bg: true, draw_bg.color: (COLOR_SECONDARY) // draw_bg.color: (COLOR_PRIMARY) // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY - + // allow the view to be scrollable but hide the actual scroll bar scroll_bars: { scroll_bar_y: { @@ -123,6 +125,19 @@ script_mod! { is_password: true, } + confirm_password_wrapper := View { + width: 275, height: Fit, + visible: false, + + confirm_password_input := RobrixTextInput { + width: 275, height: Fit + flow: Right, // do not wrap + padding: 10, + empty_text: "Confirm password" + is_password: true, + } + } + View { width: 275, height: Fit, flow: Down, @@ -160,7 +175,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } } - + login_button := RobrixIconButton { width: 275, @@ -171,54 +186,61 @@ script_mod! { text: "Login" } - LineH { - width: 275 - margin: Inset{bottom: -5} - draw_bg.color: #C8C8C8 - } + login_only_view := View { + width: Fit, height: Fit, + flow: Down, + align: Align{x: 0.5, y: 0.5} + spacing: 15.0 - Label { - width: Fit, height: Fit - padding: 0, - draw_text +: { - color: (COLOR_TEXT) - text_style: TITLE_TEXT {font_size: 11.0} + LineH { + width: 275 + margin: Inset{bottom: -5} + draw_bg.color: #C8C8C8 } - text: "Or, login with an SSO provider:" - } - sso_view := View { - width: 275, height: Fit, - margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide - flow: Flow.Right{wrap: true}, - apple_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/apple.png") + Label { + width: Fit, height: Fit + padding: 0, + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 11.0} } + text: "Or, login with an SSO provider:" } - facebook_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/facebook.png") + + sso_view := View { + width: 275, height: Fit, + margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide + flow: Flow.Right{wrap: true}, + apple_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/apple.png") + } } - } - github_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/github.png") + facebook_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/facebook.png") + } } - } - gitlab_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/gitlab.png") + github_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/github.png") + } } - } - google_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/google.png") + gitlab_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/gitlab.png") + } } - } - twitter_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/x.png") + google_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/google.png") + } + } + twitter_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/x.png") + } } } } @@ -233,7 +255,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - Label { + account_prompt_label := Label { width: Fit, height: Fit padding: Inset{left: 1, right: 1, top: 0, bottom: 0} draw_text +: { @@ -245,8 +267,8 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - - signup_button := RobrixIconButton { + + mode_toggle_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{bottom: 5} @@ -270,18 +292,77 @@ script_mod! { } } -static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; - #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + /// Whether the screen is showing the in-app sign-up flow. + #[rust] + signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight - #[rust] sso_pending: bool, + #[rust] + sso_pending: bool, /// The URL to redirect to after logging in with SSO. - #[rust] sso_redirect_url: Option, + #[rust] + sso_redirect_url: Option, + /// The most recent login failure message shown to the user. + #[rust] + last_failure_message_shown: Option, } +impl LoginScreen { + fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { + self.signup_mode = signup_mode; + self.view + .view(cx, ids!(confirm_password_wrapper)) + .set_visible(cx, signup_mode); + self.view + .view(cx, ids!(login_only_view)) + .set_visible(cx, !signup_mode); + self.view.label(cx, ids!(title)).set_text( + cx, + if signup_mode { + "Create your Robrix account" + } else { + "Login to Robrix" + }, + ); + self.view.button(cx, ids!(login_button)).set_text( + cx, + if signup_mode { + "Create account" + } else { + "Login" + }, + ); + self.view.label(cx, ids!(account_prompt_label)).set_text( + cx, + if signup_mode { + "Already have an account?" + } else { + "Don't have an account?" + }, + ); + self.view.button(cx, ids!(mode_toggle_button)).set_text( + cx, + if signup_mode { + "Back to login" + } else { + "Sign up here" + }, + ); + + if !signup_mode { + self.view + .text_input(cx, ids!(confirm_password_input)) + .set_text(cx, ""); + } + + self.redraw(cx); + } +} impl Widget for LoginScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { @@ -297,27 +378,31 @@ impl Widget for LoginScreen { impl MatchEvent for LoginScreen { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let login_button = self.view.button(cx, ids!(login_button)); - let signup_button = self.view.button(cx, ids!(signup_button)); + let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); let password_input = self.view.text_input(cx, ids!(password_input)); + let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); - let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); + let login_status_modal_inner = self + .view + .login_status_modal(cx, ids!(login_status_modal_inner)); - if signup_button.clicked(actions) { - log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); - let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); + if mode_toggle_button.clicked(actions) { + self.set_signup_mode(cx, !self.signup_mode); } if login_button.clicked(actions) || user_id_input.returned(actions).is_some() || password_input.returned(actions).is_some() + || (self.signup_mode && confirm_password_input.returned(actions).is_some()) || homeserver_input.returned(actions).is_some() { - let user_id = user_id_input.text(); + let user_id = user_id_input.text().trim().to_owned(); let password = password_input.text(); - let homeserver = homeserver_input.text(); + let confirm_password = confirm_password_input.text(); + let homeserver = homeserver_input.text().trim().to_owned(); if user_id.is_empty() { login_status_modal_inner.set_title(cx, "Missing User ID"); login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); @@ -326,27 +411,59 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, "Missing Password"); login_status_modal_inner.set_status(cx, "Please enter a valid password."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + } else if self.signup_mode && password != confirm_password { + login_status_modal_inner.set_title(cx, "Passwords do not match"); + login_status_modal_inner.set_status( + cx, + "Please enter the same password in both password fields.", + ); + login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { - login_status_modal_inner.set_title(cx, "Logging in..."); - login_status_modal_inner.set_status(cx, "Waiting for a login response..."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); - submit_async_request(MatrixRequest::Login(LoginRequest::LoginByPassword(LoginByPassword { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }))); + self.last_failure_message_shown = None; + login_status_modal_inner.set_title( + cx, + if self.signup_mode { + "Creating account..." + } else { + "Logging in..." + }, + ); + login_status_modal_inner.set_status( + cx, + if self.signup_mode { + "Waiting for the homeserver to create your account..." + } else { + "Waiting for a login response..." + }, + ); + login_status_modal_inner + .button_ref(cx) + .set_text(cx, "Cancel"); + submit_async_request(MatrixRequest::Login(if self.signup_mode { + LoginRequest::Register(RegisterAccount { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }) + } else { + LoginRequest::LoginByPassword(LoginByPassword { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }) + })); } login_status_modal.open(cx); self.redraw(cx); } - + let provider_brands = ["apple", "facebook", "github", "gitlab", "google", "twitter"]; let button_set: &[&[LiveId]] = ids_array!( - apple_button, - facebook_button, - github_button, - gitlab_button, - google_button, + apple_button, + facebook_button, + github_button, + gitlab_button, + google_button, twitter_button ); for action in actions { @@ -356,21 +473,24 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { - Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { + Some(LoginAction::CliAutoLogin { + user_id, + homeserver, + }) => { + self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); login_status_modal_inner.set_title(cx, "Logging in via CLI..."); - login_status_modal_inner.set_status( - cx, - &format!("Auto-logging in as user {user_id}...") - ); + login_status_modal_inner + .set_status(cx, &format!("Auto-logging in as user {user_id}...")); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Cancel"); login_status_modal_button.set_enabled(cx, false); // Login cancel not yet supported login_status_modal.open(cx); } Some(LoginAction::Status { title, status }) => { + self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); @@ -382,14 +502,28 @@ impl MatchEvent for LoginScreen { Some(LoginAction::LoginSuccess) => { // The main `App` component handles showing the main screen // and hiding the login screen & login status modal. + self.last_failure_message_shown = None; + self.set_signup_mode(cx, false); user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); + confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); login_status_modal.close(cx); self.redraw(cx); } Some(LoginAction::LoginFailure(error)) => { - login_status_modal_inner.set_title(cx, "Login Failed."); + if self.last_failure_message_shown.as_deref() == Some(error.as_str()) { + continue; + } + self.last_failure_message_shown = Some(error.clone()); + login_status_modal_inner.set_title( + cx, + if self.signup_mode { + "Account Creation Failed." + } else { + "Login Failed." + }, + ); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); @@ -399,9 +533,15 @@ impl MatchEvent for LoginScreen { } Some(LoginAction::SsoPending(pending)) => { let mask = if *pending { 1.0 } else { 0.0 }; - let cursor = if *pending { MouseCursor::NotAllowed } else { MouseCursor::Hand }; + let cursor = if *pending { + MouseCursor::NotAllowed + } else { + MouseCursor::Hand + }; for view_ref in self.view_set(cx, button_set).iter() { - let Some(mut view_mut) = view_ref.borrow_mut() else { continue }; + let Some(mut view_mut) = view_ref.borrow_mut() else { + continue; + }; let mut image = view_mut.image(cx, ids!(image)); script_apply_eval!(cx, image, { draw_bg.mask: #(mask) @@ -414,7 +554,7 @@ impl MatchEvent for LoginScreen { Some(LoginAction::SsoSetRedirectUrl(url)) => { self.sso_redirect_url = Some(url.to_string()); } - _ => { } + _ => {} } } @@ -423,7 +563,10 @@ impl MatchEvent for LoginScreen { let login_status_modal_button = login_status_modal_inner.button_ref(cx); if login_status_modal_button.clicked(actions) { let request_id = id!(SSO_CANCEL_BUTTON); - let request = HttpRequest::new(format!("{}/?login_token=",sso_redirect_url), HttpMethod::GET); + let request = HttpRequest::new( + format!("{}/?login_token=", sso_redirect_url), + HttpMethod::GET, + ); cx.http_request(request_id, request); self.sso_redirect_url = None; } @@ -432,15 +575,14 @@ impl MatchEvent for LoginScreen { // Handle any of the SSO login buttons being clicked for (view_ref, brand) in self.view_set(cx, button_set).iter().zip(&provider_brands) { if view_ref.finger_up(actions).is_some() && !self.sso_pending { - submit_async_request(MatrixRequest::SpawnSSOServer{ - identity_provider_id: format!("oidc-{}",brand), + submit_async_request(MatrixRequest::SpawnSSOServer { + identity_provider_id: format!("oidc-{}", brand), brand: brand.to_string(), - homeserver_url: homeserver_input.text() + homeserver_url: homeserver_input.text(), }); } } } - } /// Actions sent to or from the login screen. @@ -451,10 +593,7 @@ pub enum LoginAction { /// A negative response from the backend Matrix task to the login screen. LoginFailure(String), /// A login-related status message to display to the user. - Status { - title: String, - status: String, - }, + Status { title: String, status: String }, /// The given login info was specified on the command line (CLI), /// and the login process is underway. CliAutoLogin { @@ -465,9 +604,9 @@ pub enum LoginAction { /// informing it that the SSO login process is either still in flight (`true`) or has finished (`false`). /// /// Note that an inner value of `false` does *not* imply that the login request has - /// successfully finished. + /// successfully finished. /// The login screen can use this to prevent the user from submitting - /// additional SSO login requests while a previous request is in flight. + /// additional SSO login requests while a previous request is in flight. SsoPending(bool), /// Set the SSO redirect URL in the LoginScreen. /// diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index d99855b7c..f984a2f3b 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -6,15 +6,11 @@ use makepad_widgets::{log, Cx}; use matrix_sdk::{ authentication::matrix::MatrixSession, ruma::{OwnedUserId, UserId}, - sliding_sync, - Client, + sliding_sync, Client, }; use serde::{Deserialize, Serialize}; -use crate::{ - app_data_dir, - login::login_screen::LoginAction, -}; +use crate::{app_data_dir, login::login_screen::LoginAction}; /// The data needed to re-build a client. #[derive(Clone, Serialize, Deserialize)] @@ -57,11 +53,11 @@ pub struct FullSessionPersisted { pub sync_token: Option, /// The sliding sync version to use for this client session. - /// + /// /// This determines the sync protocol used by the Matrix client: /// - `Native`: Uses the server's native sliding sync implementation for efficient syncing /// - `None`: Falls back to standard Matrix sync (without sliding sync optimizations) - /// + /// /// The value is restored and applied to the client via `client.set_sliding_sync_version()` /// when rebuilding the session from persistent storage. #[serde(default)] @@ -93,9 +89,7 @@ impl From for SlidingSyncVersion { } fn user_id_to_file_name(user_id: &UserId) -> String { - user_id.as_str() - .replace(":", "_") - .replace("@", "") + user_id.as_str().replace(":", "_").replace("@", "") } /// Returns the path to the persistent state directory for the given user. @@ -114,14 +108,12 @@ const LATEST_USER_ID_FILE_NAME: &str = "latest_user_id.txt"; /// Returns the user ID of the most recently-logged in user session. pub async fn most_recent_user_id() -> Option { - tokio::fs::read_to_string( - app_data_dir().join(LATEST_USER_ID_FILE_NAME) - ) - .await - .ok()? - .trim() - .try_into() - .ok() + tokio::fs::read_to_string(app_data_dir().join(LATEST_USER_ID_FILE_NAME)) + .await + .ok()? + .trim() + .try_into() + .ok() } /// Save which user was the most recently logged in. @@ -129,17 +121,17 @@ async fn save_latest_user_id(user_id: &UserId) -> anyhow::Result<()> { tokio::fs::write( app_data_dir().join(LATEST_USER_ID_FILE_NAME), user_id.as_str(), - ).await?; + ) + .await?; Ok(()) } - /// Restores the given user's previous session from the filesystem. /// /// If no User ID is specified, the ID of the most recently-logged in user /// is retrieved from the filesystem. pub async fn restore_session( - user_id: Option + user_id: Option, ) -> anyhow::Result<(Client, Option)> { let user_id = if let Some(user_id) = user_id { Some(user_id) @@ -165,8 +157,12 @@ pub async fn restore_session( // The session was serialized as JSON in a file. let serialized_session = tokio::fs::read_to_string(session_file).await?; - let FullSessionPersisted { client_session, user_session, sync_token, sliding_sync_version } = - serde_json::from_str(&serialized_session)?; + let FullSessionPersisted { + client_session, + user_session, + sync_token, + sliding_sync_version, + } = serde_json::from_str(&serialized_session)?; let status_str = format!( "Loaded session file for:\n{user_id}\n\nTrying to connect to homeserver...\n{}", @@ -189,7 +185,10 @@ pub async fn restore_session( .await?; let sliding_sync_version = sliding_sync_version.into(); client.set_sliding_sync_version(sliding_sync_version); - let status_str = format!("Authenticating previous login session for {}...", user_session.meta.user_id); + let status_str = format!( + "Authenticating previous login session for {}...", + user_session.meta.user_id + ); log!("{status_str}"); Cx::post_action(LoginAction::Status { title: "Authenticating session".into(), @@ -226,7 +225,7 @@ pub async fn save_session( client_session, user_session, sync_token: None, - sliding_sync_version + sliding_sync_version, })?; if let Some(parent) = session_file.parent() { tokio::fs::create_dir_all(parent).await?; @@ -238,19 +237,39 @@ pub async fn save_session( } /// Remove the LATEST_USER_ID_FILE_NAME file if it exists -/// +/// /// Returns: /// - Ok(true) if file was found and deleted /// - Ok(false) if file didn't exist /// - Err if deletion failed pub async fn delete_latest_user_id() -> anyhow::Result { let last_login_path = app_data_dir().join(LATEST_USER_ID_FILE_NAME); - + if last_login_path.exists() { - tokio::fs::remove_file(&last_login_path).await + tokio::fs::remove_file(&last_login_path) + .await .map_err(|e| anyhow::anyhow!("Failed to remove latest user file: {e}")) .map(|_| true) } else { Ok(false) } } + +/// Remove the persisted Matrix session file for the given user if it exists. +/// +/// Returns: +/// - Ok(true) if the session file was found and deleted +/// - Ok(false) if the session file didn't exist +/// - Err if deletion failed +pub async fn delete_session(user_id: &UserId) -> anyhow::Result { + let session_file = session_file_path(user_id); + + if session_file.exists() { + tokio::fs::remove_file(&session_file) + .await + .map_err(|e| anyhow::anyhow!("Failed to remove session file {session_file:?}: {e}")) + .map(|_| true) + } else { + Ok(false) + } +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 30fccc5a2..99f799ae0 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,37 +8,110 @@ 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::{ + config::RequestConfig, + encryption::EncryptionSettings, + event_handler::EventHandlerDropGuard, + media::MediaRequestParameters, + room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, + ruma::{ + api::{ + Direction, + client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }, + }, + 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 + 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} + 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, + 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::{ + 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::{ + 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 + }, + 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)] @@ -84,9 +157,28 @@ impl std::fmt::Debug for Cli { impl From for Cli { fn from(login: LoginByPassword) -> Self { Self { - user_id: login.user_id, + user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login.homeserver, + homeserver: login + .homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), + proxy: None, + login_screen: false, + verbose: false, + } + } +} + +impl From for Cli { + fn from(registration: RegisterAccount) -> Self { + Self { + user_id: registration.user_id.trim().to_owned(), + password: registration.password, + homeserver: registration + .homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), proxy: None, login_screen: false, verbose: false, @@ -94,6 +186,151 @@ impl From for Cli { } } +fn infer_homeserver_from_user_id(user_id: &str) -> Option { + let user_id: OwnedUserId = user_id.trim().try_into().ok()?; + Some(user_id.server_name().to_string()) +} + +async fn finalize_authenticated_client( + client: Client, + client_session: ClientSessionPersisted, + fallback_user_id: &str, +) -> Result<(Client, Option)> { + if client.matrix_auth().logged_in() { + let logged_in_user_id = client + .user_id() + .map(ToString::to_string) + .unwrap_or_else(|| fallback_user_id.to_owned()); + log!("Logged in successfully."); + let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); + 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!( + "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); + bail!(err_msg); + } +} + +fn registration_localpart(user_id: &str) -> Result { + let trimmed = user_id.trim(); + if trimmed.is_empty() { + bail!("Please enter a valid username or Matrix user ID."); + } + + if let Ok(full_user_id) = >::try_from(trimmed) { + return Ok(full_user_id.localpart().to_owned()); + } + + let localpart = trimmed.trim_start_matches('@'); + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) + { + bail!("Please enter a valid username or full Matrix user ID."); + } + + Ok(localpart.to_owned()) +} + +fn registration_request( + username: &str, + password: &str, + session: Option, +) -> RegistrationRequest { + let mut request = RegistrationRequest::new(); + request.username = Some(username.to_owned()); + request.password = Some(password.to_owned()); + request.initial_device_display_name = Some("robrix-un-pw".to_owned()); + request.refresh_token = true; + if let Some(session) = session { + let mut dummy = Dummy::new(); + dummy.session = Some(session); + request.auth = Some(AuthData::Dummy(dummy)); + } + request +} + +fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { + if let matrix_sdk::Error::Http(http_error) = error { + match http_error.client_api_error_kind() { + Some(ErrorKind::UserInUse) => { + return "That user ID is already taken. Please choose another one.".to_owned(); + } + Some(ErrorKind::InvalidUsername) => { + return "That user ID is invalid. Use a username like `alice` or a full Matrix ID like `@alice:matrix.org`.".to_owned(); + } + Some(ErrorKind::WeakPassword) => { + return "That password is too weak. Please choose a stronger password.".to_owned(); + } + Some(ErrorKind::Forbidden { .. }) => { + return "This homeserver does not allow open registration.".to_owned(); + } + Some(ErrorKind::LimitExceeded { .. }) => { + return "The homeserver is rate limiting account creation right now. Please try again shortly.".to_owned(); + } + _ => {} + } + } + + format!("Could not create account: {error}") +} + +fn unsupported_registration_flow_message( + flows: &[matrix_sdk::ruma::api::client::uiaa::AuthFlow], +) -> String { + let supports_registration_token = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::RegistrationToken)) + }); + if supports_registration_token { + return "This homeserver requires a registration token. Robrix does not support token-based registration yet.".to_owned(); + } + + let supports_terms = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Terms)) + }); + if supports_terms { + return "This homeserver requires an interactive terms-of-service step. Robrix does not support that registration flow yet.".to_owned(); + } + + "This homeserver requires an unsupported registration flow. Please try another homeserver or register with a different client.".to_owned() +} + +async fn clear_persisted_session(user_id: Option<&UserId>) { + let Some(user_id) = user_id else { + return; + }; + + if let Err(e) = persistence::delete_session(user_id).await { + warning!("Failed to delete persisted session for {user_id}: {e}"); + } + + let latest_user_id = persistence::most_recent_user_id().await; + if latest_user_id.as_deref() == Some(user_id) { + if let Err(e) = persistence::delete_latest_user_id().await { + warning!("Failed to delete latest user id for {user_id}: {e}"); + } + } +} + +fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { + matches!( + error.client_api_error_kind(), + Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) + ) +} /// Build a new client. async fn build_client( @@ -116,9 +353,14 @@ async fn build_client( .collect() }; - let homeserver_url = cli.homeserver.as_deref() + let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); + let homeserver_url = cli + .homeserver + .as_deref() + .filter(|homeserver| !homeserver.trim().is_empty()) + .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); - // .unwrap_or("https://matrix.org/"); + // .unwrap_or("https://matrix.org/"); let mut builder = Client::builder() .server_name_or_homeserver_url(homeserver_url) @@ -146,13 +388,11 @@ async fn build_client( // 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)) - ); + 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(); + let homeserver_url = client.homeserver().to_string(); Ok(( client, ClientSessionPersisted { @@ -168,10 +408,7 @@ async fn build_client( /// 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)> { +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 { @@ -191,23 +428,75 @@ async fn login( .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 { + if !client.matrix_auth().logged_in() { 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() }); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); + bail!(err_msg); + } + finalize_authenticated_client(client, client_session, &cli.user_id).await + } + + LoginRequest::Register(registration) => { + let cli = Cli::from(RegisterAccount { + user_id: registration.user_id.clone(), + password: registration.password.clone(), + homeserver: registration.homeserver.clone(), + }); + let localpart = registration_localpart(®istration.user_id)?; + let (client, client_session) = build_client(&cli, app_data_dir()).await?; + Cx::post_action(LoginAction::Status { + title: "Creating account".into(), + status: format!("Creating account {localpart}..."), + }); + + let auth = client.matrix_auth(); + let initial_request = registration_request(&localpart, ®istration.password, None); + let register_result = match auth.register(initial_request).await { + Ok(response) => Ok(response), + Err(error) => { + if let Some(uiaa_info) = error.as_uiaa_response() { + let supports_dummy = uiaa_info.flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Dummy)) + }); + if supports_dummy { + Cx::post_action(LoginAction::Status { + title: "Completing sign up".into(), + status: "Confirming registration with the homeserver...".into(), + }); + auth.register(registration_request( + &localpart, + ®istration.password, + uiaa_info.session.clone(), + )) + .await + } else { + bail!(unsupported_registration_flow_message(&uiaa_info.flows)); + } + } else { + bail!(registration_uiaa_error_message(&error)); + } + } + }?; + + if !client.matrix_auth().logged_in() { + let err_msg = format!( + "Account {} was created, but the homeserver did not return a login session. Please log in manually.", + register_result.user_id, + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); bail!(err_msg); } + + finalize_authenticated_client(client, client_session, register_result.user_id.as_str()) + .await } LoginRequest::LoginBySSOSuccess(client, client_session) => { @@ -222,7 +511,6 @@ async fn login( } } - /// Which direction to paginate in. /// /// * `Forwards` will retrieve later events (towards the end of the timeline), @@ -291,7 +579,6 @@ pub type OnLinkPreviewFetchedFn = fn( Option>, ); - /// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. #[derive(Clone, Debug)] pub enum MatrixLinkAction { @@ -322,9 +609,7 @@ pub enum DirectMessageRoomAction { 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, - }, + DidNotExist { user_profile: UserProfile }, /// A direct message room didn't exist, but we successfully created a new one. NewlyCreated { user_profile: UserProfile, @@ -359,7 +644,10 @@ impl TimelineKind { 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), + TimelineKind::Thread { + thread_root_event_id, + .. + } => Some(thread_root_event_id), } } } @@ -367,7 +655,10 @@ 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 } => { + TimelineKind::Thread { + room_id, + thread_root_event_id, + } => { write!(f, "Thread({}, {})", room_id, thread_root_event_id) } } @@ -380,9 +671,7 @@ pub enum MatrixRequest { /// Request from the login screen to log in with the given credentials. Login(LoginRequest), /// Request to logout. - Logout { - is_desktop: bool, - }, + Logout { is_desktop: bool }, /// Request to paginate the older (or newer) events of a room or thread timeline. PaginateTimeline { timeline_kind: TimelineKind, @@ -414,9 +703,7 @@ pub enum MatrixRequest { /// /// 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, - }, + 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, @@ -435,13 +722,9 @@ pub enum MatrixRequest { user_id: OwnedUserId, }, /// Request to join the given room. - JoinRoom { - room_id: OwnedRoomId, - }, + JoinRoom { room_id: OwnedRoomId }, /// Request to leave the given room. - LeaveRoom { - room_id: OwnedRoomId, - }, + 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. @@ -464,9 +747,7 @@ pub enum MatrixRequest { via: Vec, }, /// Request to fetch the full details (the room preview) of a tombstoned room. - GetSuccessorRoomDetails { - tombstoned_room_id: OwnedRoomId, - }, + 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 @@ -491,9 +772,7 @@ pub enum MatrixRequest { local_only: bool, }, /// Request to fetch the number of unread messages in the given room. - GetNumberUnreadMessages { - timeline_kind: TimelineKind, - }, + GetNumberUnreadMessages { timeline_kind: TimelineKind }, /// Request to set the unread flag for the given room. SetUnreadFlag { room_id: OwnedRoomId, @@ -578,15 +857,12 @@ pub enum MatrixRequest { /// 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, - }, + 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{ + SpawnSSOServer { brand: String, homeserver_url: String, identity_provider_id: String, @@ -631,9 +907,7 @@ pub enum MatrixRequest { /// /// 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, - }, + GetRoomPowerLevels { timeline_kind: TimelineKind }, /// Toggles the given reaction to the given event in the given room. ToggleReaction { timeline_kind: TimelineKind, @@ -659,7 +933,7 @@ pub enum MatrixRequest { /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. GetMatrixRoomLinkPillInfo { matrix_id: MatrixId, - via: Vec + via: Vec, }, /// Request to fetch URL preview from the Matrix homeserver. GetUrlPreview { @@ -673,18 +947,19 @@ pub enum MatrixRequest { /// 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) + 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{ +pub enum LoginRequest { LoginByPassword(LoginByPassword), + Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), - } /// Information needed to log in to a Matrix homeserver. pub struct LoginByPassword { @@ -693,6 +968,13 @@ pub struct LoginByPassword { pub homeserver: Option, } +/// Information needed to register a new account on a Matrix homeserver. +#[derive(Clone)] +pub struct RegisterAccount { + pub user_id: String, + pub password: String, + pub homeserver: Option, +} /// The entry point for the worker task that runs Matrix-related operations. /// @@ -704,7 +986,8 @@ async fn matrix_worker_task( ) -> 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(); + 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(); @@ -714,7 +997,7 @@ async fn matrix_worker_task( 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." + "BUG: failed to send login request to login worker task.", ))); } } @@ -727,7 +1010,7 @@ async fn matrix_worker_task( match logout_with_state_machine(is_desktop).await { Ok(()) => { log!("Logout completed successfully via state machine"); - }, + } Err(e) => { error!("Logout failed: {e:?}"); } @@ -735,7 +1018,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::PaginateTimeline {timeline_kind, num_events, direction} => { + 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; @@ -777,7 +1064,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::EditMessage { timeline_kind, timeline_event_item_id, edited_content } => { + 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; @@ -799,7 +1090,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchDetailsForEvent { timeline_kind, event_id } => { + 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; @@ -816,7 +1110,10 @@ async fn matrix_worker_task( // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); } } - if sender.send(TimelineUpdate::EventDetailsFetched { event_id, result }).is_err() { + 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(); @@ -870,17 +1167,27 @@ async fn matrix_worker_task( }); } - MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { + 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}"); + 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) { + 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()); + let newly_pending = room_info + .pending_thread_timelines + .insert(thread_root_event_id.clone()); if !newly_pending { continue; } @@ -952,11 +1259,18 @@ async fn matrix_worker_task( }); } - MatrixRequest::Knock { room_or_alias_id, reason, server_names } => { + 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 { + 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 { @@ -980,23 +1294,21 @@ async fn matrix_worker_task( 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, - }), + Ok(_) => Cx::post_action(InviteResultAction::Sent { room_id, user_id }), Err(error) => Cx::post_action(InviteResultAction::Failed { room_id, user_id, error, }), } - } - else { + } 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()), + error: matrix_sdk::Error::UnknownError( + "Room/Space not found in client's known list.".into(), + ), }) } }); @@ -1017,8 +1329,7 @@ async fn matrix_worker_task( JoinRoomResultAction::Failed { room_id, error: e } } } - } - else { + } else { match client.join_room_by_id(&room_id).await { Ok(_room) => { log!("Successfully joined new unknown room {room_id}."); @@ -1053,14 +1364,20 @@ async fn matrix_worker_task( 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()), + 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 } => { + 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; @@ -1069,7 +1386,9 @@ async fn matrix_worker_task( 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(); + sender + .send(TimelineUpdate::RoomMembersListFetched { members }) + .unwrap(); SignalToUI::set_ui_signal(); }; @@ -1086,7 +1405,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetRoomPreview { room_or_alias_id, via } => { + 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; @@ -1099,7 +1421,9 @@ async fn matrix_worker_task( 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"); + error!( + "BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request" + ); continue; }; ( @@ -1115,7 +1439,10 @@ async fn matrix_worker_task( ); } - MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create } => { + 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) { @@ -1138,7 +1465,7 @@ async fn matrix_worker_task( 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 { @@ -1150,7 +1477,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUserProfile { user_id, room_id, local_only } => { + 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}, \ @@ -1244,7 +1575,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetUnreadFlag { room_id, mark_as_unread } => { + 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; @@ -1253,35 +1587,64 @@ async fn matrix_worker_task( 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), + Err(e) => error!( + "Failed to set unread flag to {} for room {}: {:?}", + mark_as_unread, room_id, e + ), } }); } - MatrixRequest::SetIsFavorite { room_id, is_favorite } => { + 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}"); + 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; + 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), + Err(e) => error!( + "Failed to set favorite to {} for room {}: {:?}", + is_favorite, room_id, e + ), } }); } - MatrixRequest::SetIsLowPriority { room_id, is_low_priority } => { + 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}"); + 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; + 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), + 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 + ), } }); } @@ -1290,15 +1653,24 @@ async fn matrix_worker_task( 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" }); + 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" }); + 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" }); + let err_msg = format!( + "Failed to {} avatar: {e}", + if is_removing { "remove" } else { "set" } + ); Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); } } @@ -1309,57 +1681,87 @@ async fn matrix_worker_task( 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{}...", + 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() + 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; + 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)); + 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" }); + 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 } => { + 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 + room.matrix_event_permalink(event_id) + .await .map(MatrixLinkAction::MatrixUri) } else { - room.matrix_permalink(join_on_click).await + 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 + room.matrix_to_event_permalink(event_id) + .await .map(MatrixLinkAction::MatrixToUri) } else { - room.matrix_to_permalink().await + 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"))); + Cx::post_action(MatrixLinkAction::Error(format!( + "Room {room_id} not found" + ))); } }); } - MatrixRequest::IgnoreUser { ignore, room_member, room_id } => { + 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(); @@ -1414,7 +1816,9 @@ async fn matrix_worker_task( 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}"); + log!( + "BUG: skipping send typing notice request for not-yet-known room {room_id}" + ); continue; }; let _typing_task = Handle::current().spawn(async move { @@ -1428,16 +1832,21 @@ async fn matrix_worker_task( 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}"); + 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."); + 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(); + let (drop_guard, receiver) = + main_timeline.room().subscribe_to_typing_notifications(); jrd.typing_notice_subscriber = Some(drop_guard); (main_timeline, receiver) } @@ -1446,7 +1855,11 @@ async fn matrix_worker_task( 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) + ( + main_timeline, + jrd.main_timeline.timeline_update_sender.clone(), + receiver, + ) }; let _typing_notices_task = Handle::current().spawn(async move { @@ -1473,15 +1886,22 @@ async fn matrix_worker_task( }); } - MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { + MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { + timeline_kind, + subscribe, + } => { if !subscribe { - if let Some(task_handler) = subscribers_own_user_read_receipts.remove(&timeline_kind) { + 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}"); + log!( + "BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}" + ); continue; }; @@ -1523,7 +1943,8 @@ async fn matrix_worker_task( } } }); - subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); + subscribers_own_user_read_receipts + .insert(timeline_kind_clone, subscribe_own_read_receipt_task); } MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { @@ -1533,9 +1954,13 @@ async fn matrix_worker_task( } continue; } - let kind = TimelineKind::MainRoom { room_id: room_id.clone() }; + 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}"); + log!( + "BUG: skipping subscribe to pinned events request for unknown room {room_id}" + ); continue; }; let subscribe_pinned_events_task = Handle::current().spawn(async move { @@ -1557,8 +1982,18 @@ async fn matrix_worker_task( 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::SpawnSSOServer { + brand, + homeserver_url, + identity_provider_id, + } => { + spawn_sso_server( + brand, + homeserver_url, + identity_provider_id, + login_sender.clone(), + ) + .await; } MatrixRequest::ResolveRoomAlias(room_alias) => { @@ -1571,7 +2006,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchAvatar { mxc_uri, on_fetched } => { + 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:?}..."); @@ -1581,13 +2019,21 @@ async fn matrix_worker_task( }; 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()) }); + on_fetched(AvatarUpdate { + mxc_uri, + avatar_data: res.map(|v| v.into()), + }); }); } - MatrixRequest::FetchMedia { media_request, on_fetched, destination, update_sender } => { + 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; @@ -1692,7 +2138,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { + 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; @@ -1713,7 +2163,7 @@ async fn matrix_worker_task( }); } }); - }, + } MatrixRequest::GetRoomPowerLevels { timeline_kind } => { let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { @@ -1721,15 +2171,21 @@ async fn matrix_worker_task( continue; }; - let Some(user_id) = current_user_id() else { 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() { + 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(); @@ -1739,9 +2195,13 @@ async fn matrix_worker_task( } } }); - }, + } - MatrixRequest::ToggleReaction { timeline_kind, timeline_event_id, reaction } => { + 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; @@ -1749,17 +2209,26 @@ async fn matrix_worker_task( 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 { + 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:?}"), + } + Err(_e) => error!( + "Failed to send toggle reaction to {timeline_kind}; error: {_e:?}" + ), } }); - }, + } - MatrixRequest::RedactMessage { timeline_kind, timeline_event_id, reason } => { + 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; @@ -1778,9 +2247,13 @@ async fn matrix_worker_task( } } }); - }, + } - MatrixRequest::PinEvent { timeline_kind, event_id, pin } => { + 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; @@ -1792,7 +2265,11 @@ async fn matrix_worker_task( } else { timeline.unpin_event(&event_id).await }; - match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { + match sender.send(TimelineUpdate::PinResult { + event_id, + pin, + result, + }) { Ok(_) => SignalToUI::set_ui_signal(), Err(_) => log!("Failed to send UI update for pin event."), } @@ -1826,7 +2303,12 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUrlPreview { url, on_fetched, destination, update_sender } => { + 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 { @@ -1836,17 +2318,19 @@ async fn matrix_worker_task( // 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") + 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()) @@ -1859,20 +2343,20 @@ async fn matrix_worker_task( // 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]); @@ -1881,22 +2365,25 @@ async fn matrix_worker_task( // } // 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) - }); + 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{ + 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) => { @@ -1916,11 +2403,12 @@ async fn matrix_worker_task( // error!("Response body that failed to parse: {}", text); UrlPreviewError::Json(e) }) - }.await; + } + .await; // match &result { // Ok(preview_data) => { - // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", + // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", // url, preview_data.title, preview_data.site_name); // } // Err(e) => { @@ -1939,7 +2427,6 @@ async fn matrix_worker_task( 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); @@ -1952,7 +2439,8 @@ static REQUEST_SENDER: Mutex>> = Mutex::ne 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())); +static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = + LazyLock::new(|| Arc::new(Notify::new())); /// Blocks the current thread until the given future completes. /// @@ -1963,36 +2451,45 @@ 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(); + 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 - }) + 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(); + 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() + DEFAULT_SSO_CLIENT + .lock() + .unwrap() .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2009,7 +2506,6 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2076,13 +2572,13 @@ impl Drop for JoinedRoomDetails { } } - /// 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())); +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>( @@ -2092,7 +2588,10 @@ fn get_per_timeline_details<'a>( 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), + TimelineKind::Thread { + thread_root_event_id, + .. + } => room_info.thread_timelines.get_mut(thread_root_event_id), } } @@ -2103,14 +2602,22 @@ fn get_timeline(kind: &TimelineKind) -> Option> { } /// 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())) +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() + ALL_JOINED_ROOMS + .lock() + .unwrap() .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2124,15 +2631,16 @@ pub fn get_client() -> Option { /// 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()) - ) + 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() @@ -2141,7 +2649,8 @@ pub fn get_sync_service() -> Option> { /// 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())); +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 { @@ -2153,7 +2662,6 @@ 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. @@ -2167,7 +2675,10 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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)?, + 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 { @@ -2180,25 +2691,18 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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() - }) +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 @@ -2226,18 +2730,14 @@ struct RoomListServiceRoomInfo { 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 { + 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(), @@ -2279,48 +2779,57 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() + 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? {}", + 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") - ); + 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: {:?}", + let new_login_opt: Option<(Client, Option, bool)> = 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), + match persistence::restore_session(specified_username.clone()).await { + Ok((client, sync_token)) => Some((client, sync_token, true)), Err(e) => { let status_err = "Could not restore previous user session.\n\nPlease login again."; log!("{status_err} Error: {e:?}"); + clear_persisted_session( + specified_username + .as_deref() + .or(most_recent_user_id.as_deref()), + ) + .await; 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); + 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), + Ok((client, sync_token)) => Some((client, sync_token, false)), 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}") - )); + 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:?}"), }); @@ -2342,44 +2851,61 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() { + let (client, _sync_token, validate_session) = 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())); + 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, false), + Err(e) => { + error!("Login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err, + status: format!("Login failed: {e}"), }); - return; } + }, + 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; } } - } + }, }; + if validate_session { + match client.whoami().await { + Ok(_) => {} + Err(e) if is_invalid_token_http_error(&e) => { + clear_persisted_session(client.user_id()).await; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; + Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.to_string(), + }); + continue 'login_loop; + } + Err(e) => { + warning!( + "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" + ); + } + } + } + // 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() + 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); @@ -2387,7 +2913,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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."); + error!( + "BUG: unexpectedly replaced an existing client when initializing the matrix client." + ); } // Listen for changes to our verification status and incoming verification requests. @@ -2396,9 +2924,6 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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(), @@ -2416,6 +2941,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } else { format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") }; + if is_invalid_token_error(&e) { + clear_persisted_session(client.user_id()).await; + } 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 }); @@ -2426,6 +2954,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } }; + // Listen for session changes, e.g., when the access token becomes invalid. + handle_session_changes(client.clone()); + break 'login_loop (client, sync_service, logged_in_user_id); }; @@ -2444,7 +2975,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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."); + 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)); @@ -2535,7 +3068,6 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } - /// 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?; @@ -2549,13 +3081,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()), - ]) - )); + 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(); @@ -2571,7 +3103,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()); } + 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); @@ -2582,20 +3120,35 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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); } + 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); + 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; + })) + .await; // Send room order update with the new room IDs let (room_id_refs, room_ids) = { @@ -2609,43 +3162,57 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu }; if !room_ids.is_empty() { enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Append { values: room_ids } + 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"); } + 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; + 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 } + 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; + 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 } + 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 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)); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PopFront, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -2653,13 +3220,18 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .await?; } } remove_diff @ VectorDiff::PopBack => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff 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)); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PopBack, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -2667,38 +3239,61 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .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; + 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 } - )); + 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; + 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() } - )); + 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 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 })); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Remove { index }, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -2706,13 +3301,19 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .await?; } else { - error!("BUG: room_list: diff Remove index {index} out of bounds, len {}", all_known_rooms.len()); + 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}"); } + 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() { @@ -2721,7 +3322,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } all_known_rooms.truncate(length); // sanity check enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Truncate { length } + VecDiff::Truncate { length }, )); } } @@ -2731,7 +3332,6 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 @@ -2751,48 +3351,58 @@ async fn optimize_remove_then_add_into_update( ) -> 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() => - { + 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); + 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; + 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() } - )); + 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() => - { + 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); + 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; + 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() } - )); + 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() => - { + 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); + 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; + 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() } - )); + 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; } @@ -2806,7 +3416,6 @@ async fn optimize_remove_then_add_into_update( Ok(()) } - /// Invoked when the room list service has received an update that changes an existing room. async fn update_room( old_room: &RoomListServiceRoomInfo, @@ -2817,18 +3426,29 @@ async fn update_room( 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); + 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); + 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); + 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 @@ -2838,11 +3458,17 @@ async fn update_room( return Ok(()); } RoomState::Joined => { - log!("update_room(): adding new Joined room: {:?} ({new_room_id})", new_room.display_name); + 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); + 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 => { @@ -2860,7 +3486,12 @@ async fn update_room( 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); + 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(), @@ -2870,12 +3501,15 @@ async fn update_room( // 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) { + 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()) { + 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, @@ -2884,9 +3518,13 @@ async fn update_room( 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); + 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(), @@ -2897,11 +3535,15 @@ async fn update_room( || 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 {} --> {}", + 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, + 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(), @@ -2912,7 +3554,8 @@ async fn update_room( } if old_room.is_direct != new_room.is_direct { - log!("Updating room {} is_direct from {} to {}", + log!( + "Updating room {} is_direct from {} to {}", new_room_id, old_room.is_direct, new_room.is_direct, @@ -2927,7 +3570,8 @@ async fn update_room( 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 = + Some(jrd.main_timeline.timeline_update_sender.clone()); } } __timeline_update_sender_opt.clone() @@ -2936,7 +3580,9 @@ async fn update_room( 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() }); + 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(), @@ -2945,7 +3591,9 @@ async fn update_room( timeline_update_sender, ); } else { - error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); + error!( + "BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}" + ); } } @@ -2956,37 +3604,38 @@ async fn update_room( 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}"), + 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."); + 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, + } 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, - } - ); + 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, @@ -2995,26 +3644,39 @@ async fn add_new_room( ) -> Result<()> { match new_room.state { RoomState::Knocked => { - log!("Got new Knocked room: {:?} ({})", new_room.display_name, new_room.room_id); + 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); + 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); + 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())); + 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) { @@ -3031,18 +3693,20 @@ async fn add_new_room( } 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, - })); + 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, @@ -3050,17 +3714,21 @@ async fn add_new_room( spawn_fetch_room_avatar(new_room); return Ok(()); } - RoomState::Joined => { } // Fall through to adding the joined room below. + 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; + room_list_service + .subscribe_to_rooms(&[&new_room.room_id]) + .await; } let timeline = Arc::new( - new_room.room.timeline_builder() + new_room + .room + .timeline_builder() .with_focus(TimelineFocus::Live { // we show threads as separate timelines in their own RoomScreen hide_threaded_events: true, @@ -3068,7 +3736,12 @@ async fn add_new_room( .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))?, + .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(); @@ -3084,7 +3757,11 @@ async fn add_new_room( // 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); + 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 { @@ -3105,7 +3782,8 @@ async fn add_new_room( let latest = get_latest_event_details( &new_room.room.latest_event().await, room_list_service.client(), - ).await; + ) + .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()); @@ -3136,7 +3814,8 @@ async fn add_new_room( #[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() + let ignored_users = client + .account() .account_data::() .await .ok()?? @@ -3200,7 +3879,9 @@ fn handle_load_app_state(user_id: OwnedUserId) { && !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)); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState( + app_state, + )); } } Err(_e) => { @@ -3219,12 +3900,12 @@ fn handle_load_app_state(user_id: OwnedUserId) { 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, + 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!( @@ -3250,6 +3931,7 @@ fn handle_session_changes(client: Client) { "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."); + clear_persisted_session(client.user_id()).await; Cx::post_action(LoginAction::LoginFailure(msg.to_string())); } Ok(SessionChange::TokensRefreshed) => {} @@ -3299,14 +3981,12 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { 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 - ); - + 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); + 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 { @@ -3319,7 +3999,10 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { } fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { - log!("Initial room list loading state is {:?}", loading_state.get()); + 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:?}"); @@ -3327,8 +4010,12 @@ fn handle_room_list_service_loading_state(mut loading_state: Subscriber { enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); } - RoomListLoadingState::Loaded { maximum_number_of_rooms } => { - enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { max_rooms: maximum_number_of_rooms }); + 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; @@ -3351,12 +4038,12 @@ fn spawn_fetch_successor_room_preview( 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 }, + 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 }) @@ -3390,12 +4077,18 @@ async fn fetch_room_preview_with_avatar( }; 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); + 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 + 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()) } @@ -3415,7 +4108,10 @@ async fn fetch_room_preview_with_avatar( async fn fetch_thread_summary_details( room: &Room, thread_root_event_id: &EventId, -) -> (u32, Option) { +) -> ( + u32, + Option, +) { let mut num_replies = 0; let mut latest_reply_event = None; @@ -3473,10 +4169,7 @@ async fn fetch_latest_thread_reply_event( } /// 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 { +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; @@ -3489,7 +4182,10 @@ async fn count_thread_replies( ..Default::default() }; - let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; + let relations = room + .relations(thread_root_event_id.to_owned(), options) + .await + .ok()?; if relations.chunk.is_empty() { break; } @@ -3515,7 +4211,8 @@ async fn text_preview_of_latest_thread_reply( Ok(Some(rm)) => Some(rm), _ => room.get_member(&sender_id).await.ok().flatten(), }; - let sender_name = sender_room_member.as_ref() + 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(|| { @@ -3532,7 +4229,6 @@ async fn text_preview_of_latest_thread_reply( } } - /// 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 @@ -3556,29 +4252,37 @@ async fn get_latest_event_details( match latest_event_value { LatestEventValue::None => None, - LatestEventValue::Remote { timestamp, sender, is_own, profile, content } => { + 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); + 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: _ } => { + 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); + 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. @@ -3586,10 +4290,9 @@ async fn get_latest_event_details( /// 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 { + 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, @@ -3626,7 +4329,6 @@ async fn timeline_subscriber_handler( 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, @@ -3635,14 +4337,13 @@ async fn timeline_subscriber_handler( 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 + 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)) @@ -3651,11 +4352,13 @@ async fn timeline_subscriber_handler( } } - 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()); + 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(), @@ -3668,262 +4371,266 @@ async fn timeline_subscriber_handler( // 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()); + 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)) + ); - // 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}."); + target_event_id = new_request_details.as_ref().map(|(ev, ..)| ev.clone()); - // 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(), + // 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, } - }, - num_events: 50, - direction: PaginationDirection::Backwards, - }); + ).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)); + // 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; } - - 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. + 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(); } - // 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 { + 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; - } else { - index_of_first_change = min(index_of_first_change, index); - index_of_last_change = usize::MAX; + timeline_items.push_front(value); } - if index >= timeline_items.len() { + 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; } - - 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; + 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. } - } else { - found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) - .map(|(i, ev)| (i + index, ev)); + // This doesn't affect whether we should reobtain the latest event. } - - 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)); + 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}"); } } - 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); + 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}"); } } - 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; + 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; } - 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; - } + 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; + 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:?}!") - ); - } + 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(); + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); + } } - } - else => { - break; + else => { + break; + } } - } } + } - error!("Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}."); + 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. @@ -3948,8 +4655,13 @@ async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvata _ => { 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 { + 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()); } } @@ -3978,7 +4690,8 @@ async fn spawn_sso_server( // 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(), + status: "Please wait while Matrix builds and configures the client object for login." + .into(), }); // Wait for the notification that the client has been built @@ -3999,19 +4712,21 @@ async fn spawn_sso_server( // 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() + 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/") - ) { + && 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 { + ) + .await + { Ok(success) => client_and_session = Some(success), Err(e) => build_client_error = Some(e), } @@ -4020,10 +4735,12 @@ async fn spawn_sso_server( 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}") + 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. @@ -4035,7 +4752,8 @@ async fn spawn_sso_server( 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(), + status: "Please finish logging in using your browser, and then come back to Robrix." + .into(), }); match client .matrix_auth() @@ -4045,12 +4763,15 @@ async fn spawn_sso_server( if key == "redirectUrl" { let redirect_url = Url::parse(&value)?; Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); - break + break; } } - Uri::new(&sso_url).open().map_err(|err| - Error::Io(io::Error::other(format!("Unable to open SSO login url. Error: {:?}", err))) - ) + 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}")) @@ -4065,10 +4786,13 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session)).await { + 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." + "BUG: failed to send login request to matrix worker thread.", ))); } enqueue_rooms_list_update(RoomsListUpdate::Status { @@ -4094,7 +4818,6 @@ async fn spawn_sso_server( }); } - bitflags! { /// The powers that a user has in a given room. #[derive(Copy, Clone, PartialEq, Eq)] @@ -4172,14 +4895,38 @@ impl UserPowerLevels { 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.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 } @@ -4225,8 +4972,7 @@ impl UserPowerLevels { } pub fn can_send_message(self) -> bool { - self.contains(UserPowerLevels::RoomMessage) - || self.contains(UserPowerLevels::Message) + self.contains(UserPowerLevels::RoomMessage) || self.contains(UserPowerLevels::Message) } pub fn can_send_reaction(self) -> bool { @@ -4243,7 +4989,6 @@ impl UserPowerLevels { } } - /// 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() { @@ -4261,9 +5006,16 @@ pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { 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 { + 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(()) From 7149b00fa9021e48d46d945971d55e7eb59f06fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 24 Mar 2026 03:12:25 +0800 Subject: [PATCH 002/283] Commit remaining workspace changes --- src/app.rs | 285 +++-- src/avatar_cache.rs | 26 +- src/event_preview.rs | 584 +++++---- src/home/add_room.rs | 307 +++-- src/home/edited_indicator.rs | 18 +- src/home/editing_pane.rs | 134 ++- src/home/event_reaction_list.rs | 57 +- src/home/event_source_modal.rs | 64 +- src/home/home_screen.rs | 70 +- src/home/invite_modal.rs | 62 +- src/home/invite_screen.rs | 163 ++- src/home/link_preview.rs | 110 +- src/home/loading_pane.rs | 98 +- src/home/location_preview.rs | 28 +- src/home/main_desktop_ui.rs | 151 ++- src/home/main_mobile_ui.rs | 35 +- src/home/navigation_tab_bar.rs | 128 +- src/home/new_message_context_menu.rs | 175 +-- src/home/room_context_menu.rs | 87 +- src/home/room_image_viewer.rs | 5 +- src/home/room_read_receipt.rs | 8 +- src/home/room_screen.rs | 1662 ++++++++++++++++---------- src/home/rooms_list.rs | 682 +++++++---- src/home/rooms_list_entry.rs | 139 ++- src/home/rooms_list_header.rs | 46 +- src/home/rooms_sidebar.rs | 7 +- src/home/search_messages.rs | 6 +- src/home/space_lobby.rs | 451 ++++--- src/home/spaces_bar.rs | 242 ++-- src/home/tombstone_footer.rs | 79 +- src/join_leave_room_modal.rs | 212 ++-- src/lib.rs | 3 - src/location.rs | 14 +- src/login/login_status_modal.rs | 5 +- src/logout/logout_confirm_modal.rs | 57 +- src/logout/logout_errors.rs | 2 +- src/logout/logout_state_machine.rs | 280 +++-- src/main.rs | 5 +- src/media_cache.rs | 85 +- src/persistence/app_state.rs | 19 +- src/persistence/tsp_state.rs | 18 +- src/profile/user_profile.rs | 223 ++-- src/profile/user_profile_cache.rs | 146 ++- src/room/mod.rs | 24 +- src/room/room_display_filter.rs | 52 +- src/room/room_input_bar.rs | 287 +++-- src/room/typing_notice.rs | 26 +- src/settings/account_settings.rs | 247 ++-- src/settings/settings_screen.rs | 48 +- src/shared/avatar.rs | 136 ++- src/shared/bouncing_dots.rs | 24 +- src/shared/collapsible_header.rs | 45 +- src/shared/command_text_input.rs | 28 +- src/shared/confirmation_modal.rs | 60 +- src/shared/expand_arrow.rs | 30 +- src/shared/html_or_plaintext.rs | 173 ++- src/shared/image_viewer.rs | 187 +-- src/shared/jump_to_bottom_button.rs | 48 +- src/shared/mentionable_text_input.rs | 29 +- src/shared/mod.rs | 1 - src/shared/popup_list.rs | 39 +- src/shared/restore_status_view.rs | 24 +- src/shared/room_filter_input_bar.rs | 17 +- src/shared/styles.rs | 31 +- src/shared/text_or_image.rs | 49 +- src/shared/timestamp.rs | 22 +- src/shared/unread_badge.rs | 44 +- src/shared/verification_badge.rs | 6 +- src/space_service_sync.rs | 852 +++++++------ src/temp_storage.rs | 2 - src/tsp/create_did_modal.rs | 52 +- src/tsp/create_wallet_modal.rs | 58 +- src/tsp/mod.rs | 761 +++++++----- src/tsp/tsp_settings_screen.rs | 201 +++- src/tsp/tsp_sign_indicator.rs | 29 +- src/tsp/tsp_verification_modal.rs | 52 +- src/tsp/verify_user.rs | 66 +- src/tsp/wallet_entry/mod.rs | 80 +- src/utils.rs | 255 ++-- src/verification.rs | 156 ++- src/verification_modal.rs | 59 +- 81 files changed, 6991 insertions(+), 4287 deletions(-) diff --git a/src/app.rs b/src/app.rs index f04e177d5..e506eb4b0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,7 +4,10 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; +use matrix_sdk::{ + RoomState, + ruma::{OwnedEventId, OwnedRoomId, RoomId}, +}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ @@ -51,7 +54,7 @@ script_mod! { close +: { draw_bg +: {color: #0, color_hover: #E81123, color_down: #FF0015} } } } - + body +: { padding: 0, @@ -80,7 +83,7 @@ script_mod! { image_viewer_modal_inner := ImageViewer {} } } - + // Context menus should be shown in front of other UI elements, // but behind verification modals. new_message_context_menu := NewMessageContextMenu { } @@ -164,9 +167,11 @@ app_main!(App); #[derive(Script)] pub struct App { - #[live] ui: WidgetRef, + #[live] + ui: WidgetRef, /// The top-level app state, shared across various parts of the app. - #[rust] app_state: AppState, + #[rust] + app_state: AppState, /// The details of a room we're waiting on to be loaded so that we can navigate to it. /// 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. @@ -198,15 +203,27 @@ impl MatchEvent for App { let _ = tracing_subscriber::fmt::try_init(); // Override Makepad's new default-JSON logger. We just want regular formatting. - fn regular_log(file_name: &str, line_start: u32, column_start: u32, _line_end: u32, _column_end: u32, message: String, level: LogLevel) { + fn regular_log( + file_name: &str, + line_start: u32, + column_start: u32, + _line_end: u32, + _column_end: u32, + message: String, + level: LogLevel, + ) { let l = match level { - LogLevel::Panic => "[!]", - LogLevel::Error => "[E]", + LogLevel::Panic => "[!]", + LogLevel::Error => "[E]", LogLevel::Warning => "[W]", - LogLevel::Log => "[I]", - LogLevel::Wait => "[.]", + LogLevel::Log => "[I]", + LogLevel::Wait => "[.]", }; - println!("{l} {file_name}:{}:{}: {message}", line_start + 1, column_start + 1); + println!( + "{l} {file_name}:{}:{}: {message}", + line_start + 1, + column_start + 1 + ); } *LOG_WITH_LEVEL.write().unwrap() = regular_log; @@ -233,41 +250,52 @@ impl MatchEvent for App { log!("App::Startup: starting matrix sdk loop"); let _tokio_rt_handle = crate::sliding_sync::start_matrix_tokio().unwrap(); - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { log!("App::Startup: initializing TSP (Trust Spanning Protocol) module."); crate::tsp::tsp_init(_tokio_rt_handle).unwrap(); } } fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - let invite_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); + let invite_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); if let Some(_accepted) = invite_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(invite_confirmation_modal)).close(cx); } - let delete_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); + let delete_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); if let Some(_accepted) = delete_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(delete_confirmation_modal)).close(cx); } - let positive_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); + let positive_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); if let Some(_accepted) = positive_confirmation_modal_inner.closed(actions) { - self.ui.modal(cx, ids!(positive_confirmation_modal)).close(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .close(cx); } for action in actions { match action.downcast_ref() { Some(LogoutConfirmModalAction::Open) => { - self.ui.logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)).reset_state(cx); + self.ui + .logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)) + .reset_state(cx); self.ui.modal(cx, ids!(logout_confirm_modal)).open(cx); continue; - }, + } Some(LogoutConfirmModalAction::Close { was_internal, .. }) => { if *was_internal { self.ui.modal(cx, ids!(logout_confirm_modal)).close(cx); } continue; - }, + } _ => {} } @@ -279,8 +307,8 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } - Some(LogoutAction::ClearAppState { on_clear_appstate }) => { - // Clear user profile cache, invited_rooms timeline states + Some(LogoutAction::ClearAppState { on_clear_appstate }) => { + // Clear user profile cache, invited_rooms timeline states clear_all_app_state(cx); // Reset all app state to its default. self.app_state = Default::default(); @@ -303,7 +331,9 @@ impl MatchEvent for App { // When not yet logged in, the login_screen widget handles displaying the failure modal. if let Some(LoginAction::LoginFailure(_)) = action.downcast_ref() { if self.app_state.logged_in { - log!("Received LoginAction::LoginFailure while logged in; showing login screen."); + log!( + "Received LoginAction::LoginFailure while logged in; showing login screen." + ); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); @@ -312,9 +342,13 @@ impl MatchEvent for App { } // Handle an action requesting to open the new message context menu. - if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { + if let MessageAction::OpenMessageContextMenu { details, abs_pos } = + action.as_widget_action().cast() + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); + let new_message_context_menu = self + .ui + .new_message_context_menu(cx, ids!(new_message_context_menu)); let expected_dimensions = new_message_context_menu.show(cx, details); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); @@ -335,7 +369,9 @@ impl MatchEvent for App { } // Handle an action requesting to open the room context menu. - if let RoomsListAction::OpenRoomContextMenu { details, pos } = action.as_widget_action().cast() { + if let RoomsListAction::OpenRoomContextMenu { details, pos } = + action.as_widget_action().cast() + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); let expected_dimensions = room_context_menu.show(cx, details); @@ -413,18 +449,25 @@ impl MatchEvent for App { cx.action(MainDesktopUiAction::LoadDockFromAppState); continue; } - Some(AppStateAction::NavigateToRoom { room_to_close, destination_room }) => { + Some(AppStateAction::NavigateToRoom { + room_to_close, + destination_room, + }) => { self.navigate_to_room(cx, room_to_close.as_ref(), destination_room); continue; } // If we successfully loaded a room that we were waiting on, // we can now navigate to it and optionally close a previous room. - Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) if - self.waiting_to_navigate_to_room.as_ref() + Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) + if self + .waiting_to_navigate_to_room + .as_ref() .is_some_and(|(dr, _)| dr.room_id() == room_name_id.room_id()) => { log!("Loaded awaited room {room_name_id:?}, navigating to it now..."); - if let Some((dest_room, room_to_close)) = self.waiting_to_navigate_to_room.take() { + if let Some((dest_room, room_to_close)) = + self.waiting_to_navigate_to_room.take() + { self.navigate_to_room(cx, room_to_close.as_ref(), &dest_room); } continue; @@ -434,18 +477,22 @@ impl MatchEvent for App { // Handle actions for showing or hiding the tooltip. match action.as_widget_action().cast() { - TooltipAction::HoverIn { text, widget_rect, options } => { + TooltipAction::HoverIn { + text, + widget_rect, + options, + } => { // Don't show any tooltips if the message context menu is currently shown. - if self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)).is_currently_shown(cx) { + if self + .ui + .new_message_context_menu(cx, ids!(new_message_context_menu)) + .is_currently_shown(cx) + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - } - else { - self.ui.callout_tooltip(cx, ids!(app_tooltip)).show_with_options( - cx, - &text, - widget_rect, - options, - ); + } else { + self.ui + .callout_tooltip(cx, ids!(app_tooltip)) + .show_with_options(cx, &text, widget_rect, options); } continue; } @@ -479,7 +526,8 @@ impl MatchEvent for App { // // Note: other verification actions are handled by the verification modal itself. if let Some(VerificationAction::RequestReceived(state)) = action.downcast_ref() { - self.ui.verification_modal(cx, ids!(verification_modal_inner)) + self.ui + .verification_modal(cx, ids!(verification_modal_inner)) .initialize_with_data(cx, state.clone()); self.ui.modal(cx, ids!(verification_modal)).open(cx); continue; @@ -500,12 +548,23 @@ impl MatchEvent for App { _ => {} } // Handle actions to open/close the TSP verification modal. - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { use std::ops::Deref; - use crate::tsp::{tsp_verification_modal::{TspVerificationModalAction, TspVerificationModalWidgetRefExt}, TspIdentityAction}; + use crate::tsp::{ + tsp_verification_modal::{ + TspVerificationModalAction, TspVerificationModalWidgetRefExt, + }, + TspIdentityAction, + }; - if let Some(TspIdentityAction::ReceivedDidAssociationRequest { details, wallet_db }) = action.downcast_ref() { - self.ui.tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) + if let Some(TspIdentityAction::ReceivedDidAssociationRequest { + details, + wallet_db, + }) = action.downcast_ref() + { + self.ui + .tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) .initialize_with_details(cx, details.clone(), wallet_db.deref().clone()); self.ui.modal(cx, ids!(tsp_verification_modal)).open(cx); continue; @@ -517,7 +576,9 @@ impl MatchEvent for App { } // Handle a request to show the invite confirmation modal. - if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = action.downcast_ref() { + if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = + action.downcast_ref() + { if let Some(content) = content_opt.borrow_mut().take() { invite_confirmation_modal_inner.show(cx, content); self.ui.modal(cx, ids!(invite_confirmation_modal)).open(cx); @@ -526,10 +587,13 @@ impl MatchEvent for App { } // Handle a request to show the generic positive confirmation modal. - if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() { + if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() + { if let Some(content) = content_opt.borrow_mut().take() { positive_confirmation_modal_inner.show(cx, content); - self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .open(cx); } continue; } @@ -537,7 +601,9 @@ impl MatchEvent for App { // Handle a request to show the delete confirmation modal. if let Some(ConfirmDeleteAction::Show(content_opt)) = action.downcast_ref() { if let Some(content) = content_opt.borrow_mut().take() { - self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)).show(cx, content); + self.ui + .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)) + .show(cx, content); self.ui.modal(cx, ids!(delete_confirmation_modal)).open(cx); } continue; @@ -546,8 +612,10 @@ impl MatchEvent for App { // Handle InviteModalAction to open/close the invite modal. match action.downcast_ref() { Some(InviteModalAction::Open(room_name_id)) => { - self.ui.invite_modal(cx, ids!(invite_modal_inner)).show(cx, room_name_id.clone()); - self.ui.modal(cx, ids!(invite_modal)).open(cx); + self.ui + .invite_modal(cx, ids!(invite_modal_inner)) + .show(cx, room_name_id.clone()); + self.ui.modal(cx, ids!(invite_modal)).open(cx); continue; } Some(InviteModalAction::Close) => { @@ -559,8 +627,13 @@ impl MatchEvent for App { // Handle EventSourceModalAction to open/close the event source modal. match action.downcast_ref() { - Some(EventSourceModalAction::Open { room_id, event_id, original_json }) => { - self.ui.event_source_modal(cx, ids!(event_source_modal_inner)) + Some(EventSourceModalAction::Open { + room_id, + event_id, + original_json, + }) => { + self.ui + .event_source_modal(cx, ids!(event_source_modal_inner)) .show(cx, room_id.clone(), event_id.clone(), original_json.clone()); self.ui.modal(cx, ids!(event_source_modal)).open(cx); continue; @@ -575,7 +648,11 @@ impl MatchEvent for App { // Handle DirectMessageRoomActions match action.downcast_ref() { Some(DirectMessageRoomAction::FoundExisting { room_name_id, .. }) => { - self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); + self.navigate_to_room( + cx, + None, + &BasicRoomDetails::RoomId(room_name_id.clone()), + ); } Some(DirectMessageRoomAction::DidNotExist { user_profile }) => { let user_profile = user_profile.clone(); @@ -583,8 +660,7 @@ impl MatchEvent for App { Some(un) if !un.is_empty() => format!( "You don't have an existing direct message room with {} ({}).\n\n\ Would you like to create one now?", - un, - user_profile.user_id, + un, user_profile.user_id, ), _ => format!( "You don't have an existing direct message room with {}.\n\n\ @@ -612,17 +688,29 @@ impl MatchEvent for App { ..Default::default() }, ); - self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .open(cx); } - Some(DirectMessageRoomAction::FailedToCreate { user_profile, error }) => { + Some(DirectMessageRoomAction::FailedToCreate { + user_profile, + error, + }) => { enqueue_popup_notification( - format!("Failed to create a new DM room with {}.\n\nError: {error}", user_profile.displayable_name()), + format!( + "Failed to create a new DM room with {}.\n\nError: {error}", + user_profile.displayable_name() + ), PopupKind::Error, None, ); } Some(DirectMessageRoomAction::NewlyCreated { room_name_id, .. }) => { - self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); + self.navigate_to_room( + cx, + None, + &BasicRoomDetails::RoomId(room_name_id.clone()), + ); } _ => {} } @@ -631,7 +719,7 @@ impl MatchEvent for App { } /// Clears all thread-local UI caches (user profiles, invited rooms, and timeline states). -/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, +/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, fn clear_all_app_state(cx: &mut Cx) { clear_user_profile_cache(cx); clear_all_invited_rooms(cx); @@ -683,27 +771,34 @@ impl AppMain for App { error!("Failed to save app state. Error: {e}"); } } - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { // Save the TSP wallet state, if it exists, with a 3-second timeout. let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap()); let res = crate::sliding_sync::block_on_async_with_timeout( Some(std::time::Duration::from_secs(3)), async move { match tsp_state.close_and_serialize().await { - Ok(saved_state) => match persistence::save_tsp_state_async(saved_state).await { - Ok(_) => { } - Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), + Ok(saved_state) => { + match persistence::save_tsp_state_async(saved_state).await { + Ok(_) => {} + Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), + } + } + Err(e) => { + error!("Failed to close and serialize TSP wallet state. Error: {e}") } - Err(e) => error!("Failed to close and serialize TSP wallet state. Error: {e}"), } }, ); if let Err(_e) = res { - error!("Failed to save TSP wallet state before app shutdown. Error: Timed Out."); + error!( + "Failed to save TSP wallet state before app shutdown. Error: Timed Out." + ); } } } - + // Forward events to the MatchEvent trait implementation. self.match_event(cx, event); let scope = &mut Scope::with_data(&mut self.app_state); @@ -751,8 +846,12 @@ impl App { .modal(cx, ids!(login_screen_view.login_screen.login_status_modal)) .close(cx); } - self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, show_login); - self.ui.view(cx, ids!(home_screen_view)).set_visible(cx, !show_login); + self.ui + .view(cx, ids!(login_screen_view)) + .set_visible(cx, show_login); + self.ui + .view(cx, ids!(home_screen_view)) + .set_visible(cx, !show_login); } /// Navigates to the given `destination_room`, optionally closing the `room_to_close`. @@ -767,16 +866,17 @@ impl App { let tab_id = LiveId::from_str(to_close.as_str()); let widget_uid = self.ui.widget_uid(); move |cx: &mut Cx| { - cx.widget_action( - widget_uid, - DockAction::TabCloseWasPressed(tab_id), - ); - enqueue_rooms_list_update(RoomsListUpdate::HideRoom { room_id: to_close.clone() }); + cx.widget_action(widget_uid, DockAction::TabCloseWasPressed(tab_id)); + enqueue_rooms_list_update(RoomsListUpdate::HideRoom { + room_id: to_close.clone(), + }); } }); let destination_room_id = destination_room.room_id(); - let room_state = cx.get_global::().get_room_state(destination_room_id); + let room_state = cx + .get_global::() + .get_room_state(destination_room_id); let new_selected_room = match room_state { Some(RoomState::Joined) => SelectedRoom::JoinedRoom { room_name_id: destination_room.room_name_id().clone(), @@ -786,11 +886,12 @@ impl App { }, // If the destination room is not yet loaded, show a join modal. _ => { - log!("Destination room {:?} not loaded, showing join modal...", destination_room.room_name_id()); - self.waiting_to_navigate_to_room = Some(( - destination_room.clone(), - room_to_close.cloned(), - )); + log!( + "Destination room {:?} not loaded, showing join modal...", + destination_room.room_name_id() + ); + self.waiting_to_navigate_to_room = + Some((destination_room.clone(), room_to_close.cloned())); cx.action(JoinLeaveRoomModalAction::Open { kind: JoinLeaveModalKind::JoinRoom { details: destination_room.clone(), @@ -802,8 +903,8 @@ impl App { } }; - - log!("Navigating to destination room {:?}, closing room {:?}", + log!( + "Navigating to destination room {:?}, closing room {:?}", destination_room.room_name_id(), room_to_close, ); @@ -814,7 +915,7 @@ impl App { cx.action(NavigationBarAction::GoToHome); } cx.widget_action( - self.ui.widget_uid(), + self.ui.widget_uid(), RoomsListAction::Selected(new_selected_room), ); // Select and scroll to the destination room in the rooms list. @@ -966,7 +1067,6 @@ pub struct SavedDockState { pub selected_room: Option, } - /// Represents a room currently or previously selected by the user. /// /// ## PartialEq/Eq equality comparison behavior @@ -1023,9 +1123,7 @@ impl SelectedRoom { match self { SelectedRoom::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => { let name = room_name_id.clone(); - *self = SelectedRoom::JoinedRoom { - room_name_id: name, - }; + *self = SelectedRoom::JoinedRoom { room_name_id: name }; true } _ => false, @@ -1035,11 +1133,14 @@ impl SelectedRoom { /// Returns the `LiveId` of the room tab corresponding to this `SelectedRoom`. pub fn tab_id(&self) -> LiveId { match self { - SelectedRoom::Thread { room_name_id, thread_root_event_id } => { - LiveId::from_str( - &format!("{}##{}", room_name_id.room_id(), thread_root_event_id) - ) - } + SelectedRoom::Thread { + room_name_id, + thread_root_event_id, + } => LiveId::from_str(&format!( + "{}##{}", + room_name_id.room_id(), + thread_root_event_id + )), other => LiveId::from_str(other.room_id().as_str()), } } diff --git a/src/avatar_cache.rs b/src/avatar_cache.rs index 4d6d240b7..85bf71b65 100644 --- a/src/avatar_cache.rs +++ b/src/avatar_cache.rs @@ -6,7 +6,6 @@ use matrix_sdk::ruma::OwnedMxcUri; use crate::sliding_sync::{submit_async_request, MatrixRequest}; - thread_local! { /// A cache of Avatar images, indexed by Matrix URI. /// @@ -65,21 +64,16 @@ pub fn process_avatar_updates(_cx: &mut Cx) { /// This function requires passing in a reference to `Cx`, /// which isn't used, but acts as a guarantee that this function /// must only be called by the main UI thread. -pub fn get_or_fetch_avatar( - _cx: &mut Cx, - avatar_uri: &OwnedMxcUri, -) -> AvatarCacheEntry { - AVATAR_NEW_CACHE.with_borrow_mut(|cache| { - match cache.raw_entry_mut().from_key(avatar_uri) { - RawEntryMut::Occupied(occupied) => occupied.get().clone(), - RawEntryMut::Vacant(vacant) => { - vacant.insert(avatar_uri.clone(), AvatarCacheEntry::Requested); - submit_async_request(MatrixRequest::FetchAvatar { - mxc_uri: avatar_uri.clone(), - on_fetched: enqueue_avatar_update, - }); - AvatarCacheEntry::Requested - } +pub fn get_or_fetch_avatar(_cx: &mut Cx, avatar_uri: &OwnedMxcUri) -> AvatarCacheEntry { + AVATAR_NEW_CACHE.with_borrow_mut(|cache| match cache.raw_entry_mut().from_key(avatar_uri) { + RawEntryMut::Occupied(occupied) => occupied.get().clone(), + RawEntryMut::Vacant(vacant) => { + vacant.insert(avatar_uri.clone(), AvatarCacheEntry::Requested); + submit_async_request(MatrixRequest::FetchAvatar { + mxc_uri: avatar_uri.clone(), + on_fetched: enqueue_avatar_update, + }); + AvatarCacheEntry::Requested } }) } diff --git a/src/event_preview.rs b/src/event_preview.rs index d4e0cde25..6a34ab655 100644 --- a/src/event_preview.rs +++ b/src/event_preview.rs @@ -7,9 +7,28 @@ use std::borrow::Cow; -use matrix_sdk::{ruma::{OwnedUserId, events::{room::{guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, message::{MessageFormat, MessageType}}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent}, serde::Raw, UserId}}; +use matrix_sdk::{ + ruma::{ + OwnedUserId, + events::{ + room::{ + guest_access::GuestAccess, + history_visibility::HistoryVisibility, + join_rules::JoinRule, + message::{MessageFormat, MessageType}, + }, + AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, + SyncMessageLikeEvent, + }, + serde::Raw, + UserId, + }, +}; use matrix_sdk_base::crypto::types::events::UtdCause; -use matrix_sdk_ui::timeline::{self, AnyOtherFullStateEventContent, EncryptedMessage, EventTimelineItem, MemberProfileChange, MembershipChange, MsgLikeKind, OtherMessageLike, RoomMembershipChange, TimelineItemContent}; +use matrix_sdk_ui::timeline::{ + self, AnyOtherFullStateEventContent, EncryptedMessage, EventTimelineItem, MemberProfileChange, + MembershipChange, MsgLikeKind, OtherMessageLike, RoomMembershipChange, TimelineItemContent, +}; use crate::utils; @@ -38,22 +57,24 @@ impl From<(String, BeforeText)> for TextPreview { } impl TextPreview { /// Formats the text preview with the appropriate preceding username. - pub fn format_with( - self, - username: &str, - as_html: bool, - ) -> String { + pub fn format_with(self, username: &str, as_html: bool) -> String { let Self { text, before_text } = self; match before_text { BeforeText::Nothing => text, - BeforeText::UsernameWithColon => if as_html { - format!("{}: {}", htmlize::escape_text(username), text) - } else { - format!("{}: {}", username, text) - }, + BeforeText::UsernameWithColon => { + if as_html { + format!("{}: {}", htmlize::escape_text(username), text) + } else { + format!("{}: {}", username, text) + } + } BeforeText::UsernameWithoutColon => format!( "{} {}", - if as_html { htmlize::escape_text(username) } else { username.into() }, + if as_html { + htmlize::escape_text(username) + } else { + username.into() + }, text, ), } @@ -67,52 +88,53 @@ pub fn text_preview_of_timeline_item( sender_username: &str, ) -> TextPreview { match content { - TimelineItemContent::MsgLike(msg_like_content) => { - match &msg_like_content.kind { - MsgLikeKind::Message(msg) => text_preview_of_message(msg.msgtype(), sender_username), - MsgLikeKind::Sticker(sticker) => TextPreview::from(( - format!("[Sticker]: {}", htmlize::escape_text(&sticker.content().body)), - BeforeText::UsernameWithColon, - )), - MsgLikeKind::Poll(poll_state) => TextPreview::from(( - format!( - "[Poll]: {}", - htmlize::escape_text( - poll_state.fallback_text() - .unwrap_or_else(|| poll_state.results().question) - ), + TimelineItemContent::MsgLike(msg_like_content) => match &msg_like_content.kind { + MsgLikeKind::Message(msg) => text_preview_of_message(msg.msgtype(), sender_username), + MsgLikeKind::Sticker(sticker) => TextPreview::from(( + format!( + "[Sticker]: {}", + htmlize::escape_text(&sticker.content().body) + ), + BeforeText::UsernameWithColon, + )), + MsgLikeKind::Poll(poll_state) => TextPreview::from(( + format!( + "[Poll]: {}", + htmlize::escape_text( + poll_state + .fallback_text() + .unwrap_or_else(|| poll_state.results().question) ), - BeforeText::UsernameWithColon, - )), - MsgLikeKind::Redacted => { - let mut preview = text_preview_of_redacted_message( - None, - sender_user_id, - sender_username, - ); - preview.text = htmlize::escape_text(&preview.text).into(); - preview - } - MsgLikeKind::UnableToDecrypt(em) => text_preview_of_encrypted_message(em), - MsgLikeKind::Other(oml) => text_preview_of_other_message_like(oml), + ), + BeforeText::UsernameWithColon, + )), + MsgLikeKind::Redacted => { + let mut preview = + text_preview_of_redacted_message(None, sender_user_id, sender_username); + preview.text = htmlize::escape_text(&preview.text).into(); + preview } - } + MsgLikeKind::UnableToDecrypt(em) => text_preview_of_encrypted_message(em), + MsgLikeKind::Other(oml) => text_preview_of_other_message_like(oml), + }, TimelineItemContent::MembershipChange(membership_change) => { - text_preview_of_room_membership_change(membership_change, true) - .unwrap_or_else(|| TextPreview::from(( + text_preview_of_room_membership_change(membership_change, true).unwrap_or_else(|| { + TextPreview::from(( String::from("underwent a membership change"), BeforeText::UsernameWithoutColon, - ))) + )) + }) } TimelineItemContent::ProfileChange(profile_change) => { text_preview_of_member_profile_change(profile_change, sender_username, true) } TimelineItemContent::OtherState(other_state) => { - text_preview_of_other_state(other_state, true) - .unwrap_or_else(|| TextPreview::from(( + text_preview_of_other_state(other_state, true).unwrap_or_else(|| { + TextPreview::from(( String::from("initiated another state change"), BeforeText::UsernameWithoutColon, - ))) + )) + }) } TimelineItemContent::FailedToParseMessageLike { event_type, .. } => TextPreview::from(( format!("[Failed to parse {} message]", event_type), @@ -133,83 +155,94 @@ pub fn text_preview_of_timeline_item( } } - - /// Returns the plaintext `body` of the given timeline event. -pub fn plaintext_body_of_timeline_item( - event_tl_item: &EventTimelineItem, -) -> String { +pub fn plaintext_body_of_timeline_item(event_tl_item: &EventTimelineItem) -> String { match event_tl_item.content() { - TimelineItemContent::MsgLike(msg_likecontent) => { - match &msg_likecontent.kind { - MsgLikeKind::Message(msg) => { - msg.body().into() - } - MsgLikeKind::Sticker(sticker) => { - sticker.content().body.clone() - } - MsgLikeKind::Poll(poll_state) => { - format!("[Poll]: {}", - poll_state.fallback_text().unwrap_or_else(|| poll_state.results().question) - ) - } - MsgLikeKind::Redacted => { - let sender_username = utils::get_or_fetch_event_sender(event_tl_item, None); - text_preview_of_redacted_message( - event_tl_item.latest_json(), - event_tl_item.sender(), - &sender_username, - ).format_with(&sender_username, false) - } - MsgLikeKind::UnableToDecrypt(em) => { - text_preview_of_encrypted_message(em) - .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false) - } - MsgLikeKind::Other(other_msg_like) => { - text_preview_of_other_message_like(other_msg_like) - .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false)} + TimelineItemContent::MsgLike(msg_likecontent) => match &msg_likecontent.kind { + MsgLikeKind::Message(msg) => msg.body().into(), + MsgLikeKind::Sticker(sticker) => sticker.content().body.clone(), + MsgLikeKind::Poll(poll_state) => { + format!( + "[Poll]: {}", + poll_state + .fallback_text() + .unwrap_or_else(|| poll_state.results().question) + ) } - } + MsgLikeKind::Redacted => { + let sender_username = utils::get_or_fetch_event_sender(event_tl_item, None); + text_preview_of_redacted_message( + event_tl_item.latest_json(), + event_tl_item.sender(), + &sender_username, + ) + .format_with(&sender_username, false) + } + MsgLikeKind::UnableToDecrypt(em) => text_preview_of_encrypted_message(em).format_with( + &utils::get_or_fetch_event_sender(event_tl_item, None), + false, + ), + MsgLikeKind::Other(other_msg_like) => { + text_preview_of_other_message_like(other_msg_like).format_with( + &utils::get_or_fetch_event_sender(event_tl_item, None), + false, + ) + } + }, TimelineItemContent::MembershipChange(membership_change) => { text_preview_of_room_membership_change(membership_change, false) - .unwrap_or_else(|| TextPreview::from(( - String::from("underwent a membership change."), - BeforeText::UsernameWithoutColon, - ))) - .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false) + .unwrap_or_else(|| { + TextPreview::from(( + String::from("underwent a membership change."), + BeforeText::UsernameWithoutColon, + )) + }) + .format_with( + &utils::get_or_fetch_event_sender(event_tl_item, None), + false, + ) } TimelineItemContent::ProfileChange(profile_change) => { text_preview_of_member_profile_change( profile_change, &utils::get_or_fetch_event_sender(event_tl_item, None), false, - ).text + ) + .text } TimelineItemContent::OtherState(other_state) => { text_preview_of_other_state(other_state, false) - .unwrap_or_else(|| TextPreview::from(( - String::from("initiated another state change."), - BeforeText::UsernameWithoutColon, - ))) - .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false) + .unwrap_or_else(|| { + TextPreview::from(( + String::from("initiated another state change."), + BeforeText::UsernameWithoutColon, + )) + }) + .format_with( + &utils::get_or_fetch_event_sender(event_tl_item, None), + false, + ) } TimelineItemContent::FailedToParseMessageLike { event_type, error } => { format!("Failed to parse {} message. Error: {}", event_type, error) } - TimelineItemContent::FailedToParseState { event_type, error, state_key } => { - format!("Failed to parse {} state; key: {}. Error: {}", event_type, state_key, error) + TimelineItemContent::FailedToParseState { + event_type, + error, + state_key, + } => { + format!( + "Failed to parse {} state; key: {}. Error: {}", + event_type, state_key, error + ) } TimelineItemContent::CallInvite => String::from("[Call Invitation]"), TimelineItemContent::RtcNotification => String::from("[RTC Call Notification]"), } } - /// Returns a text preview of the given message as an Html-formatted string. -fn text_preview_of_message( - msg: &MessageType, - sender_username: &str, -) -> TextPreview { +fn text_preview_of_message(msg: &MessageType, sender_username: &str) -> TextPreview { let text = match msg { MessageType::Audio(audio) => format!( "[Audio]: {}", @@ -248,7 +281,8 @@ fn text_preview_of_message( "[Location]: {}", htmlize::escape_text(&location.body), ), - MessageType::Notice(notice) => format!("{}", + MessageType::Notice(notice) => format!( + "{}", if let Some(formatted_body) = notice.formatted.as_ref() { utils::trim_start_html_whitespace(&formatted_body.body).into() } else { @@ -260,38 +294,32 @@ fn text_preview_of_message( notice.server_notice_type.as_str(), notice.body, ), - MessageType::Text(text) => { - text.formatted - .as_ref() - .and_then(|fb| - (fb.format == MessageFormat::Html).then(|| { - let filtered_and_trimmed = utils::trim_start_html_whitespace( - utils::remove_mx_reply(&fb.body) - ); - utils::linkify(filtered_and_trimmed, true).to_string() - }) - ) - .unwrap_or_else(|| match utils::linkify(&text.body, false) { - Cow::Borrowed(plaintext) => htmlize::escape_text(plaintext).to_string(), - Cow::Owned(linkified) => linkified, + MessageType::Text(text) => text + .formatted + .as_ref() + .and_then(|fb| { + (fb.format == MessageFormat::Html).then(|| { + let filtered_and_trimmed = + utils::trim_start_html_whitespace(utils::remove_mx_reply(&fb.body)); + utils::linkify(filtered_and_trimmed, true).to_string() }) + }) + .unwrap_or_else(|| match utils::linkify(&text.body, false) { + Cow::Borrowed(plaintext) => htmlize::escape_text(plaintext).to_string(), + Cow::Owned(linkified) => linkified, + }), + MessageType::VerificationRequest(verification) => { + format!("[Verification Request] to user {}", verification.to,) } - MessageType::VerificationRequest(verification) => format!( - "[Verification Request] to user {}", - verification.to, - ), MessageType::Video(video) => format!( "[Video]: {}", if let Some(formatted_body) = video.formatted.as_ref() { - Cow::Borrowed(formatted_body.body.as_str()) + Cow::Borrowed(formatted_body.body.as_str()) } else { htmlize::escape_text(&video.body) } ), - MessageType::_Custom(custom) => format!( - "[Custom message]: {:?}", - custom, - ), + MessageType::_Custom(custom) => format!("[Custom message]: {:?}", custom,), other => format!( "[Unknown message type]: {}", htmlize::escape_text(other.body()), @@ -306,20 +334,19 @@ pub fn text_preview_of_raw_timeline_event( sender_username: &str, ) -> Option { match raw_event.deserialize().ok()? { - AnySyncTimelineEvent::MessageLike( - AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Original(ev) - ) - ) => Some(text_preview_of_message( + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Original(ev), + )) => Some(text_preview_of_message( &ev.content.msgtype, sender_username, )), - AnySyncTimelineEvent::MessageLike( - AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Redacted(_) - ) - ) => { - let sender_user_id = raw_event.get_field::("sender").ok().flatten()?; + AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(_), + )) => { + let sender_user_id = raw_event + .get_field::("sender") + .ok() + .flatten()?; Some(text_preview_of_redacted_message( Some(raw_event), sender_user_id.as_ref(), @@ -330,7 +357,6 @@ pub fn text_preview_of_raw_timeline_event( } } - /// Returns a plaintext preview of the given redacted message. /// /// Note: this function accepts the component parts of an [`EventTimelineItem`] @@ -345,32 +371,38 @@ pub fn text_preview_of_redacted_message( ) -> TextPreview { let mut redactor_and_reason = None; if let Some(redacted_msg) = latest_json { - if let Ok(AnySyncTimelineEvent::MessageLike( - AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Redacted(redaction) - ) - )) = redacted_msg.deserialize() { + if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(redaction), + ))) = redacted_msg.deserialize() + { if let Ok(redacted_because) = redaction.unsigned.redacted_because.deserialize() { - redactor_and_reason = Some(( - redacted_because.sender, - redacted_because.content.reason, - )); + redactor_and_reason = + Some((redacted_because.sender, redacted_because.content.reason)); } } } let text = match redactor_and_reason { Some((redactor, Some(reason))) => { if redactor == sender_user_id { - format!("{} deleted their own message: \"{}\".", original_sender_username, reason) + format!( + "{} deleted their own message: \"{}\".", + original_sender_username, reason + ) } else { - format!("{} deleted {}'s message: \"{}\".", redactor, original_sender_username, reason) + format!( + "{} deleted {}'s message: \"{}\".", + redactor, original_sender_username, reason + ) } } Some((redactor, None)) => { if redactor == sender_user_id { format!("{} deleted their own message.", original_sender_username) } else { - format!("{} deleted {}'s message.", redactor, original_sender_username) + format!( + "{} deleted {}'s message.", + redactor, original_sender_username + ) } } None => { @@ -380,42 +412,31 @@ pub fn text_preview_of_redacted_message( TextPreview::from((text, BeforeText::Nothing)) } - /// Returns a plaintext preview of the given encrypted message that could not be decrypted. /// /// This is used for "Unable to decrypt" messages, which may have a known cause /// for why they could not be decrypted. -pub fn text_preview_of_encrypted_message( - encrypted_message: &EncryptedMessage, -) -> TextPreview { +pub fn text_preview_of_encrypted_message(encrypted_message: &EncryptedMessage) -> TextPreview { let cause_str = match encrypted_message { EncryptedMessage::MegolmV1AesSha2 { cause, .. } => match cause { UtdCause::Unknown => None, - UtdCause::SentBeforeWeJoined => Some( - "this message was sent before you joined the room." - ), - UtdCause::VerificationViolation => Some( - "this message was sent by an unverified user." - ), - UtdCause::UnsignedDevice => Some( - "the sending device wasn't signed by its owner." - ), - UtdCause::UnknownDevice => Some( - "the sending device's signature was not found." - ), + UtdCause::SentBeforeWeJoined => { + Some("this message was sent before you joined the room.") + } + UtdCause::VerificationViolation => Some("this message was sent by an unverified user."), + UtdCause::UnsignedDevice => Some("the sending device wasn't signed by its owner."), + UtdCause::UnknownDevice => Some("the sending device's signature was not found."), UtdCause::HistoricalMessageAndBackupIsDisabled => Some( - "historical messages are not available on this device because server-side key backup was disabled." - ), - UtdCause::WithheldForUnverifiedOrInsecureDevice => Some( - "your device doesn't meet the sender's security requirements." + "historical messages are not available on this device because server-side key backup was disabled.", ), - UtdCause::WithheldBySender => Some( - "the sender withheld this message from you." - ), - UtdCause::HistoricalMessageAndDeviceIsUnverified => Some( - "historical messages are not available; you must verify this device." - ), - } + UtdCause::WithheldForUnverifiedOrInsecureDevice => { + Some("your device doesn't meet the sender's security requirements.") + } + UtdCause::WithheldBySender => Some("the sender withheld this message from you."), + UtdCause::HistoricalMessageAndDeviceIsUnverified => { + Some("historical messages are not available; you must verify this device.") + } + }, _ => None, }; let text = if let Some(cause) = cause_str { @@ -427,9 +448,7 @@ pub fn text_preview_of_encrypted_message( } /// Returns a plaintext preview of the given other message-like event. -pub fn text_preview_of_other_message_like( - other_msg_like: &OtherMessageLike, -) -> TextPreview { +pub fn text_preview_of_other_message_like(other_msg_like: &OtherMessageLike) -> TextPreview { TextPreview::from(( format!("[Other message type: {}]", other_msg_like.event_type()), BeforeText::UsernameWithColon, @@ -442,7 +461,10 @@ pub fn text_preview_of_other_state( format_as_html: bool, ) -> Option { let text = match other_state.content() { - AnyOtherFullStateEventContent::RoomAliases(FullStateEventContent::Original { content, .. }) => { + AnyOtherFullStateEventContent::RoomAliases(FullStateEventContent::Original { + content, + .. + }) => { let mut s = String::from("set this room's aliases to "); let last_alias = content.aliases.len() - 1; for (i, alias) in content.aliases.iter().enumerate() { @@ -457,50 +479,76 @@ pub fn text_preview_of_other_state( AnyOtherFullStateEventContent::RoomAvatar(_) => { Some(String::from("set this room's avatar picture.")) } - AnyOtherFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original { content, .. }) => { - Some(format!("set the main address of this room to {}.", - content.alias.as_ref().map(|a| a.as_str()).unwrap_or("none") - )) - } - AnyOtherFullStateEventContent::RoomCreate(FullStateEventContent::Original { content, .. }) => { - Some(format!("created this room (v{}).", content.room_version.as_str())) - } + AnyOtherFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original { + content, + .. + }) => Some(format!( + "set the main address of this room to {}.", + content.alias.as_ref().map(|a| a.as_str()).unwrap_or("none") + )), + AnyOtherFullStateEventContent::RoomCreate(FullStateEventContent::Original { + content, + .. + }) => Some(format!( + "created this room (v{}).", + content.room_version.as_str() + )), AnyOtherFullStateEventContent::RoomEncryption(_) => { Some(String::from("enabled encryption in this room.")) } - AnyOtherFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original { content, .. }) => { - Some(match &content.guest_access { - GuestAccess::CanJoin => String::from("has allowed guests to join this room."), - GuestAccess::Forbidden => String::from("has forbidden guests from joining this room."), - custom => format!("has set custom guest access rules for this room: {}", custom.as_str()), - }) - } - AnyOtherFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original { content, .. }) => { - Some(format!("set this room's history to be visible by {}", - match &content.history_visibility { - HistoryVisibility::Invited => "invited users, since they were invited.", - HistoryVisibility::Joined => "joined users, since they joined.", - HistoryVisibility::Shared => "joined users, for all of time.", - HistoryVisibility::WorldReadable => "anyone for all time.", - custom => custom.as_str(), - }, - )) - } - AnyOtherFullStateEventContent::RoomJoinRules(FullStateEventContent::Original { content, .. }) => { - Some(match &content.join_rule { - JoinRule::Public => String::from("set this room to be joinable by anyone."), - JoinRule::Knock => String::from("set this room to be joinable by invite only or by request."), - JoinRule::Private => String::from("set this room to be private."), - JoinRule::Restricted(_) => String::from("set this room to be joinable by invite only or with restrictions."), - JoinRule::KnockRestricted(_) => String::from("set this room to be joinable by invite only or requestable with restrictions."), - JoinRule::Invite => String::from("set this room to be joinable by invite only."), - custom => format!("set custom join rules for this room: {}", custom.as_str()), - }) - } - AnyOtherFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { content, .. }) => { - Some(format!("pinned {} events in this room.", content.pinned.len())) - } - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => { + AnyOtherFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original { + content, + .. + }) => Some(match &content.guest_access { + GuestAccess::CanJoin => String::from("has allowed guests to join this room."), + GuestAccess::Forbidden => String::from("has forbidden guests from joining this room."), + custom => format!( + "has set custom guest access rules for this room: {}", + custom.as_str() + ), + }), + AnyOtherFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original { + content, + .. + }) => Some(format!( + "set this room's history to be visible by {}", + match &content.history_visibility { + HistoryVisibility::Invited => "invited users, since they were invited.", + HistoryVisibility::Joined => "joined users, since they joined.", + HistoryVisibility::Shared => "joined users, for all of time.", + HistoryVisibility::WorldReadable => "anyone for all time.", + custom => custom.as_str(), + }, + )), + AnyOtherFullStateEventContent::RoomJoinRules(FullStateEventContent::Original { + content, + .. + }) => Some(match &content.join_rule { + JoinRule::Public => String::from("set this room to be joinable by anyone."), + JoinRule::Knock => { + String::from("set this room to be joinable by invite only or by request.") + } + JoinRule::Private => String::from("set this room to be private."), + JoinRule::Restricted(_) => { + String::from("set this room to be joinable by invite only or with restrictions.") + } + JoinRule::KnockRestricted(_) => String::from( + "set this room to be joinable by invite only or requestable with restrictions.", + ), + JoinRule::Invite => String::from("set this room to be joinable by invite only."), + custom => format!("set custom join rules for this room: {}", custom.as_str()), + }), + AnyOtherFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { + content, + .. + }) => Some(format!( + "pinned {} events in this room.", + content.pinned.len() + )), + AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { + content, + .. + }) => { let name = if format_as_html { htmlize::escape_text(&content.name) } else { @@ -511,13 +559,20 @@ pub fn text_preview_of_other_state( AnyOtherFullStateEventContent::RoomPowerLevels(_) => { Some(String::from("set the power levels for this room.")) } - AnyOtherFullStateEventContent::RoomServerAcl(_) => { - Some(String::from("set the server access control list for this room.")) - } - AnyOtherFullStateEventContent::RoomTombstone(FullStateEventContent::Original { content, .. }) => { - Some(format!("closed this room and upgraded it to {}", content.replacement_room.matrix_to_uri())) - } - AnyOtherFullStateEventContent::RoomTopic(FullStateEventContent::Original { content, .. }) => { + AnyOtherFullStateEventContent::RoomServerAcl(_) => Some(String::from( + "set the server access control list for this room.", + )), + AnyOtherFullStateEventContent::RoomTombstone(FullStateEventContent::Original { + content, + .. + }) => Some(format!( + "closed this room and upgraded it to {}", + content.replacement_room.matrix_to_uri() + )), + AnyOtherFullStateEventContent::RoomTopic(FullStateEventContent::Original { + content, + .. + }) => { let topic = if format_as_html { htmlize::escape_text(&content.topic) } else { @@ -526,7 +581,7 @@ pub fn text_preview_of_other_state( Some(format!("changed this room's topic to \"{topic}\".")) } AnyOtherFullStateEventContent::SpaceParent(_) => { - let state_key = if format_as_html { + let state_key = if format_as_html { htmlize::escape_text(other_state.state_key()) } else { Cow::Borrowed(other_state.state_key()) @@ -534,7 +589,7 @@ pub fn text_preview_of_other_state( Some(format!("set this room's parent space to \"{state_key}\".")) } AnyOtherFullStateEventContent::SpaceChild(_) => { - let state_key = if format_as_html { + let state_key = if format_as_html { htmlize::escape_text(other_state.state_key()) } else { Cow::Borrowed(other_state.state_key()) @@ -549,7 +604,6 @@ pub fn text_preview_of_other_state( text.map(|t| TextPreview::from((t, BeforeText::UsernameWithoutColon))) } - /// Returns a text preview of the given member profile change /// as a plaintext or HTML-formatted string. pub fn text_preview_of_member_profile_change( @@ -559,9 +613,17 @@ pub fn text_preview_of_member_profile_change( ) -> TextPreview { let name_text = if let Some(name_change) = change.displayname_change() { let old = name_change.old.as_deref().unwrap_or(username); - let old_un = if format_as_html { htmlize::escape_text(old) } else { old.into() }; + let old_un = if format_as_html { + htmlize::escape_text(old) + } else { + old.into() + }; if let Some(new) = name_change.new.as_ref() { - let new_un = if format_as_html { htmlize::escape_text(new) } else { new.into() }; + let new_un = if format_as_html { + htmlize::escape_text(new) + } else { + new.into() + }; format!("{old_un} changed their display name to \"{new_un}\"") } else { format!("{old_un} removed their display name") @@ -590,7 +652,6 @@ pub fn text_preview_of_member_profile_change( )) } - /// Returns a text preview of the given room membership change /// as a plaintext or HTML-formatted string. pub fn text_preview_of_room_membership_change( @@ -598,8 +659,7 @@ pub fn text_preview_of_room_membership_change( format_as_html: bool, ) -> Option { let dn = change.display_name(); - let change_user_id = dn.as_deref() - .unwrap_or_else(|| change.user_id().as_str()); + let change_user_id = dn.as_deref().unwrap_or_else(|| change.user_id().as_str()); let change_user_id = if format_as_html { htmlize::escape_text(change_user_id) } else { @@ -613,34 +673,34 @@ pub fn text_preview_of_room_membership_change( // Don't actually display anything for nonexistent/unimportant membership changes. return None; } - Some(MembershipChange::Joined) => - String::from("joined this room."), - Some(MembershipChange::Left) => - String::from("left this room."), - Some(MembershipChange::Banned) => - format!("banned {} from this room.", change_user_id), - Some(MembershipChange::Unbanned) => - format!("unbanned {} from this room.", change_user_id), - Some(MembershipChange::Kicked) => - format!("kicked {} from this room.", change_user_id), - Some(MembershipChange::Invited) => - format!("invited {} to this room.", change_user_id), - Some(MembershipChange::KickedAndBanned) => - format!("kicked and banned {} from this room.", change_user_id), - Some(MembershipChange::InvitationAccepted) => - String::from("accepted an invitation to this room."), - Some(MembershipChange::InvitationRejected) => - String::from("rejected an invitation to this room."), - Some(MembershipChange::InvitationRevoked) => - format!("revoked {}'s invitation to this room.", change_user_id), - Some(MembershipChange::Knocked) => - String::from("requested to join this room."), - Some(MembershipChange::KnockAccepted) => - format!("accepted {}'s request to join this room.", change_user_id), - Some(MembershipChange::KnockRetracted) => - String::from("retracted their request to join this room."), - Some(MembershipChange::KnockDenied) => - format!("denied {}'s request to join this room.", change_user_id), + Some(MembershipChange::Joined) => String::from("joined this room."), + Some(MembershipChange::Left) => String::from("left this room."), + Some(MembershipChange::Banned) => format!("banned {} from this room.", change_user_id), + Some(MembershipChange::Unbanned) => format!("unbanned {} from this room.", change_user_id), + Some(MembershipChange::Kicked) => format!("kicked {} from this room.", change_user_id), + Some(MembershipChange::Invited) => format!("invited {} to this room.", change_user_id), + Some(MembershipChange::KickedAndBanned) => { + format!("kicked and banned {} from this room.", change_user_id) + } + Some(MembershipChange::InvitationAccepted) => { + String::from("accepted an invitation to this room.") + } + Some(MembershipChange::InvitationRejected) => { + String::from("rejected an invitation to this room.") + } + Some(MembershipChange::InvitationRevoked) => { + format!("revoked {}'s invitation to this room.", change_user_id) + } + Some(MembershipChange::Knocked) => String::from("requested to join this room."), + Some(MembershipChange::KnockAccepted) => { + format!("accepted {}'s request to join this room.", change_user_id) + } + Some(MembershipChange::KnockRetracted) => { + String::from("retracted their request to join this room.") + } + Some(MembershipChange::KnockDenied) => { + format!("denied {}'s request to join this room.", change_user_id) + } }; Some(TextPreview::from((text, BeforeText::UsernameWithoutColon))) } diff --git a/src/home/add_room.rs b/src/home/add_room.rs index 981369897..cc909213e 100644 --- a/src/home/add_room.rs +++ b/src/home/add_room.rs @@ -1,11 +1,24 @@ //! A top-level view for adding (joining) or exploring new rooms and spaces. - use makepad_widgets::*; use matrix_sdk::RoomState; -use ruma::{IdParseError, MatrixToUri, MatrixUri, OwnedRoomOrAliasId, OwnedServerName, matrix_uri::MatrixId, room::{JoinRuleSummary, RoomType}}; - -use crate::{app::AppStateAction, home::invite_screen::JoinRoomResultAction, room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{avatar::AvatarWidgetRefExt, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, submit_async_request}, utils}; +use ruma::{ + IdParseError, MatrixToUri, MatrixUri, OwnedRoomOrAliasId, OwnedServerName, + matrix_uri::MatrixId, + room::{JoinRuleSummary, RoomType}, +}; + +use crate::{ + app::AppStateAction, + home::invite_screen::JoinRoomResultAction, + room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, + shared::{ + avatar::AvatarWidgetRefExt, + popup_list::{PopupKind, enqueue_popup_notification}, + }, + sliding_sync::{MatrixRequest, submit_async_request}, + utils, +}; script_mod! { use mod.prelude.widgets.* @@ -32,7 +45,7 @@ script_mod! { text_style: theme.font_regular {font_size: 18}, } } - + LineH { padding: 10, margin: Inset{top: 10, right: 2} } SubsectionLabel { @@ -248,16 +261,19 @@ script_mod! { } } } - + } } #[derive(Script, ScriptHook, Widget)] pub struct AddRoomScreen { - #[deref] view: View, - #[rust] state: AddRoomState, + #[deref] + view: View, + #[rust] + state: AddRoomState, /// The function to perform when the user clicks the `join_room_button`. - #[rust(JoinButtonFunction::None)] join_function: JoinButtonFunction, + #[rust(JoinButtonFunction::None)] + join_function: JoinButtonFunction, } #[derive(Default)] @@ -286,20 +302,16 @@ enum AddRoomState { FetchError(String), /// We successfully knocked on the room or space, and are waiting for /// a member of that room/space to acknowledge our knock by inviting us. - Knocked { - frp: FetchedRoomPreview, - }, + Knocked { frp: FetchedRoomPreview }, /// We successfully joined the room or space, and are waiting for it /// to be loaded from the homeserver. - Joined { - frp: FetchedRoomPreview, - }, + Joined { frp: FetchedRoomPreview }, /// The fetched room or space has been loaded from the homeserver, /// so we can allow the user to jump to it via the `join_room_button`. Loaded { frp: FetchedRoomPreview, is_invite: bool, - } + }, } impl AddRoomState { fn fetched_room_preview(&self) -> Option<&FetchedRoomPreview> { @@ -333,9 +345,7 @@ impl AddRoomState { fn transition_to_loaded(&mut self, is_invite: bool) { let prev = std::mem::take(self); match prev { - Self::FetchedRoomPreview { frp, .. } - | Self::Joined { frp } - | Self::Knocked { frp } => { + Self::FetchedRoomPreview { frp, .. } | Self::Joined { frp } | Self::Knocked { frp } => { *self = Self::Loaded { frp, is_invite }; } _ => { @@ -348,12 +358,16 @@ impl AddRoomState { impl Widget for AddRoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - + if let Event::Actions(actions) = event { let room_alias_id_input = self.view.text_input(cx, ids!(room_alias_id_input)); let search_for_room_button = self.view.button(cx, ids!(search_for_room_button)); - let cancel_button = self.view.button(cx, ids!(fetched_room_summary.buttons_view.cancel_button)); - let join_room_button = self.view.button(cx, ids!(fetched_room_summary.buttons_view.join_room_button)); + let cancel_button = self + .view + .button(cx, ids!(fetched_room_summary.buttons_view.cancel_button)); + let join_room_button = self + .view + .button(cx, ids!(fetched_room_summary.buttons_view.join_room_button)); // Enable or disable the button based on if the text input is empty. if let Some(text) = room_alias_id_input.changed(actions) { @@ -373,7 +387,8 @@ impl Widget for AddRoomScreen { match (&self.join_function, &self.state) { ( JoinButtonFunction::NavigateOrJoin, - AddRoomState::FetchedRoomPreview { frp, .. } | AddRoomState::Loaded { frp, .. } + AddRoomState::FetchedRoomPreview { frp, .. } + | AddRoomState::Loaded { frp, .. }, ) => { cx.action(AppStateAction::NavigateToRoom { room_to_close: None, @@ -382,23 +397,28 @@ impl Widget for AddRoomScreen { } ( JoinButtonFunction::Knock, - AddRoomState::FetchedRoomPreview { frp, room_or_alias_id, via } + AddRoomState::FetchedRoomPreview { + frp, + room_or_alias_id, + via, + }, ) => { submit_async_request(MatrixRequest::Knock { - room_or_alias_id: frp.canonical_alias.clone().map_or_else( - || room_or_alias_id.clone(), - Into::into - ), + room_or_alias_id: frp + .canonical_alias + .clone() + .map_or_else(|| room_or_alias_id.clone(), Into::into), reason: None, server_names: via.clone(), }); } - _ => { } + _ => {} } } // If the button was clicked or enter was pressed, try to parse the room address. - let new_room_query = search_for_room_button.clicked(actions) + let new_room_query = search_for_room_button + .clicked(actions) .then(|| room_alias_id_input.text()) .or_else(|| room_alias_id_input.returned(actions).map(|(t, _)| t)); if let Some(t) = new_room_query { @@ -408,15 +428,16 @@ impl Widget for AddRoomScreen { room_or_alias_id: room_or_alias_id.clone(), via: via.clone(), }; - submit_async_request(MatrixRequest::GetRoomPreview { room_or_alias_id, via }); + submit_async_request(MatrixRequest::GetRoomPreview { + room_or_alias_id, + via, + }); } Err(e) => { - let err_str = format!("Could not parse the text as a valid room address.\nError: {e}."); - enqueue_popup_notification( - err_str.clone(), - PopupKind::Error, - None, + let err_str = format!( + "Could not parse the text as a valid room address.\nError: {e}." ); + enqueue_popup_notification(err_str.clone(), PopupKind::Error, None); self.state = AddRoomState::ParseError(err_str); room_alias_id_input.set_key_focus(cx); } @@ -426,7 +447,11 @@ impl Widget for AddRoomScreen { // If we're waiting for the room preview to be fetched (i.e., in the Parsed state), // then check if we've received it via an action. - if let AddRoomState::Parsed { room_or_alias_id, via } = &self.state { + if let AddRoomState::Parsed { + room_or_alias_id, + via, + } = &self.state + { for action in actions { match action.downcast_ref() { Some(RoomPreviewAction::Fetched(Ok(frp))) => { @@ -445,11 +470,7 @@ impl Widget for AddRoomScreen { } Some(RoomPreviewAction::Fetched(Err(e))) => { let err_str = format!("Failed to fetch room info.\n\nError: {e}."); - enqueue_popup_notification( - err_str.clone(), - PopupKind::Error, - None, - ); + enqueue_popup_notification(err_str.clone(), PopupKind::Error, None); self.state = AddRoomState::FetchError(err_str); self.redraw(cx); break; @@ -459,28 +480,40 @@ impl Widget for AddRoomScreen { } } - // If we've fetched and displayed the room preview, handle any responses to // the user clicking the join button (e.g., knocked on or joined the room/space). let mut transition_to_knocked = false; - let mut transition_to_joined = false; - if let AddRoomState::FetchedRoomPreview { frp, room_or_alias_id, .. } = &self.state { + let mut transition_to_joined = false; + if let AddRoomState::FetchedRoomPreview { + frp, + room_or_alias_id, + .. + } = &self.state + { for action in actions { match action.downcast_ref() { - Some(KnockResultAction::Knocked { room, .. }) if room.room_id() == frp.room_name_id.room_id() => { + Some(KnockResultAction::Knocked { room, .. }) + if room.room_id() == frp.room_name_id.room_id() => + { let room_type = match room.room_type() { Some(RoomType::Space) => "space", _ => "room", }; enqueue_popup_notification( - format!("Successfully knocked on {room_type} {}.", frp.room_name_id), + format!( + "Successfully knocked on {room_type} {}.", + frp.room_name_id + ), PopupKind::Success, Some(4.0), ); transition_to_knocked = true; break; } - Some(KnockResultAction::Failed { error, room_or_alias_id: roai }) if room_or_alias_id == roai => { + Some(KnockResultAction::Failed { + error, + room_or_alias_id: roai, + }) if room_or_alias_id == roai => { enqueue_popup_notification( format!("Failed to knock on room.\n\nError: {error}."), PopupKind::Error, @@ -488,11 +521,13 @@ impl Widget for AddRoomScreen { ); break; } - _ => { } + _ => {} } match action.downcast_ref() { - Some(JoinRoomResultAction::Joined { room_id }) if room_id == frp.room_name_id.room_id() => { + Some(JoinRoomResultAction::Joined { room_id }) + if room_id == frp.room_name_id.room_id() => + { let room_type = match &frp.room_type { Some(RoomType::Space) => "space", _ => "room", @@ -505,7 +540,9 @@ impl Widget for AddRoomScreen { transition_to_joined = true; break; } - Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == frp.room_name_id.room_id() => { + Some(JoinRoomResultAction::Failed { room_id, error }) + if room_id == frp.room_name_id.room_id() => + { enqueue_popup_notification( format!("Failed to join room.\n\nError: {error}."), PopupKind::Error, @@ -529,9 +566,17 @@ impl Widget for AddRoomScreen { for action in actions { // If the room/space the user is searching for has been loaded from the homeserver // (e.g., by getting invited to it, or joining it in another client), - // then update the state of - if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, is_invite }) = action.downcast_ref() { - if self.state.fetched_room_preview().is_some_and(|frp| frp.room_name_id.room_id() == room_name_id.room_id()) { + // then update the state of + if let Some(AppStateAction::RoomLoadedSuccessfully { + room_name_id, + is_invite, + }) = action.downcast_ref() + { + if self + .state + .fetched_room_preview() + .is_some_and(|frp| frp.room_name_id.room_id() == room_name_id.room_id()) + { self.state.transition_to_loaded(*is_invite); self.redraw(cx); } @@ -540,7 +585,6 @@ impl Widget for AddRoomScreen { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { let loading_room_view = self.view.view(cx, ids!(loading_room_view)); let fetched_room_summary = self.view.view(cx, ids!(fetched_room_summary)); @@ -554,22 +598,23 @@ impl Widget for AddRoomScreen { } AddRoomState::ParseError(err_str) | AddRoomState::FetchError(err_str) => { loading_room_view.set_visible(cx, false); - fetched_room_summary.set_visible(cx, false); + fetched_room_summary.set_visible(cx, false); error_view.set_visible(cx, true); error_view.label(cx, ids!(error_text)).set_text(cx, err_str); } - AddRoomState::Parsed { room_or_alias_id, .. } => { + AddRoomState::Parsed { + room_or_alias_id, .. + } => { loading_room_view.set_visible(cx, true); - loading_room_view.label(cx, ids!(loading_text)).set_text( - cx, - &format!("Fetching {room_or_alias_id}..."), - ); - fetched_room_summary.set_visible(cx, false); + loading_room_view + .label(cx, ids!(loading_text)) + .set_text(cx, &format!("Fetching {room_or_alias_id}...")); + fetched_room_summary.set_visible(cx, false); error_view.set_visible(cx, false); } - ars @ AddRoomState::FetchedRoomPreview { frp, .. } + ars @ AddRoomState::FetchedRoomPreview { frp, .. } | ars @ AddRoomState::Knocked { frp } - | ars @ AddRoomState::Joined { frp } + | ars @ AddRoomState::Joined { frp } | ars @ AddRoomState::Loaded { frp, .. } => { loading_room_view.set_visible(cx, false); fetched_room_summary.set_visible(cx, true); @@ -582,11 +627,9 @@ impl Widget for AddRoomScreen { room_avatar.show_text(cx, None, None, text); } FetchedRoomAvatar::Image(image_data) => { - let res = room_avatar.show_image( - cx, - None, - |cx, img_ref| utils::load_png_or_jpg(&img_ref, cx, image_data), - ); + let res = room_avatar.show_image(cx, None, |cx, img_ref| { + utils::load_png_or_jpg(&img_ref, cx, image_data) + }); if res.is_err() { room_avatar.show_text( cx, @@ -605,55 +648,75 @@ impl Widget for AddRoomScreen { let room_name = fetched_room_summary.label(cx, ids!(room_name)); match frp.room_name_id.name_for_avatar() { Some(n) => room_name.set_text(cx, n), - _ => room_name.set_text(cx, &format!("Unnamed {room_or_space_uc}, ID: {}", frp.room_name_id.room_id())), + _ => room_name.set_text( + cx, + &format!( + "Unnamed {room_or_space_uc}, ID: {}", + frp.room_name_id.room_id() + ), + ), } - fetched_room_summary.label(cx, ids!(subsection_alias_id)).set_text( - cx, - &format!("Main {room_or_space_uc} Alias and ID"), - ); + fetched_room_summary + .label(cx, ids!(subsection_alias_id)) + .set_text(cx, &format!("Main {room_or_space_uc} Alias and ID")); fetched_room_summary.label(cx, ids!(room_alias)).set_text( cx, - &format!("Alias: {}", frp.canonical_alias.as_ref().map_or("not set", |a| a.as_str())), - ); - fetched_room_summary.label(cx, ids!(room_id)).set_text( - cx, - &format!("ID: {}", frp.room_name_id.room_id().as_str()), - ); - fetched_room_summary.label(cx, ids!(subsection_topic)).set_text( - cx, - &format!("{room_or_space_uc} Topic"), - ); - fetched_room_summary.html(cx, ids!(room_topic)).set_text( - cx, - frp.topic.as_deref().unwrap_or("No topic set"), + &format!( + "Alias: {}", + frp.canonical_alias + .as_ref() + .map_or("not set", |a| a.as_str()) + ), ); + fetched_room_summary + .label(cx, ids!(room_id)) + .set_text(cx, &format!("ID: {}", frp.room_name_id.room_id().as_str())); + fetched_room_summary + .label(cx, ids!(subsection_topic)) + .set_text(cx, &format!("{room_or_space_uc} Topic")); + fetched_room_summary + .html(cx, ids!(room_topic)) + .set_text(cx, frp.topic.as_deref().unwrap_or("No topic set")); let room_summary = fetched_room_summary.label(cx, ids!(room_summary)); let join_room_button = fetched_room_summary.button(cx, ids!(join_room_button)); let join_function = match (&frp.state, &frp.join_rule) { (Some(RoomState::Joined), _) => { - room_summary.set_text(cx, &format!("You have already joined this {room_or_space_lc}.")); + room_summary.set_text( + cx, + &format!("You have already joined this {room_or_space_lc}."), + ); join_room_button.set_text(cx, &format!("Go to {room_or_space_lc}")); JoinButtonFunction::NavigateOrJoin } (Some(RoomState::Banned), _) => { - room_summary.set_text(cx, &format!("You have been banned from this {room_or_space_lc}.")); + room_summary.set_text( + cx, + &format!("You have been banned from this {room_or_space_lc}."), + ); join_room_button.set_text(cx, "Cannot join until un-banned"); JoinButtonFunction::None } (Some(RoomState::Invited), _) => { - room_summary.set_text(cx, &format!("You have already been invited to this {room_or_space_lc}.")); + room_summary.set_text( + cx, + &format!("You have already been invited to this {room_or_space_lc}."), + ); join_room_button.set_text(cx, "Go to invitation"); JoinButtonFunction::NavigateOrJoin } (Some(RoomState::Knocked), _) => { - room_summary.set_text(cx, &format!("You have already knocked on this {room_or_space_lc}.")); + room_summary.set_text( + cx, + &format!("You have already knocked on this {room_or_space_lc}."), + ); join_room_button.set_text(cx, "Knock again (be nice!)"); JoinButtonFunction::Knock } (Some(RoomState::Left), join_rule) => { - room_summary.set_text(cx, &format!("You previously left this {room_or_space_lc}.")); + room_summary + .set_text(cx, &format!("You previously left this {room_or_space_lc}.")); let (join_room_text, join_function) = match join_rule { Some(JoinRuleSummary::Public) => ( format!("Re-join this {room_or_space_lc}"), @@ -669,7 +732,9 @@ impl Widget for AddRoomScreen { ), // TODO: handle this after we update matrix-sdk to the new `JoinRule` enum. Some(JoinRuleSummary::Restricted(_)) => ( - format!("Re-joining {room_or_space_lc} requires an invite or other room membership"), + format!( + "Re-joining {room_or_space_lc} requires an invite or other room membership" + ), JoinButtonFunction::None, ), _ => ( @@ -682,15 +747,22 @@ impl Widget for AddRoomScreen { } // This room is not yet known to the user. (None, join_rule) => { - let direct = if frp.is_direct == Some(true) { "direct" } else { "regular" }; - room_summary.set_text(cx, &format!( - "This is a {direct} {room_or_space_lc} with {} {}.", - frp.num_joined_members, - match frp.num_joined_members { - 1 => "member", - _ => "members", - }, - )); + let direct = if frp.is_direct == Some(true) { + "direct" + } else { + "regular" + }; + room_summary.set_text( + cx, + &format!( + "This is a {direct} {room_or_space_lc} with {} {}.", + frp.num_joined_members, + match frp.num_joined_members { + 1 => "member", + _ => "members", + }, + ), + ); let (join_room_text, join_function) = match join_rule { Some(JoinRuleSummary::Public) => ( @@ -707,10 +779,12 @@ impl Widget for AddRoomScreen { ), // TODO: handle this after we update matrix-sdk to the new `JoinRule` enum. Some(JoinRuleSummary::Restricted(_)) => ( - format!("Joining {room_or_space_lc} requires an invite or other room membership"), + format!( + "Joining {room_or_space_lc} requires an invite or other room membership" + ), JoinButtonFunction::None, ), - _ => ( + _ => ( format!("Not allowed to join this {room_or_space_lc}"), JoinButtonFunction::None, ), @@ -722,7 +796,8 @@ impl Widget for AddRoomScreen { match ars { AddRoomState::FetchedRoomPreview { .. } => { - join_room_button.set_enabled(cx, !matches!(join_function, JoinButtonFunction::None)); + join_room_button + .set_enabled(cx, !matches!(join_function, JoinButtonFunction::None)); self.join_function = join_function; } AddRoomState::Knocked { .. } => { @@ -736,8 +811,13 @@ impl Widget for AddRoomScreen { join_room_button.set_enabled(cx, false); } AddRoomState::Loaded { is_invite, .. } => { - let verb = if *is_invite { "been invited to" } else { "fully joined" }; - room_summary.set_text(cx, &format!("You have {verb} this {room_or_space_lc}.")); + let verb = if *is_invite { + "been invited to" + } else { + "fully joined" + }; + room_summary + .set_text(cx, &format!("You have {verb} this {room_or_space_lc}.")); let adj = if *is_invite { "invited" } else { "joined" }; join_room_button.set_text(cx, &format!("Go to {adj} {room_or_space_lc}")); join_room_button.set_enabled(cx, true); @@ -752,7 +832,6 @@ impl Widget for AddRoomScreen { } } - /// The function to perform when the user clicks the join button in the fetched room preview. enum JoinButtonFunction { None, @@ -761,7 +840,6 @@ enum JoinButtonFunction { /// Knock on (request to join) a room/space. Knock, } - /// Actions sent from the backend task as a result of a [`MatrixRequest::Knock`]. #[derive(Debug)] @@ -778,10 +856,9 @@ pub enum KnockResultAction { /// The room alias/ID that was originally sent with the knock request. room_or_alias_id: OwnedRoomOrAliasId, error: matrix_sdk::Error, - } + }, } - /// Tries to extract a room address (Alias or ID) from the given text. /// /// This function is quite flexible and will attempt to parse `text` as: @@ -795,8 +872,10 @@ fn parse_address(text: &str) -> Result<(OwnedRoomOrAliasId, Vec Err(e) => { let uri_result = MatrixToUri::parse(text) .map(|uri| (uri.id().clone(), uri.via().to_owned())) - .or_else(|_| MatrixUri::parse(text).map(|uri| (uri.id().clone(), uri.via().to_owned()))); - + .or_else(|_| { + MatrixUri::parse(text).map(|uri| (uri.id().clone(), uri.via().to_owned())) + }); + if let Ok((matrix_id, via)) = uri_result { if let Some(room_or_alias_id) = match matrix_id { MatrixId::Room(room_id) => Some(room_id.into()), @@ -809,5 +888,5 @@ fn parse_address(text: &str) -> Result<(OwnedRoomOrAliasId, Vec } Err(e) } - } + } } diff --git a/src/home/edited_indicator.rs b/src/home/edited_indicator.rs index 07fb24f0d..64ae610f3 100644 --- a/src/home/edited_indicator.rs +++ b/src/home/edited_indicator.rs @@ -47,8 +47,10 @@ script_mod! { /// A interactive label that indicates a message has been edited. #[derive(Script, ScriptHook, Widget)] pub struct EditedIndicator { - #[deref] view: View, - #[rust] latest_edit_ts: Option>, + #[deref] + view: View, + #[rust] + latest_edit_ts: Option>, } impl Widget for EditedIndicator { @@ -57,36 +59,35 @@ impl Widget for EditedIndicator { let area = self.view.area(); let should_hover_in = match event.hits(cx, area) { - Hit::FingerLongPress(_) - | Hit::FingerHoverIn(..) => true, + Hit::FingerLongPress(_) | Hit::FingerHoverIn(..) => true, // TODO: show edit history modal on click // Hit::FingerUp(fue) if fue.is_over && fue.is_primary_hit() => { // log!("todo: show edit history."); // false // }, Hit::FingerHoverOut(_) => { - cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); false } _ => false, }; if should_hover_in { // TODO: use pure_rust_locales crate to format the time based on the chosen Locale. - let locale_extended_fmt_en_us= "%a %b %-d, %Y, %r"; + let locale_extended_fmt_en_us = "%a %b %-d, %Y, %r"; let text = if let Some(ts) = self.latest_edit_ts { format!("Last edited {}", ts.format(locale_extended_fmt_en_us)) } else { "Last edit time unknown".to_string() }; cx.widget_action( - self.widget_uid(), + self.widget_uid(), TooltipAction::HoverIn { text, widget_rect: area.rect(cx), options: CalloutTooltipOptions { position: TooltipPosition::Right, ..Default::default() - } + }, }, ); } @@ -120,7 +121,6 @@ impl EditedIndicatorRef { } } - /// Actions emitted by an `EditedIndicator` widget. #[derive(Clone, Debug, Default)] pub enum EditedIndicatorAction { diff --git a/src/home/editing_pane.rs b/src/home/editing_pane.rs index ae89492b0..56e09ff92 100644 --- a/src/home/editing_pane.rs +++ b/src/home/editing_pane.rs @@ -8,9 +8,13 @@ use matrix_sdk::{ }, }, }; -use matrix_sdk_ui::timeline::{EventTimelineItem, MsgLikeKind, TimelineEventItemId, TimelineItemContent}; +use matrix_sdk_ui::timeline::{ + EventTimelineItem, MsgLikeKind, TimelineEventItemId, TimelineItemContent, +}; -use crate::shared::mentionable_text_input::{MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt}; +use crate::shared::mentionable_text_input::{ + MentionableTextInputWidgetExt, MentionableTextInputWidgetRefExt, +}; use crate::{ shared::popup_list::{enqueue_popup_notification, PopupKind}, sliding_sync::{submit_async_request, MatrixRequest, TimelineKind}, @@ -142,19 +146,26 @@ struct EditingPaneInfo { /// A view that slides in from the bottom of the screen to allow editing a message. #[derive(Script, ScriptHook, Widget, Animator)] pub struct EditingPane { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - - #[rust] info: Option, - #[rust] is_animating_out: bool, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + + #[rust] + info: Option, + #[rust] + is_animating_out: bool, } impl Widget for EditingPane { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - if !self.visible { return; } + if !self.visible { + return; + } let animator_action = self.animator_handle_event(cx, event); if animator_action.must_redraw() { @@ -170,21 +181,20 @@ impl Widget for EditingPane { (true, false) => { self.visible = false; self.info = None; - cx.widget_action(self.widget_uid(), EditingPaneAction::Hidden); + cx.widget_action(self.widget_uid(), EditingPaneAction::Hidden); cx.revert_key_focus(); self.redraw(cx); return; - }, + } (false, true) => { self.is_animating_out = true; return; - }, - _ => {}, + } + _ => {} } } if let Event::Actions(actions) = event { - let edit_text_input = self .mentionable_text_input(cx, ids!(editing_content.edit_text_input)) .text_input_ref(); @@ -199,10 +209,14 @@ impl Widget for EditingPane { return; } - let Some(info) = self.info.as_ref() else { return }; + let Some(info) = self.info.as_ref() else { + return; + }; if self.button(cx, ids!(accept_button)).clicked(actions) - || edit_text_input.returned(actions).is_some_and(|(_, m)| m.is_primary()) + || edit_text_input + .returned(actions) + .is_some_and(|(_, m)| m.is_primary()) { let edited_text = edit_text_input.text().trim().to_string(); let edited_content = match info.event_tl_item.content() { @@ -217,7 +231,9 @@ impl Widget for EditingPane { // TODO: also handle "/html" or "/plain" prefixes, just like when sending new messages. MessageType::Text(_text) => EditedContent::RoomMessage( - RoomMessageEventContentWithoutRelation::text_markdown(&edited_text), + RoomMessageEventContentWithoutRelation::text_markdown( + &edited_text, + ), ), MessageType::Emote(_emote) => EditedContent::RoomMessage( RoomMessageEventContentWithoutRelation::emote_markdown( @@ -231,7 +247,8 @@ impl Widget for EditingPane { MessageType::Image(image) => { let mut new_image_msg = image.clone(); if image.formatted.is_some() { - new_image_msg.formatted = FormattedBody::markdown(&edited_text); + new_image_msg.formatted = + FormattedBody::markdown(&edited_text); } new_image_msg.body = edited_text.clone(); EditedContent::RoomMessage( @@ -239,11 +256,12 @@ impl Widget for EditingPane { MessageType::Image(new_image_msg), ), ) - }, + } MessageType::Audio(audio) => { let mut new_audio_msg = audio.clone(); if audio.formatted.is_some() { - new_audio_msg.formatted = FormattedBody::markdown(&edited_text); + new_audio_msg.formatted = + FormattedBody::markdown(&edited_text); } new_audio_msg.body = edited_text.clone(); EditedContent::RoomMessage( @@ -251,23 +269,25 @@ impl Widget for EditingPane { MessageType::Audio(new_audio_msg), ), ) - }, + } MessageType::File(file) => { let mut new_file_msg = file.clone(); if file.formatted.is_some() { - new_file_msg.formatted = FormattedBody::markdown(&edited_text); + new_file_msg.formatted = + FormattedBody::markdown(&edited_text); } new_file_msg.body = edited_text.clone(); EditedContent::RoomMessage( - RoomMessageEventContentWithoutRelation::new(MessageType::File( - new_file_msg, - )), + RoomMessageEventContentWithoutRelation::new( + MessageType::File(new_file_msg), + ), ) - }, + } MessageType::Video(video) => { let mut new_video_msg = video.clone(); if video.formatted.is_some() { - new_video_msg.formatted = FormattedBody::markdown(&edited_text); + new_video_msg.formatted = + FormattedBody::markdown(&edited_text); } new_video_msg.body = edited_text.clone(); EditedContent::RoomMessage( @@ -275,7 +295,7 @@ impl Widget for EditingPane { MessageType::Video(new_video_msg), ), ) - }, + } _non_editable => { enqueue_popup_notification( "That message type cannot be edited.", @@ -285,7 +305,7 @@ impl Widget for EditingPane { self.animator_play(cx, ids!(panel.hide)); self.redraw(cx); return; - }, + } }; // TODO: extract mentions out of the new edited text and use them here. @@ -293,7 +313,8 @@ impl Widget for EditingPane { if let EditedContent::RoomMessage(new_message_content) = &mut edited_content { - new_message_content.mentions = Some(existing_mentions.clone()); + new_message_content.mentions = + Some(existing_mentions.clone()); } // TODO: once we update the matrix-sdk dependency, uncomment this. // EditedContent::MediaCaption { mentions, .. }) => { @@ -334,7 +355,6 @@ impl Widget for EditingPane { fallback_text: edited_text, new_content: new_content_block, } - } _ => { enqueue_popup_notification( @@ -353,7 +373,7 @@ impl Widget for EditingPane { None, ); return; - }, + } }; submit_async_request(MatrixRequest::EditMessage { @@ -402,14 +422,14 @@ impl EditingPane { match edit_result { Ok(()) => { self.animator_play(cx, ids!(panel.hide)); - }, + } Err(e) => { enqueue_popup_notification( format!("Failed to edit message: {}", e), PopupKind::Error, None, ); - }, + } } } @@ -421,15 +441,12 @@ impl EditingPane { timeline_kind: TimelineKind, ) { if !event_tl_item.is_editable() { - enqueue_popup_notification( - "That message cannot be edited.", - PopupKind::Error, - None, - ); + enqueue_popup_notification("That message cannot be edited.", PopupKind::Error, None); return; } - let edit_text_input = self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)); + let edit_text_input = + self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)); if let Some(message) = event_tl_item.content().as_message() { edit_text_input.set_text(cx, message.body()); @@ -444,7 +461,6 @@ impl EditingPane { return; } - self.info = Some(EditingPaneInfo { event_tl_item, timeline_kind, @@ -460,7 +476,10 @@ impl EditingPane { let text_len = edit_text_input.text().len(); inner_text_input.set_cursor( cx, - Cursor { index: text_len, prefer_next_row: false }, + Cursor { + index: text_len, + prefer_next_row: false, + }, false, ); // TODO: this doesn't work, likely because of Makepad's bug in which you cannot @@ -473,7 +492,8 @@ impl EditingPane { pub fn save_state(&self) -> Option { self.info.as_ref().map(|info| EditingPaneState { event_tl_item: info.event_tl_item.clone(), - text_input_state: self.child_by_path(ids!(editing_content.edit_text_input)) + text_input_state: self + .child_by_path(ids!(editing_content.edit_text_input)) .as_mentionable_text_input() .text_input_ref() .save_state(), @@ -487,7 +507,10 @@ impl EditingPane { editing_pane_state: EditingPaneState, timeline_kind: TimelineKind, ) { - let EditingPaneState { event_tl_item, text_input_state } = editing_pane_state; + let EditingPaneState { + event_tl_item, + text_input_state, + } = editing_pane_state; self.mentionable_text_input(cx, ids!(editing_content.edit_text_input)) .text_input_ref() .restore_state(cx, text_input_state); @@ -524,7 +547,9 @@ impl EditingPaneRef { timeline_event_item_id: TimelineEventItemId, edit_result: Result<(), matrix_sdk_ui::timeline::Error>, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.handle_edit_result(cx, timeline_event_item_id, edit_result); } @@ -538,13 +563,10 @@ impl EditingPaneRef { } /// See [`EditingPane::show()`]. - pub fn show( - &self, - cx: &mut Cx, - event_tl_item: EventTimelineItem, - timeline_kind: TimelineKind, - ) { - let Some(mut inner) = self.borrow_mut() else { return; }; + pub fn show(&self, cx: &mut Cx, event_tl_item: EventTimelineItem, timeline_kind: TimelineKind) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, event_tl_item, timeline_kind); } @@ -562,7 +584,9 @@ impl EditingPaneRef { editing_pane_state: EditingPaneState, timeline_kind: TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.restore_state(cx, editing_pane_state, timeline_kind); } @@ -570,7 +594,9 @@ impl EditingPaneRef { /// /// This function *DOES NOT* emit an [`EditingPaneAction::Hidden`] action. pub fn force_reset_hide(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.visible = false; inner.animator_cut(cx, ids!(panel.hide)); inner.is_animating_out = false; diff --git a/src/home/event_reaction_list.rs b/src/home/event_reaction_list.rs index b47749a77..374e819cd 100644 --- a/src/home/event_reaction_list.rs +++ b/src/home/event_reaction_list.rs @@ -113,15 +113,24 @@ pub struct ReactionData { #[derive(Script, ScriptHook, Widget)] pub struct ReactionList { - #[uid] uid: WidgetUid, - #[redraw] #[rust] area: Area, - #[live] item: Option, - #[rust] children: Vec<(ButtonRef, ReactionData)>, - #[layout] layout: Layout, - #[walk] walk: Walk, + #[uid] + uid: WidgetUid, + #[redraw] + #[rust] + area: Area, + #[live] + item: Option, + #[rust] + children: Vec<(ButtonRef, ReactionData)>, + #[layout] + layout: Layout, + #[walk] + walk: Walk, - #[rust] timeline_kind: Option, - #[rust] timeline_event_id: Option, + #[rust] + timeline_kind: Option, + #[rust] + timeline_event_id: Option, } impl Widget for ReactionList { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { @@ -163,7 +172,9 @@ impl Widget for ReactionList { } // Otherwise, a primary click/press over the button should toggle the reaction. else if fue.is_primary_hit() && fue.was_tap() { - let Some(kind) = &self.timeline_kind else { return }; + let Some(kind) = &self.timeline_kind else { + return; + }; let Some(timeline_event_id) = &self.timeline_event_id else { return; }; @@ -176,7 +187,10 @@ impl Widget for ReactionList { let (bg_color, border_color) = if !reaction_data.includes_user { (EMOJI_BG_COLOR_INCLUDE_SELF, EMOJI_BORDER_COLOR_INCLUDE_SELF) } else { - (EMOJI_BG_COLOR_NOT_INCLUDE_SELF, EMOJI_BORDER_COLOR_NOT_INCLUDE_SELF) + ( + EMOJI_BG_COLOR_NOT_INCLUDE_SELF, + EMOJI_BORDER_COLOR_NOT_INCLUDE_SELF, + ) }; let mut reaction_button = button_ref.clone(); script_apply_eval!(cx, reaction_button, { @@ -206,7 +220,7 @@ impl ReactionList { reaction_data: ReactionData, ) { cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomScreenTooltipActions::HoverInReactionButton { widget_rect: button_ref.area().rect(cx), reaction_data, @@ -218,20 +232,14 @@ impl ReactionList { } /// Deals with to any event/hit that triggers a hover-out action. - fn do_hover_out( - &self, - cx: &mut Cx, - _scope: &mut Scope, - button_ref: &ButtonRef, - ) { - cx.widget_action(self.widget_uid(), RoomScreenTooltipActions::HoverOut); + fn do_hover_out(&self, cx: &mut Cx, _scope: &mut Scope, button_ref: &ButtonRef) { + cx.widget_action(self.widget_uid(), RoomScreenTooltipActions::HoverOut); let mut button_ref = button_ref.clone(); script_apply_eval!(cx, button_ref, { draw_bg +: { hover: 0.0 } }); cx.set_cursor(MouseCursor::Default); } } - impl ReactionListRef { /// Set the list of reactions and their counts to display in the ReactionList widget, /// along with the room ID and event ID that these reactions are for. @@ -278,7 +286,8 @@ impl ReactionListRef { cx, sender.clone(), Some(timeline_kind.room_id()), - true, |_, _| { }, + true, + |_, _| {}, ); } @@ -289,10 +298,10 @@ impl ReactionListRef { room_id: timeline_kind.room_id().clone(), }; let mut button = widget_ref_from_live_ptr(cx, inner.item).as_button(); - button.set_text(cx, &format!("{} {}", - reaction_data.reaction, - reaction_senders.len() - )); + button.set_text( + cx, + &format!("{} {}", reaction_data.reaction, reaction_senders.len()), + ); let (bg_color, border_color) = if reaction_data.includes_user { (EMOJI_BG_COLOR_INCLUDE_SELF, EMOJI_BORDER_COLOR_INCLUDE_SELF) } else { diff --git a/src/home/event_source_modal.rs b/src/home/event_source_modal.rs index 69405d24d..dc3203ed9 100644 --- a/src/home/event_source_modal.rs +++ b/src/home/event_source_modal.rs @@ -6,7 +6,6 @@ use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId}; use crate::shared::popup_list::{PopupKind, enqueue_popup_notification}; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -177,7 +176,7 @@ script_mod! { code_block := View { width: Fill, height: Fit, - flow: Overlay + flow: Overlay // align the left side of the border frame with the left side of the room id / event id rows padding: 6 @@ -251,13 +250,16 @@ pub enum EventSourceModalAction { Close, } - #[derive(Script, ScriptHook, Widget)] pub struct EventSourceModal { - #[deref] view: View, - #[rust] room_id: Option, - #[rust] event_id: Option, - #[rust] original_json: Option, + #[deref] + view: View, + #[rust] + room_id: Option, + #[rust] + event_id: Option, + #[rust] + original_json: Option, } impl Widget for EventSourceModal { @@ -268,10 +270,14 @@ impl Widget for EventSourceModal { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { if let Some(room_id) = &self.room_id { - self.view.label(cx, ids!(room_id_value)).set_text(cx, room_id.as_str()); + self.view + .label(cx, ids!(room_id_value)) + .set_text(cx, room_id.as_str()); } if let Some(event_id) = &self.event_id { - self.view.label(cx, ids!(event_id_value)).set_text(cx, event_id.as_str()); + self.view + .label(cx, ids!(event_id_value)) + .set_text(cx, event_id.as_str()); } if let Some(json) = &self.original_json { self.view.code_view(cx, ids!(code_view)).set_text(cx, json); @@ -286,8 +292,10 @@ impl WidgetMatchEvent for EventSourceModal { // Handle canceling/closing the modal. let close_clicked = close_button.clicked(actions); - if close_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if close_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // If the modal was dismissed by clicking outside of it, we MUST NOT emit // an EventSourceModalAction::Close action, as that would cause @@ -298,7 +306,11 @@ impl WidgetMatchEvent for EventSourceModal { return; } - if self.view.button(cx, ids!(room_id_copy_button)).clicked(actions) { + if self + .view + .button(cx, ids!(room_id_copy_button)) + .clicked(actions) + { if let Some(room_id) = &self.room_id { cx.copy_to_clipboard(room_id.as_str()); enqueue_popup_notification( @@ -309,7 +321,11 @@ impl WidgetMatchEvent for EventSourceModal { } } - if self.view.button(cx, ids!(event_id_copy_button)).clicked(actions) { + if self + .view + .button(cx, ids!(event_id_copy_button)) + .clicked(actions) + { if let Some(event_id) = &self.event_id { cx.copy_to_clipboard(event_id.as_str()); enqueue_popup_notification( @@ -320,7 +336,11 @@ impl WidgetMatchEvent for EventSourceModal { } } - if self.view.button(cx, ids!(copy_source_button)).clicked(actions) { + if self + .view + .button(cx, ids!(copy_source_button)) + .clicked(actions) + { if let Some(json) = &self.original_json { cx.copy_to_clipboard(json); enqueue_popup_notification( @@ -347,9 +367,15 @@ impl EventSourceModal { self.original_json = original_json.clone(); self.view.button(cx, ids!(close_button)).reset_hover(cx); - self.view.button(cx, ids!(room_id_copy_button)).reset_hover(cx); - self.view.button(cx, ids!(event_id_copy_button)).reset_hover(cx); - self.view.button(cx, ids!(copy_source_button)).reset_hover(cx); + self.view + .button(cx, ids!(room_id_copy_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(event_id_copy_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(copy_source_button)) + .reset_hover(cx); self.view.redraw(cx); } } @@ -363,7 +389,9 @@ impl EventSourceModalRef { event_id: Option, original_json: Option, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, room_id, event_id, original_json); } } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 910f817e1..123925f50 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,6 +1,10 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; +use crate::{ + app::AppState, + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, + settings::settings_screen::SettingsScreenWidgetRefExt, +}; script_mod! { use mod.prelude.widgets.* @@ -303,7 +307,7 @@ script_mod! { // We wrap it in the SpacesBarWrapper in order to animate it in or out, // and wrap *that* in a CachedWidget in order to maintain its shown/hidden state // across AdaptiveView transitions between Mobile view mode and Desktop view mode. - // + // // ... Then we wrap *that* in a ... CachedWidget { spaces_bar_wrapper := mod.widgets.SpacesBarWrapper {} @@ -353,13 +357,15 @@ script_mod! { } } - /// A simple wrapper around the SpacesBar that allows us to animate showing or hiding it. #[derive(Script, ScriptHook, Widget, Animator)] pub struct SpacesBarWrapper { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for SpacesBarWrapper { @@ -384,7 +390,9 @@ impl Widget for SpacesBarWrapper { impl SpacesBarWrapperRef { /// Shows or hides the spaces bar by animating it in or out. fn show_or_hide(&self, cx: &mut Cx, show: bool) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; if show { inner.animator_play(cx, ids!(spaces_bar_animator.show)); } else { @@ -394,18 +402,20 @@ impl SpacesBarWrapperRef { } } - #[derive(Script, ScriptHook, Widget)] pub struct HomeScreen { - #[deref] view: View, + #[deref] + view: View, /// The previously-selected navigation tab, used to determine which tab /// and top-level view we return to after closing the settings screen. /// /// Note that the current selected tap is stored in `AppState` so that /// other widgets can easily access it. - #[rust] previous_selection: SelectedTab, - #[rust] is_spaces_bar_shown: bool, + #[rust] + previous_selection: SelectedTab, + #[rust] + is_spaces_bar_shown: bool, } impl Widget for HomeScreen { @@ -418,7 +428,9 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Home) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Home; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -427,17 +439,23 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::AddRoom) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::AddRoom; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::GoToSpace { space_name_id }) => { - let new_space_selection = SelectedTab::Space { space_name_id: space_name_id.clone() }; + let new_space_selection = SelectedTab::Space { + space_name_id: space_name_id.clone(), + }; if app_state.selected_tab != new_space_selection { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = new_space_selection; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -447,8 +465,12 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Settings) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Settings; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); - if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); + if let Some(settings_page) = + self.update_active_page_from_selection(cx, app_state) + { settings_page .settings_screen(cx, ids!(settings_screen)) .populate(cx, None); @@ -461,19 +483,21 @@ impl Widget for HomeScreen { Some(NavigationBarAction::CloseSettings) => { if matches!(app_state.selected_tab, SelectedTab::Settings) { app_state.selected_tab = self.previous_selection.clone(); - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::ToggleSpacesBar) => { self.is_spaces_bar_shown = !self.is_spaces_bar_shown; - self.view.spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) + self.view + .spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) .show_or_hide(cx, self.is_spaces_bar_shown); } // We're the ones who emitted this action, so we don't need to handle it again. - Some(NavigationBarAction::TabSelected(_)) - | None => { } + Some(NavigationBarAction::TabSelected(_)) | None => {} } } } @@ -504,12 +528,10 @@ impl HomeScreen { .set_active_page( cx, match app_state.selected_tab { - SelectedTab::Space { .. } - | SelectedTab::Home => id!(home_page), + SelectedTab::Space { .. } | SelectedTab::Home => id!(home_page), SelectedTab::Settings => id!(settings_page), SelectedTab::AddRoom => id!(add_room_page), }, ) } } - diff --git a/src/home/invite_modal.rs b/src/home/invite_modal.rs index d644bf542..56bd5a413 100644 --- a/src/home/invite_modal.rs +++ b/src/home/invite_modal.rs @@ -7,7 +7,6 @@ use crate::home::room_screen::InviteResultAction; use crate::sliding_sync::{MatrixRequest, submit_async_request}; use crate::utils::RoomNameId; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -138,12 +137,14 @@ enum InviteModalState { InviteError, } - #[derive(Script, ScriptHook, Widget)] pub struct InviteModal { - #[deref] view: View, - #[rust] state: InviteModalState, - #[rust] room_name_id: Option, + #[deref] + view: View, + #[rust] + state: InviteModalState, + #[rust] + room_name_id: Option, } impl Widget for InviteModal { @@ -163,8 +164,10 @@ impl WidgetMatchEvent for InviteModal { // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); - if cancel_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // If the modal was dismissed by clicking outside of it, we MUST NOT emit // a `InviteModalAction::Close` action, as that would cause @@ -188,7 +191,8 @@ impl WidgetMatchEvent for InviteModal { let mut status_label = self.view.label(cx, ids!(status_label_view.status_label)); // Handle return key or invite button click. - if let Some(user_id_str) = confirm_button.clicked(actions) + if let Some(user_id_str) = confirm_button + .clicked(actions) .then(|| user_id_input.text()) .or_else(|| user_id_input.returned(actions).map(|(t, _)| t)) { @@ -244,9 +248,12 @@ impl WidgetMatchEvent for InviteModal { for action in actions { let new_state = match action.downcast_ref() { Some(InviteResultAction::Sent { room_id, user_id }) - if self.room_name_id.as_ref().is_some_and(|rni| rni.room_id() == room_id) - && invited_user_id == user_id - => { + if self + .room_name_id + .as_ref() + .is_some_and(|rni| rni.room_id() == room_id) + && invited_user_id == user_id => + { let status = format!("Successfully invited {user_id}!"); script_apply_eval!(cx, status_label, { text: #(status), @@ -260,10 +267,16 @@ impl WidgetMatchEvent for InviteModal { okay_button.set_visible(cx, true); Some(InviteModalState::InviteSuccess) } - Some(InviteResultAction::Failed { room_id, user_id, error }) - if self.room_name_id.as_ref().is_some_and(|rni| rni.room_id() == room_id) - && invited_user_id == user_id - => { + Some(InviteResultAction::Failed { + room_id, + user_id, + error, + }) if self + .room_name_id + .as_ref() + .is_some_and(|rni| rni.room_id() == room_id) + && invited_user_id == user_id => + { let status = format!("Failed to send invite: {error}"); script_apply_eval!(cx, status_label, { text: #(status), @@ -291,10 +304,9 @@ impl WidgetMatchEvent for InviteModal { impl InviteModal { pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { - self.view.label(cx, ids!(title)).set_text( - cx, - &format!("Invite to {room_name_id}"), - ); + self.view + .label(cx, ids!(title)) + .set_text(cx, &format!("Invite to {room_name_id}")); self.state = InviteModalState::WaitingForUserInput; self.room_name_id = Some(room_name_id); @@ -313,8 +325,12 @@ impl InviteModal { okay_button.reset_hover(cx); user_id_input.set_is_read_only(cx, false); user_id_input.set_text(cx, ""); - self.view.view(cx, ids!(status_label_view)).set_visible(cx, false); - self.view.label(cx, ids!(status_label_view.status_label)).set_text(cx, ""); + self.view + .view(cx, ids!(status_label_view)) + .set_visible(cx, false); + self.view + .label(cx, ids!(status_label_view.status_label)) + .set_text(cx, ""); self.view.redraw(cx); user_id_input.set_key_focus(cx); } @@ -322,7 +338,9 @@ impl InviteModal { impl InviteModalRef { pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, room_name_id); } } diff --git a/src/home/invite_screen.rs b/src/home/invite_screen.rs index 672b6d1ba..37531e022 100644 --- a/src/home/invite_screen.rs +++ b/src/home/invite_screen.rs @@ -8,11 +8,22 @@ use std::ops::Deref; use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{app::AppStateAction, home::rooms_list::RoomsListRef, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, popup_list::{enqueue_popup_notification, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt}, sliding_sync::{submit_async_request, MatrixRequest}, utils::{self, RoomNameId}}; +use crate::{ + app::AppStateAction, + home::rooms_list::RoomsListRef, + join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, + room::{BasicRoomDetails, FetchedRoomAvatar}, + shared::{ + avatar::AvatarWidgetRefExt, + popup_list::{enqueue_popup_notification, PopupKind}, + restore_status_view::RestoreStatusViewWidgetExt, + }, + sliding_sync::{submit_async_request, MatrixRequest}, + utils::{self, RoomNameId}, +}; use super::rooms_list::{InviteState, InviterInfo}; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -208,14 +219,12 @@ impl Deref for InviteDetails { #[derive(Debug)] pub enum JoinRoomResultAction { /// The user has successfully joined the room. - Joined { - room_id: OwnedRoomId, - }, + Joined { room_id: OwnedRoomId }, /// There was an error attempting to join the room. Failed { room_id: OwnedRoomId, error: matrix_sdk::Error, - } + }, } /// Actions sent from the backend task as a result of a [`MatrixRequest::LeaveRoom`]. @@ -224,33 +233,37 @@ pub enum JoinRoomResultAction { #[derive(Debug)] pub enum LeaveRoomResultAction { /// The user has successfully left the room. - Left { - room_id: OwnedRoomId, - }, + Left { room_id: OwnedRoomId }, /// There was an error attempting to leave the room. Failed { room_id: OwnedRoomId, error: matrix_sdk::Error, - } + }, } - /// A view that shows information about a room that the user has been invited to. #[derive(Script, ScriptHook, Widget)] pub struct InviteScreen { - #[deref] view: View, + #[deref] + view: View, - #[rust] invite_state: InviteState, - #[rust] info: Option, + #[rust] + invite_state: InviteState, + #[rust] + info: Option, /// Whether a JoinLeaveRoomModal dialog has been displayed /// to allow the user to confirm their join/reject action. /// This is used to prevent showing multiple popup notifications /// (one from the JoinLeaveRoomModal, and one from this invite screen). - #[rust] has_shown_confirmation: bool, + #[rust] + has_shown_confirmation: bool, /// The name and ID of the invited room. - #[rust] room_name_id: Option, - #[rust] is_loaded: bool, - #[rust] all_rooms_loaded: bool, + #[rust] + room_name_id: Option, + #[rust] + is_loaded: bool, + #[rust] + all_rooms_loaded: bool, } impl Widget for InviteScreen { @@ -258,7 +271,11 @@ impl Widget for InviteScreen { // Currently, a Signal event is only used to tell this widget // to check if the room has been loaded from the homeserver yet. if let Event::Signal = event { - if let (false, Some(room_name_id), true) = (self.is_loaded, self.room_name_id.as_ref(), cx.has_global::()) { + if let (false, Some(room_name_id), true) = ( + self.is_loaded, + self.room_name_id.as_ref(), + cx.has_global::(), + ) { let rooms_list_ref = cx.get_global::(); if !rooms_list_ref.is_room_loaded(room_name_id.room_id()) { self.all_rooms_loaded = rooms_list_ref.all_rooms_loaded(); @@ -279,16 +296,28 @@ impl Widget for InviteScreen { // First, we quickly loop over the actions up front to handle the case // where this room was restored and has now been successfully loaded from the homeserver. for action in actions { - if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = action.downcast_ref() { - if self.room_name_id.as_ref().is_some_and(|current| current.room_id() == room_name_id.room_id()) { + if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = + action.downcast_ref() + { + if self + .room_name_id + .as_ref() + .is_some_and(|current| current.room_id() == room_name_id.room_id()) + { self.set_displayed_invite(cx, room_name_id); break; } } } - let Some(info) = self.info.as_ref() else { return; }; - if let Some(modifiers) = self.view.button(cx, ids!(cancel_button)).clicked_modifiers(actions) { + let Some(info) = self.info.as_ref() else { + return; + }; + if let Some(modifiers) = self + .view + .button(cx, ids!(cancel_button)) + .clicked_modifiers(actions) + { self.invite_state = InviteState::WaitingForLeaveResult; if modifiers.shift { submit_async_request(MatrixRequest::LeaveRoom { @@ -303,7 +332,11 @@ impl Widget for InviteScreen { self.has_shown_confirmation = true; } } - if let Some(modifiers) = self.view.button(cx, ids!(accept_button)).clicked_modifiers(actions) { + if let Some(modifiers) = self + .view + .button(cx, ids!(accept_button)) + .clicked_modifiers(actions) + { self.invite_state = InviteState::WaitingForJoinResult; if modifiers.shift { submit_async_request(MatrixRequest::JoinRoom { @@ -324,14 +357,25 @@ impl Widget for InviteScreen { Some(JoinRoomResultAction::Joined { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingForJoinedRoom; if !self.has_shown_confirmation { - enqueue_popup_notification("Successfully joined room.", PopupKind::Success, Some(5.0)); + enqueue_popup_notification( + "Successfully joined room.", + PopupKind::Success, + Some(5.0), + ); } continue; } - Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == info.room_id() => { + Some(JoinRoomResultAction::Failed { room_id, error }) + if room_id == info.room_id() => + { self.invite_state = InviteState::WaitingOnUserInput; if !self.has_shown_confirmation { - let msg = utils::stringify_join_leave_error(error, info.room_name_id(), true, true); + let msg = utils::stringify_join_leave_error( + error, + info.room_name_id(), + true, + true, + ); enqueue_popup_notification(msg, PopupKind::Error, None); } continue; @@ -343,21 +387,33 @@ impl Widget for InviteScreen { Some(LeaveRoomResultAction::Left { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::RoomLeft; if !self.has_shown_confirmation { - enqueue_popup_notification("Successfully rejected invite.", PopupKind::Success, Some(5.0)); + enqueue_popup_notification( + "Successfully rejected invite.", + PopupKind::Success, + Some(5.0), + ); } continue; } - Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == info.room_id() => { + Some(LeaveRoomResultAction::Failed { room_id, error }) + if room_id == info.room_id() => + { self.invite_state = InviteState::WaitingOnUserInput; if !self.has_shown_confirmation { - enqueue_popup_notification(format!("Failed to reject invite: {error}"), PopupKind::Error, None); + enqueue_popup_notification( + format!("Failed to reject invite: {error}"), + PopupKind::Error, + None, + ); } continue; } _ => {} } - if let Some(JoinLeaveRoomModalAction::Close { successful, .. }) = action.downcast_ref() { + if let Some(JoinLeaveRoomModalAction::Close { successful, .. }) = + action.downcast_ref() + { // If the modal didn't result in a successful join/leave, // then we must reset the invite state to waiting for user input. if !*successful { @@ -373,10 +429,10 @@ impl Widget for InviteScreen { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { if !self.is_loaded { - let mut restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); + let mut restore_status_view = + self.view.restore_status_view(cx, ids!(restore_status_view)); if let Some(room_name) = &self.room_name_id { restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); } @@ -393,18 +449,23 @@ impl Widget for InviteScreen { let inviter_avatar = inviter_view.avatar(cx, ids!(inviter_avatar)); let mut drew_avatar = false; if let Some(avatar_bytes) = inviter.avatar.as_ref() { - drew_avatar = inviter_avatar.show_image( - cx, - None, // don't make this avatar clickable. - |cx, img| utils::load_png_or_jpg(&img, cx, avatar_bytes), - ).is_ok(); + drew_avatar = inviter_avatar + .show_image( + cx, + None, // don't make this avatar clickable. + |cx, img| utils::load_png_or_jpg(&img, cx, avatar_bytes), + ) + .is_ok(); } if !drew_avatar { inviter_avatar.show_text( cx, None, None, // don't make this avatar clickable. - inviter.display_name.as_deref().unwrap_or_else(|| inviter.user_id.as_str()), + inviter + .display_name + .as_deref() + .unwrap_or_else(|| inviter.user_id.as_str()), ); } let inviter_name = inviter_view.label(cx, ids!(inviter_name)); @@ -414,20 +475,20 @@ impl Widget for InviteScreen { inviter_name.set_text(cx, inviter_user_name); inviter_user_id.set_visible(cx, true); inviter_user_id.set_text(cx, inviter.user_id.as_str()); - } - else { + } else { // If we only have a user ID, show it in the user_name field, // and hide the user ID field. inviter_name.set_text(cx, inviter.user_id.as_str()); inviter_user_id.set_visible(cx, false); } (true, "has invited you to join:") - } - else { + } else { (false, "You have been invited to join:") }; inviter_view.set_visible(cx, is_visible); - self.view.label(cx, ids!(invite_message)).set_text(cx, invite_text); + self.view + .label(cx, ids!(invite_message)) + .set_text(cx, invite_text); // Second, populate the room info, if we have it. let room_view = self.view.view(cx, ids!(room_view)); @@ -435,9 +496,7 @@ impl Widget for InviteScreen { match &info.room_avatar() { FetchedRoomAvatar::Text(text) => { room_avatar.show_text( - cx, - None, - None, // don't make this avatar clickable. + cx, None, None, // don't make this avatar clickable. text, ); } @@ -450,7 +509,9 @@ impl Widget for InviteScreen { } } let invite_room_label = info.room_name_id().to_string(); - room_view.label(cx, ids!(room_name)).set_text(cx, &invite_room_label); + room_view + .label(cx, ids!(room_name)) + .set_text(cx, &invite_room_label); // Third, set the buttons' text based on the invite state. let cancel_button = self.view.button(cx, ids!(cancel_button)); @@ -518,11 +579,7 @@ impl InviteScreen { let restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); if !self.is_loaded { - restore_status_view.set_content( - cx, - self.all_rooms_loaded, - room_name_id, - ); + restore_status_view.set_content(cx, self.all_rooms_loaded, room_name_id); restore_status_view.set_visible(cx, true); } else { restore_status_view.set_visible(cx, false); diff --git a/src/home/link_preview.rs b/src/home/link_preview.rs index 1d605dc3d..ed4b7d32e 100644 --- a/src/home/link_preview.rs +++ b/src/home/link_preview.rs @@ -8,7 +8,10 @@ use std::{ use makepad_widgets::*; use crate::{LivePtr, widget_ref_from_live_ptr}; -use matrix_sdk::ruma::{events::room::{ImageInfo, MediaSource}, OwnedMxcUri, UInt}; +use matrix_sdk::ruma::{ + events::room::{ImageInfo, MediaSource}, + OwnedMxcUri, UInt, +}; use serde::Deserialize; use url::Url; @@ -235,8 +238,12 @@ impl Widget for LinkPreview { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { // Handle collapsible button clicks if let Event::Actions(actions) = event { - let expand_btn = self.view.button(cx, ids!(collapsible_buttons.expand_button)); - let collapse_btn = self.view.button(cx, ids!(collapsible_buttons.collapse_button)); + let expand_btn = self + .view + .button(cx, ids!(collapsible_buttons.expand_button)); + let collapse_btn = self + .view + .button(cx, ids!(collapsible_buttons.collapse_button)); if expand_btn.clicked(actions) || collapse_btn.clicked(actions) { self.is_expanded = !self.is_expanded; self.update_button_and_visibility(cx); @@ -265,10 +272,12 @@ impl Widget for LinkPreview { draw_bg.color: mod.widgets.COLOR_BG_PREVIEW }); if fe.is_over && fe.is_primary_hit() && fe.was_tap() { - if let Some(html_link) = view.link_label(cx, ids!(content_view.title_label)).borrow() { + if let Some(html_link) = + view.link_label(cx, ids!(content_view.title_label)).borrow() + { if !html_link.url.is_empty() { cx.widget_action( - html_link.widget_uid(), + html_link.widget_uid(), HtmlLinkAction::Clicked { url: html_link.url.clone(), key_modifiers: fe.modifiers, @@ -287,7 +296,11 @@ impl Widget for LinkPreview { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // Draw children (link preview items) - let max_visible = if self.is_expanded { self.children.len() } else { 2 }; + let max_visible = if self.is_expanded { + self.children.len() + } else { + 2 + }; for (index, view) in self.children.iter_mut().enumerate() { if index < max_visible { let _ = view.draw(cx, scope); @@ -306,9 +319,15 @@ impl LinkPreview { fn update_button_and_visibility(&mut self, cx: &mut Cx) { if self.show_collapsible_buttons { - self.view.view(cx, ids!(collapsible_buttons)).set_visible(cx, true); - let expand_btn = self.view.button(cx, ids!(collapsible_buttons.expand_button)); - let collapse_btn = self.view.button(cx, ids!(collapsible_buttons.collapse_button)); + self.view + .view(cx, ids!(collapsible_buttons)) + .set_visible(cx, true); + let expand_btn = self + .view + .button(cx, ids!(collapsible_buttons.expand_button)); + let collapse_btn = self + .view + .button(cx, ids!(collapsible_buttons.collapse_button)); if self.is_expanded { expand_btn.set_visible(cx, false); collapse_btn.set_visible(cx, true); @@ -320,7 +339,9 @@ impl LinkPreview { expand_btn.reset_hover(cx); collapse_btn.reset_hover(cx); } else { - self.view.view(cx, ids!(collapsible_buttons)).set_visible(cx, false); + self.view + .view(cx, ids!(collapsible_buttons)) + .set_visible(cx, false); } } } @@ -346,19 +367,27 @@ impl LinkPreviewRef { } /// Shows the collapsible button for the link preview. - /// + /// /// This function is usually called when the link preview is updated. /// If the link preview is updated, and the collapsible button should be shown, /// this function should be called. fn show_collapsible_buttons(&mut self, cx: &mut Cx, hidden_count: usize) { - if let Some(mut inner) = self.borrow_mut() { + if let Some(mut inner) = self.borrow_mut() { inner.show_collapsible_buttons = true; inner.hidden_links_count = hidden_count; - let expand_btn = inner.view.button(cx, ids!(collapsible_buttons.expand_button)); + let expand_btn = inner + .view + .button(cx, ids!(collapsible_buttons.expand_button)); expand_btn.set_text(cx, &format!("Show {} more links", inner.hidden_links_count)); expand_btn.set_visible(cx, true); - inner.view.button(cx, ids!(collapsible_buttons.collapse_button)).set_visible(cx, false); - inner.view.view(cx, ids!(collapsible_buttons)).set_visible(cx, true); + inner + .view + .button(cx, ids!(collapsible_buttons.collapse_button)) + .set_visible(cx, false); + inner + .view + .view(cx, ids!(collapsible_buttons)) + .set_visible(cx, true); } } @@ -373,7 +402,14 @@ impl LinkPreviewRef { image_populate_fn: F, ) -> (ViewRef, bool) where - F: FnOnce(&mut Cx, &TextOrImageRef, Option>, MediaSource, &str, &mut MediaCache) -> bool, + F: FnOnce( + &mut Cx, + &TextOrImageRef, + Option>, + MediaSource, + &str, + &mut MediaCache, + ) -> bool, { let view_ref = widget_ref_from_live_ptr(cx, self.item_template()).as_view(); let mut fully_drawn = true; @@ -450,7 +486,7 @@ impl LinkPreviewRef { /// The given `media_cache` is used to fetch the thumbnails from cache. /// /// The given `link_preview_cache` is used to fetch the link previews from cache. - /// + /// /// Return true when the link preview is fully drawn pub fn populate_below_message( &mut self, @@ -459,9 +495,16 @@ impl LinkPreviewRef { media_cache: &mut MediaCache, link_preview_cache: &mut LinkPreviewCache, populate_image_fn: &F, - ) -> bool + ) -> bool where - F: Fn(&mut Cx, &TextOrImageRef, Option>, MediaSource, &str, &mut MediaCache) -> bool, + F: Fn( + &mut Cx, + &TextOrImageRef, + Option>, + MediaSource, + &str, + &mut MediaCache, + ) -> bool, { const SKIPPED_DOMAINS: &[&str] = &["matrix.to", "matrix.io"]; const MAX_LINK_PREVIEWS_BY_EXPAND: usize = 2; @@ -469,13 +512,13 @@ impl LinkPreviewRef { let mut accepted_link_count = 0; let mut views = Vec::new(); let mut seen_urls = std::collections::HashSet::new(); - + for link in links { let url_string = link.to_string(); if seen_urls.contains(&url_string) { continue; } - + if let Some(domain) = link.host_str() { if SKIPPED_DOMAINS .iter() @@ -484,7 +527,7 @@ impl LinkPreviewRef { continue; } } - + seen_urls.insert(url_string.clone()); accepted_link_count += 1; let (view_ref, was_image_drawn) = self.populate_view( @@ -493,7 +536,14 @@ impl LinkPreviewRef { link, media_cache, |cx, text_or_image_ref, image_info_source, original_source, body, media_cache| { - populate_image_fn(cx, text_or_image_ref, image_info_source, original_source, body, media_cache) + populate_image_fn( + cx, + text_or_image_ref, + image_info_source, + original_source, + body, + media_cache, + ) }, ); fully_drawn_count += was_image_drawn as usize; @@ -679,11 +729,11 @@ fn insert_into_cache( UrlPreviewError::HttpStatus(404) => LinkPreviewError::NotFound, UrlPreviewError::HttpStatus(429) => LinkPreviewError::RateLimited, UrlPreviewError::Json(_) => LinkPreviewError::ParseError(e.to_string()), - UrlPreviewError::Request(_) | - UrlPreviewError::ClientNotAvailable | - UrlPreviewError::AccessTokenNotAvailable | - UrlPreviewError::UrlParse(_) | - UrlPreviewError::HttpStatus(_) => LinkPreviewError::NetworkError(e.to_string()), + UrlPreviewError::Request(_) + | UrlPreviewError::ClientNotAvailable + | UrlPreviewError::AccessTokenNotAvailable + | UrlPreviewError::UrlParse(_) + | UrlPreviewError::HttpStatus(_) => LinkPreviewError::NetworkError(e.to_string()), }; if let LinkPreviewError::RateLimited = error_type { LinkPreviewCacheEntry::Requested @@ -693,12 +743,12 @@ fn insert_into_cache( } } }; - + if let Ok(mut timestamped_entry) = value_ref.lock() { timestamped_entry.entry = new_entry; timestamped_entry.timestamp = Instant::now(); } - + if let Some(sender) = update_sender { // Reuse TimelineUpdate MediaFetched to trigger redraw in the timeline. let _ = sender.send(TimelineUpdate::LinkPreviewFetched); diff --git a/src/home/loading_pane.rs b/src/home/loading_pane.rs index baa975a3d..474dbc78b 100644 --- a/src/home/loading_pane.rs +++ b/src/home/loading_pane.rs @@ -3,7 +3,6 @@ use matrix_sdk::ruma::OwnedEventId; use crate::sliding_sync::TimelineRequestSender; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -88,8 +87,6 @@ script_mod! { } } - - /// The state of a LoadingPane: the possible tasks that it may be performing. #[derive(Clone, Default)] pub enum LoadingPaneState { @@ -110,16 +107,25 @@ pub enum LoadingPaneState { None, } - #[derive(Script, ScriptHook, Widget)] pub struct LoadingPane { - #[deref] view: View, - #[rust] state: LoadingPaneState, + #[deref] + view: View, + #[rust] + state: LoadingPaneState, } impl Drop for LoadingPane { fn drop(&mut self) { - if let LoadingPaneState::BackwardsPaginateUntilEvent { target_event_id, request_sender, .. } = &self.state { - warning!("Dropping LoadingPane with target_event_id: {}", target_event_id); + if let LoadingPaneState::BackwardsPaginateUntilEvent { + target_event_id, + request_sender, + .. + } = &self.state + { + warning!( + "Dropping LoadingPane with target_event_id: {}", + target_event_id + ); request_sender.send_if_modified(|requests| { let initial_len = requests.len(); requests.retain(|r| &r.target_event_id != target_event_id); @@ -131,7 +137,6 @@ impl Drop for LoadingPane { } } - impl Widget for LoadingPane { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { self.visible = true; @@ -144,7 +149,9 @@ impl Widget for LoadingPane { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - if !self.visible { return; } + if !self.visible { + return; + } self.view.handle_event(cx, event, scope); let area = self.view.area(); @@ -159,23 +166,31 @@ impl Widget for LoadingPane { matches!( event, Event::Actions(actions) if self.button(cx, ids!(cancel_button)).clicked(actions) - ) - || event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(_fde) => { - cx.set_key_focus(area); - false - } - Hit::FingerUp(fue) if fue.is_over => { - fue.mouse_button().is_some_and(|b| b.is_back()) - || !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + ) || event.back_pressed() + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + Hit::FingerUp(fue) if fue.is_over => { + fue.mouse_button().is_some_and(|b| b.is_back()) + || !self + .view(cx, ids!(main_content)) + .area() + .rect(cx) + .contains(fue.abs) + } + _ => false, } - _ => false, - } }; if close_pane { - if let LoadingPaneState::BackwardsPaginateUntilEvent { target_event_id, request_sender, .. } = &self.state { + if let LoadingPaneState::BackwardsPaginateUntilEvent { + target_event_id, + request_sender, + .. + } = &self.state + { let _did_send = request_sender.send_if_modified(|requests| { let initial_len = requests.len(); requests.retain(|r| &r.target_event_id != target_event_id); @@ -183,7 +198,8 @@ impl Widget for LoadingPane { // such that they can stop looking for the target event. requests.len() != initial_len }); - log!("LoadingPane: {} cancel request for target_event_id: {target_event_id}", + log!( + "LoadingPane: {} cancel request for target_event_id: {target_event_id}", if _did_send { "Sent" } else { "Did not send" }, ); } @@ -194,7 +210,6 @@ impl Widget for LoadingPane { } } - impl LoadingPane { /// Returns `true` if this pane is currently being shown. pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { @@ -216,10 +231,13 @@ impl LoadingPane { .. } => { self.set_title(cx, "Searching older messages..."); - self.set_status(cx, &format!( - "Looking for event {target_event_id}\n\n\ + self.set_status( + cx, + &format!( + "Looking for event {target_event_id}\n\n\ Fetched {events_paginated} messages so far...", - )); + ), + ); cancel_button.set_text(cx, "Cancel"); } LoadingPaneState::Error(error_message) => { @@ -227,7 +245,7 @@ impl LoadingPane { self.set_status(cx, error_message); cancel_button.set_text(cx, "Okay"); } - LoadingPaneState::None => { } + LoadingPaneState::None => {} } self.state = state; @@ -246,13 +264,17 @@ impl LoadingPane { impl LoadingPaneRef { /// See [`LoadingPane::is_currently_shown()`] pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { return false }; + let Some(inner) = self.borrow() else { + return false; + }; inner.is_currently_shown(cx) } /// See [`LoadingPane::show()`] pub fn show(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx); } @@ -263,17 +285,23 @@ impl LoadingPaneRef { } pub fn set_state(&self, cx: &mut Cx, state: LoadingPaneState) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_state(cx, state); } pub fn set_status(&self, cx: &mut Cx, status: &str) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_status(cx, status); } pub fn set_title(&self, cx: &mut Cx, title: &str) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_title(cx, title); } } diff --git a/src/home/location_preview.rs b/src/home/location_preview.rs index 958b4416f..f88f5f96c 100644 --- a/src/home/location_preview.rs +++ b/src/home/location_preview.rs @@ -10,7 +10,9 @@ use std::time::SystemTime; use makepad_widgets::*; use robius_location::Coordinates; -use crate::location::{get_latest_location, request_location_update, LocationAction, LocationRequest, LocationUpdate}; +use crate::location::{ + get_latest_location, request_location_update, LocationAction, LocationRequest, LocationUpdate, +}; script_mod! { use mod.prelude.widgets.* @@ -89,12 +91,14 @@ script_mod! { } } - #[derive(Script, ScriptHook, Widget)] struct LocationPreview { - #[deref] view: View, - #[rust] coords: Option>, - #[rust] timestamp: Option, + #[deref] + view: View, + #[rust] + coords: Option>, + #[rust] + timestamp: Option, } impl Widget for LocationPreview { @@ -106,16 +110,18 @@ impl Widget for LocationPreview { Some(LocationAction::Update(LocationUpdate { coordinates, time })) => { self.coords = Some(Ok(*coordinates)); self.timestamp = *time; - self.button(cx, ids!(send_location_button)).set_enabled(cx, true); + self.button(cx, ids!(send_location_button)) + .set_enabled(cx, true); needs_redraw = true; } Some(LocationAction::Error(e)) => { self.coords = Some(Err(*e)); self.timestamp = None; - self.button(cx, ids!(send_location_button)).set_enabled(cx, false); + self.button(cx, ids!(send_location_button)) + .set_enabled(cx, false); needs_redraw = true; } - _ => { } + _ => {} } } @@ -123,7 +129,10 @@ impl Widget for LocationPreview { // in the RoomScreen handle_event function. // Handle the cancel location button being clicked. - if self.button(cx, ids!(cancel_location_button)).clicked(actions) { + if self + .button(cx, ids!(cancel_location_button)) + .clicked(actions) + { self.clear(); needs_redraw = true; } @@ -149,7 +158,6 @@ impl Widget for LocationPreview { } } - impl LocationPreview { fn show(&mut self) { request_location_update(LocationRequest::UpdateOnce); diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 628d477bf..fd95bd753 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,8 +3,19 @@ use ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, utils::RoomNameId}; -use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; +use crate::{ + app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, + home::{ + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + rooms_list::RoomsListRef, + space_lobby::SpaceLobbyScreenWidgetRefExt, + }, + utils::RoomNameId, +}; +use super::{ + invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, + rooms_list::RoomsListAction, +}; script_mod! { use mod.prelude.widgets.* @@ -75,7 +86,8 @@ pub struct MainDesktopUI { /// The default layout that should be loaded into the dock /// when there is no previously-saved content to restore. /// This is a Rust-level instance of the dock content defined in the above live DSL. - #[rust] default_layout: SavedDockState, + #[rust] + default_layout: SavedDockState, /// The rooms that are currently open, keyed by the LiveId of their tab. #[rust] @@ -99,7 +111,8 @@ pub struct MainDesktopUI { /// /// This determines which set of rooms this dock is currently showing. /// If `None`, we're displaying the main home view of all rooms from any space. - #[rust] selected_space: Option, + #[rust] + selected_space: Option, /// Boolean to indicate if we've drawn the MainDesktopUi previously in the desktop view. /// @@ -142,7 +155,11 @@ impl MainDesktopUI { /// Focuses on a room if it is already open, otherwise creates a new tab for the room. fn focus_or_create_tab(&mut self, cx: &mut Cx, room: SelectedRoom) { // Do nothing if the room to select is already created and focused. - if self.most_recently_selected_room.as_ref().is_some_and(|sr| sr == &room) { + if self + .most_recently_selected_room + .as_ref() + .is_some_and(|sr| sr == &room) + { return; } @@ -158,15 +175,16 @@ impl MainDesktopUI { // Create a new tab for the room let kind = match &room { - SelectedRoom::JoinedRoom { .. } - | SelectedRoom::Thread { .. } => id!(room_screen), + SelectedRoom::JoinedRoom { .. } | SelectedRoom::Thread { .. } => id!(room_screen), SelectedRoom::InvitedRoom { .. } => id!(invite_screen), SelectedRoom::Space { .. } => id!(space_lobby_screen), }; // Insert the tab after the currently-selected room's tab, if possible. // Otherwise, insert it after the home tab, which should always exist. - let (tab_bar, insert_after) = self.most_recently_selected_room.as_ref() + let (tab_bar, insert_after) = self + .most_recently_selected_room + .as_ref() .and_then(|curr_room| dock.find_tab_bar_of_tab(curr_room.tab_id())) .unwrap_or_else(|| dock.find_tab_bar_of_tab(id!(home_tab)).unwrap()); @@ -184,14 +202,15 @@ impl MainDesktopUI { if let Some(new_widget) = new_tab_widget { self.room_order.push(room.clone()); match &room { - SelectedRoom::JoinedRoom { room_name_id } => { - new_widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - None, - ); + SelectedRoom::JoinedRoom { room_name_id } => { + new_widget + .as_room_screen() + .set_displayed_room(cx, room_name_id, None); } - SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + SelectedRoom::Thread { + room_name_id, + thread_root_event_id, + } => { new_widget.as_room_screen().set_displayed_room( cx, room_name_id, @@ -199,16 +218,14 @@ impl MainDesktopUI { ); } SelectedRoom::InvitedRoom { room_name_id } => { - new_widget.as_invite_screen().set_displayed_invite( - cx, - room_name_id, - ); + new_widget + .as_invite_screen() + .set_displayed_invite(cx, room_name_id); } SelectedRoom::Space { space_name_id } => { - new_widget.as_space_lobby_screen().set_displayed_space( - cx, - space_name_id, - ); + new_widget + .as_space_lobby_screen() + .set_displayed_space(cx, space_name_id); } } cx.action(MainDesktopUiAction::SaveDockIntoAppState); @@ -256,7 +273,7 @@ impl MainDesktopUI { /// Closes all tabs pub fn close_all_tabs(&mut self, cx: &mut Cx) { let dock = self.view.dock(cx, ids!(dock)); - for tab_id in self.open_rooms.keys() { + for tab_id in self.open_rooms.keys() { dock.close_tab(cx, *tab_id); } @@ -297,7 +314,9 @@ impl MainDesktopUI { // Go through all existing `SelectedRoom` instances and replace the // `SelectedRoom::InvitedRoom`s with `SelectedRoom::JoinedRoom`s. - for selected_room in self.most_recently_selected_room.iter_mut() + for selected_room in self + .most_recently_selected_room + .iter_mut() .chain(self.room_order.iter_mut()) .chain(self.open_rooms.values_mut()) { @@ -305,7 +324,9 @@ impl MainDesktopUI { } // Finally, emit an action to update the AppState with the new room. - cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name_id.room_id().clone())); + cx.action(AppStateAction::UpgradedInviteToJoinedRoom( + room_name_id.room_id().clone(), + )); } /// Saves a copy of the current UI state of the dock into the given app state, @@ -313,19 +334,18 @@ impl MainDesktopUI { fn save_dock_state_to(&mut self, cx: &mut Cx, app_state: &mut AppState) { if self.open_rooms.is_empty() { return; - } + } let saved_dock_state = self.save_dock_state(cx); if let Some(space_id) = self.selected_space.as_ref() { - app_state.saved_dock_state_per_space.insert( - space_id.clone(), - saved_dock_state, - ); + app_state + .saved_dock_state_per_space + .insert(space_id.clone(), saved_dock_state); } else { app_state.saved_dock_state_home = saved_dock_state; } } - /// An inner function that creates a `SavedDockState` from the current contents of this widget. + /// An inner function that creates a `SavedDockState` from the current contents of this widget. fn save_dock_state(&self, cx: &mut Cx) -> SavedDockState { let dock = self.view.dock(cx, ids!(dock)); SavedDockState { @@ -352,7 +372,12 @@ impl MainDesktopUI { Some(sds) if sds.open_rooms.is_empty() => &self.default_layout, Some(sds) => sds, }; - let SavedDockState { dock_items, open_rooms, room_order, selected_room } = to_restore; + let SavedDockState { + dock_items, + open_rooms, + room_order, + selected_room, + } = to_restore; self.room_order = room_order.clone(); self.open_rooms = open_rooms.clone(); @@ -364,37 +389,38 @@ impl MainDesktopUI { for (head_live_id, (_, widget)) in dock.items().iter() { match self.open_rooms.get(head_live_id) { Some(SelectedRoom::JoinedRoom { room_name_id }) => { - widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - None, - ); + widget + .as_room_screen() + .set_displayed_room(cx, room_name_id, None); } Some(SelectedRoom::InvitedRoom { room_name_id }) => { - widget.as_invite_screen().set_displayed_invite( - cx, - room_name_id, - ); + widget + .as_invite_screen() + .set_displayed_invite(cx, room_name_id); } Some(SelectedRoom::Space { space_name_id }) => { - widget.as_space_lobby_screen().set_displayed_space( - cx, - space_name_id, - ); + widget + .as_space_lobby_screen() + .set_displayed_space(cx, space_name_id); } - Some(SelectedRoom::Thread { room_name_id, thread_root_event_id }) => { + Some(SelectedRoom::Thread { + room_name_id, + thread_root_event_id, + }) => { widget.as_room_screen().set_displayed_room( cx, room_name_id, Some(thread_root_event_id.clone()), ); } - None => { } + None => {} } } } } else { - error!("BUG: failed to borrow dock widget to restore state upon LoadDockFromAppState action."); + error!( + "BUG: failed to borrow dock widget to restore state upon LoadDockFromAppState action." + ); return; } // Note: the borrow of `dock` must end here *before* we call `self.focus_or_create_tab()`. @@ -415,7 +441,8 @@ impl WidgetMatchEvent for MainDesktopUI { for action in actions { let widget_action = action.as_widget_action(); - if let Some(MainDesktopUiAction::CloseAllTabs { on_close_all }) = action.downcast_ref() { + if let Some(MainDesktopUiAction::CloseAllTabs { on_close_all }) = action.downcast_ref() + { self.close_all_tabs(cx); on_close_all.notify_one(); continue; @@ -426,7 +453,7 @@ impl WidgetMatchEvent for MainDesktopUI { if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { let new_space = match (tab, self.selected_space.as_ref()) { (SelectedTab::Space { space_name_id }, space_id_opt) - if space_id_opt.is_none_or(|id| id != space_name_id.room_id()) => + if space_id_opt.is_none_or(|id| id != space_name_id.room_id()) => { Some(space_name_id.room_id().clone()) } @@ -448,8 +475,7 @@ impl WidgetMatchEvent for MainDesktopUI { if tab_id == id!(home_tab) { cx.action(AppStateAction::FocusNone); self.most_recently_selected_room = None; - } - else if let Some(selected_room) = self.open_rooms.get(&tab_id) { + } else if let Some(selected_room) = self.open_rooms.get(&tab_id) { cx.action(AppStateAction::RoomFocused(selected_room.clone())); self.most_recently_selected_room = Some(selected_room.clone()); } @@ -475,7 +501,11 @@ impl WidgetMatchEvent for MainDesktopUI { // When dragging a tab, allow it to be dragged DockAction::Drag(drag_event) => { if drag_event.items.len() == 1 { - self.view.dock(cx, ids!(dock)).accept_drag(cx, drag_event, DragResponse::Move); + self.view.dock(cx, ids!(dock)).accept_drag( + cx, + drag_event, + DragResponse::Move, + ); } } // When dropping a tab, move it to the new position @@ -484,8 +514,11 @@ impl WidgetMatchEvent for MainDesktopUI { if let DragItem::FilePath { internal_id: Some(internal_id), .. - } = &drop_event.items[0] { - self.view.dock(cx, ids!(dock)).drop_move(cx, drop_event.abs, *internal_id); + } = &drop_event.items[0] + { + self.view + .dock(cx, ids!(dock)) + .drop_move(cx, drop_event.abs, *internal_id); } should_save_dock_action = true; } @@ -504,7 +537,7 @@ impl WidgetMatchEvent for MainDesktopUI { self.replace_invite_with_joined_room(cx, scope, room_name_id); } RoomsListAction::OpenRoomContextMenu { .. } => {} - RoomsListAction::None => { } + RoomsListAction::None => {} } // Handle our own actions related to dock updates that we have previously emitted. @@ -535,7 +568,5 @@ pub enum MainDesktopUiAction { /// Load the room panel state from the AppState to the dock. LoadDockFromAppState, /// Close all tabs; see [`MainDesktopUI::close_all_tabs()`] - CloseAllTabs { - on_close_all: Arc, - }, + CloseAllTabs { on_close_all: Arc }, } diff --git a/src/home/main_mobile_ui.rs b/src/home/main_mobile_ui.rs index f06118447..2741c4ea7 100644 --- a/src/home/main_mobile_ui.rs +++ b/src/home/main_mobile_ui.rs @@ -1,7 +1,11 @@ use makepad_widgets::*; use crate::{ - app::{AppState, AppStateAction, SelectedRoom}, home::{room_screen::RoomScreenWidgetExt, rooms_list::RoomsListAction, space_lobby::SpaceLobbyScreenWidgetExt} + app::{AppState, AppStateAction, SelectedRoom}, + home::{ + room_screen::RoomScreenWidgetExt, rooms_list::RoomsListAction, + space_lobby::SpaceLobbyScreenWidgetExt, + }, }; use super::invite_screen::InviteScreenWidgetExt; @@ -61,8 +65,12 @@ impl Widget for MainMobileUI { RoomsListAction::Selected(_selected_room) => {} // Because the MainMobileUI is drawn based on the AppState only, // all we need to do is update the AppState here. - RoomsListAction::InviteAccepted { room_name_id: room_name } => { - cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name.room_id().clone())); + RoomsListAction::InviteAccepted { + room_name_id: room_name, + } => { + cx.action(AppStateAction::UpgradedInviteToJoinedRoom( + room_name.room_id().clone(), + )); } RoomsListAction::OpenRoomContextMenu { .. } => {} RoomsListAction::None => {} @@ -107,7 +115,10 @@ impl Widget for MainMobileUI { .space_lobby_screen(cx, ids!(space_lobby_screen)) .set_displayed_space(cx, space_name_id); } - Some(SelectedRoom::Thread { room_name_id, thread_root_event_id }) => { + Some(SelectedRoom::Thread { + room_name_id, + thread_root_event_id, + }) => { show_welcome = false; show_room = true; show_invite = false; @@ -124,10 +135,18 @@ impl Widget for MainMobileUI { } } - self.view.view(cx, ids!(welcome)).set_visible(cx, show_welcome); - self.view.view(cx, ids!(room_view)).set_visible(cx, show_room); - self.view.view(cx, ids!(invite_view)).set_visible(cx, show_invite); - self.view.view(cx, ids!(space_lobby_view)).set_visible(cx, show_space_lobby); + self.view + .view(cx, ids!(welcome)) + .set_visible(cx, show_welcome); + self.view + .view(cx, ids!(room_view)) + .set_visible(cx, show_room); + self.view + .view(cx, ids!(invite_view)) + .set_visible(cx, show_invite); + self.view + .view(cx, ids!(space_lobby_view)) + .set_visible(cx, show_space_lobby); self.view.draw_walk(cx, scope, walk) } } diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index 95cec1317..371560d93 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -9,7 +9,7 @@ //! 2. Add Room (plus sign icon): a separate view that allows adding (joining) existing rooms, //! exploring public rooms, or creating new rooms/spaces. //! 3. Spaces: a button that toggles the `SpacesBar` (shows/hides it). -//! * This is NOT a regular radio button, it's a separate toggle. +//! * This is NOT a regular radio button, it's a separate toggle. //! * This is only shown in Mobile view mode, because the `SpacesBar` is always shown //! within the NavigationTabBar itself in Desktop view mode. //! 4. Activity (an inbox, alert bell, or notifications icon): a separate view that shows @@ -31,12 +31,20 @@ use makepad_widgets::*; use serde::{Deserialize, Serialize}; use crate::{ - avatar_cache::{self, AvatarCacheEntry}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, profile::{ + avatar_cache::{self, AvatarCacheEntry}, + login::login_screen::LoginAction, + logout::logout_confirm_modal::LogoutAction, + profile::{ user_profile::UserProfile, user_profile_cache::{self, UserProfileUpdate}, - }, shared::{ - avatar::{AvatarState, AvatarWidgetExt}, styles::*, verification_badge::VerificationBadgeWidgetExt - }, sliding_sync::{current_user_id, AccountDataAction}, utils::{self, RoomNameId} + }, + shared::{ + avatar::{AvatarState, AvatarWidgetExt}, + styles::*, + verification_badge::VerificationBadgeWidgetExt, + }, + sliding_sync::{current_user_id, AccountDataAction}, + utils::{self, RoomNameId}, }; script_mod! { @@ -162,7 +170,7 @@ script_mod! { flow: Down, align: Align{x: 0.5} padding: Inset{top: 40., bottom: 8} - width: (NAVIGATION_TAB_BAR_SIZE), + width: (NAVIGATION_TAB_BAR_SIZE), height: Fill draw_bg +: { @@ -228,8 +236,10 @@ script_mod! { /// Clicking on this icon will open the settings screen. #[derive(Script, Widget)] pub struct ProfileIcon { - #[deref] view: View, - #[rust] own_profile: Option, + #[deref] + view: View, + #[rust] + own_profile: Option, } impl ScriptHook for ProfileIcon { @@ -258,13 +268,15 @@ impl Widget for ProfileIcon { needs_redraw = true; } // If we're waiting for an avatar image, process avatar updates. - if let Some(p) = self.own_profile.as_mut() && p.avatar_state.uri().is_some() { + if let Some(p) = self.own_profile.as_mut() + && p.avatar_state.uri().is_some() + { avatar_cache::process_avatar_updates(cx); let new_data = p.avatar_state.update_from_cache(cx); needs_redraw |= new_data.is_some(); if new_data.is_some() { user_profile_cache::enqueue_user_profile_update( - UserProfileUpdate::UserProfileOnly(p.clone()) + UserProfileUpdate::UserProfileOnly(p.clone()), ); } } @@ -296,7 +308,7 @@ impl Widget for ProfileIcon { if let Some(p) = self.own_profile.as_mut() { p.avatar_state = AvatarState::Known(None); user_profile_cache::enqueue_user_profile_update( - UserProfileUpdate::UserProfileOnly(p.clone()) + UserProfileUpdate::UserProfileOnly(p.clone()), ); self.view.redraw(cx); } @@ -307,7 +319,7 @@ impl Widget for ProfileIcon { p.avatar_state = AvatarState::Known(Some(new_uri.clone())); p.avatar_state.update_from_cache(cx); user_profile_cache::enqueue_user_profile_update( - UserProfileUpdate::UserProfileOnly(p.clone()) + UserProfileUpdate::UserProfileOnly(p.clone()), ); self.view.redraw(cx); } @@ -321,7 +333,7 @@ impl Widget for ProfileIcon { if let Some(p) = self.own_profile.as_mut() { p.username = new_display_name.clone(); user_profile_cache::enqueue_user_profile_update( - UserProfileUpdate::UserProfileOnly(p.clone()) + UserProfileUpdate::UserProfileOnly(p.clone()), ); self.view.redraw(cx); } @@ -339,22 +351,33 @@ impl Widget for ProfileIcon { let area = self.view.area(); match event.hits(cx, area) { Hit::FingerLongPress(_) | Hit::FingerHoverIn(_) => { - let (verification_str, bg_color) = self.view + let (verification_str, bg_color) = self + .view .verification_badge(cx, ids!(verification_badge)) .tooltip_content(); let text = self.own_profile.as_ref().map_or_else( || format!("Not logged in.\n\n{}", verification_str), - |p| format!("Logged in as \"{}\".\n\n{}", p.displayable_name(), verification_str) + |p| { + format!( + "Logged in as \"{}\".\n\n{}", + p.displayable_name(), + verification_str + ) + }, ); let mut options = CalloutTooltipOptions { - position: if cx.display_context.is_desktop() { TooltipPosition::Right} else { TooltipPosition::Top}, + position: if cx.display_context.is_desktop() { + TooltipPosition::Right + } else { + TooltipPosition::Top + }, ..Default::default() }; if let Some(c) = bg_color { options.bg_color = c; } cx.widget_action( - self.widget_uid(), + self.widget_uid(), TooltipAction::HoverIn { text, widget_rect: area.rect(cx), @@ -363,9 +386,9 @@ impl Widget for ProfileIcon { ); } Hit::FingerHoverOut(_) => { - cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); } - _ => { } + _ => {} }; self.view.handle_event(cx, event, scope); @@ -386,11 +409,13 @@ impl Widget for ProfileIcon { let mut drew_avatar = false; if let Some(avatar_img_data) = own_profile.avatar_state.data() { - drew_avatar = our_own_avatar.show_image( - cx, - None, // don't make this avatar clickable; we handle clicks on this ProfileIcon widget directly. - |cx, img| utils::load_png_or_jpg(&img, cx, avatar_img_data), - ).is_ok(); + drew_avatar = our_own_avatar + .show_image( + cx, + None, // don't make this avatar clickable; we handle clicks on this ProfileIcon widget directly. + |cx, img| utils::load_png_or_jpg(&img, cx, avatar_img_data), + ) + .is_ok(); } if !drew_avatar { our_own_avatar.show_text( @@ -405,16 +430,17 @@ impl Widget for ProfileIcon { } } - /// The tab bar with buttons that navigate through top-level app pages. /// /// * In the "desktop" (wide) layout, this is a vertical bar on the left. /// * In the "mobile" (narrow) layout, this is a horizontal bar on the bottom. #[derive(Script, Widget)] pub struct NavigationTabBar { - #[deref] view: AdaptiveView, + #[deref] + view: AdaptiveView, - #[rust] is_spaces_bar_shown: bool, + #[rust] + is_spaces_bar_shown: bool, } impl ScriptHook for NavigationTabBar { @@ -435,19 +461,22 @@ impl Widget for NavigationTabBar { if let Event::Actions(actions) = event { // Handle one of the radio buttons being clicked (selected). - let radio_button_set = self.view.radio_button_set(cx, ids_array!( - home_button, - add_room_button, - settings_button, - )); + let radio_button_set = self.view.radio_button_set( + cx, + ids_array!(home_button, add_room_button, settings_button,), + ); match radio_button_set.selected(cx, actions) { Some(0) => cx.action(NavigationBarAction::GoToHome), Some(1) => cx.action(NavigationBarAction::GoToAddRoom), Some(2) => cx.action(NavigationBarAction::OpenSettings), - _ => { } + _ => {} } - if self.view.button(cx, ids!(toggle_spaces_bar_button)).clicked(actions) { + if self + .view + .button(cx, ids!(toggle_spaces_bar_button)) + .clicked(actions) + { self.is_spaces_bar_shown = !self.is_spaces_bar_shown; cx.action(NavigationBarAction::ToggleSpacesBar); } @@ -457,9 +486,18 @@ impl Widget for NavigationTabBar { // update our radio buttons accordingly. if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { match tab { - SelectedTab::Home => self.view.radio_button(cx, ids!(home_button)).select(cx, scope), - SelectedTab::AddRoom => self.view.radio_button(cx, ids!(add_room_button)).select(cx, scope), - SelectedTab::Settings => self.view.radio_button(cx, ids!(settings_button)).select(cx, scope), + SelectedTab::Home => self + .view + .radio_button(cx, ids!(home_button)) + .select(cx, scope), + SelectedTab::AddRoom => self + .view + .radio_button(cx, ids!(add_room_button)) + .select(cx, scope), + SelectedTab::Settings => self + .view + .radio_button(cx, ids!(settings_button)) + .select(cx, scope), SelectedTab::Space { .. } => { for rb in radio_button_set.iter() { if let Some(mut rb_inner) = rb.borrow_mut() { @@ -479,7 +517,6 @@ impl Widget for NavigationTabBar { } } - /// Which top-level view is currently shown, and which navigation tab is selected. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SelectedTab { @@ -488,10 +525,11 @@ pub enum SelectedTab { AddRoom, Settings, // AlertsInbox, - Space { space_name_id: RoomNameId }, + Space { + space_name_id: RoomNameId, + }, } - /// Actions for navigating through the top-level views of the app, /// e.g., when the user clicks/taps on a button in the NavigationTabBar. /// @@ -534,9 +572,8 @@ pub enum NavigationBarAction { GoToSpace { space_name_id: RoomNameId }, // TODO: add GoToAlertsInbox, once we add that button/screen - /// The given tab was selected as the active top-level view. - /// This is needed to ensure that the proper tab is marked as selected. + /// This is needed to ensure that the proper tab is marked as selected. TabSelected(SelectedTab), /// Toggle whether the SpacesBar is shown, i.e., show/hide it. /// This is only applicable in the Mobile view mode, because the SpacesBar @@ -544,7 +581,6 @@ pub enum NavigationBarAction { ToggleSpacesBar, } - /// Returns the current user's profile and avatar, if available. pub fn get_own_profile(cx: &mut Cx) -> Option { let mut own_profile = None; @@ -562,12 +598,14 @@ pub fn get_own_profile(cx: &mut Cx) -> Option { ); // If we have an avatar URI to fetch, try to fetch it. if let Some(Some(avatar_uri)) = avatar_uri_to_fetch { - if let AvatarCacheEntry::Loaded(data) = avatar_cache::get_or_fetch_avatar(cx, &avatar_uri) { + if let AvatarCacheEntry::Loaded(data) = + avatar_cache::get_or_fetch_avatar(cx, &avatar_uri) + { if let Some(p) = own_profile.as_mut() { p.avatar_state = AvatarState::Loaded(data); // Update the user profile cache with the new avatar data. user_profile_cache::enqueue_user_profile_update( - UserProfileUpdate::UserProfileOnly(p.clone()) + UserProfileUpdate::UserProfileOnly(p.clone()), ); } } diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 06c963fb3..9291b07a0 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -11,7 +11,7 @@ use crate::sliding_sync::UserPowerLevels; use super::room_screen::MessageAction; const BUTTON_HEIGHT: f64 = 35.0; // KEEP IN SYNC WITH BUTTON_HEIGHT BELOW -const MENU_WIDTH: f64 = 215.0; // KEEP IN SYNC WITH MENU_WIDTH BELOW +const MENU_WIDTH: f64 = 215.0; // KEEP IN SYNC WITH MENU_WIDTH BELOW script_mod! { use mod.prelude.widgets.* @@ -203,7 +203,6 @@ script_mod! { } } - bitflags! { /// Possible actions that the user can perform on a message. /// @@ -243,7 +242,9 @@ impl MessageAbilities { abilities.set(Self::CanDelete, user_power_levels.can_redact_own()); } abilities.set(Self::CanReplyTo, event_tl_item.can_be_replied_to()); - if let Some(event_id) = event_tl_item.event_id() && user_power_levels.can_pin() { + if let Some(event_id) = event_tl_item.event_id() + && user_power_levels.can_pin() + { if pinned_events.iter().any(|ev| ev == event_id) { abilities.set(Self::CanUnpin, true); } else { @@ -254,7 +255,6 @@ impl MessageAbilities { abilities.set(Self::HasHtml, has_html); abilities } - } /// Details about the message that define its context menu content. @@ -290,9 +290,12 @@ impl MessageDetails { #[derive(Script, ScriptHook, Widget)] pub struct NewMessageContextMenu { - #[deref] view: View, - #[source] source: ScriptObjectRef, - #[rust] details: Option, + #[deref] + view: View, + #[source] + source: ScriptObjectRef, + #[rust] + details: Option, } impl Widget for NewMessageContextMenu { @@ -305,7 +308,9 @@ impl Widget for NewMessageContextMenu { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - if !self.visible { return; } + if !self.visible { + return; + } self.view.handle_event(cx, event, scope); let area = self.view.area(); @@ -317,23 +322,27 @@ impl Widget for NewMessageContextMenu { // 4. The user scrolls anywhere. let close_menu = { event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(fde) => { - let reaction_text_input = self.view.text_input(cx, ids!(reaction_input_view.reaction_text_input)); - if reaction_text_input.area().rect(cx).contains(fde.abs) { - reaction_text_input.set_key_focus(cx); - } else { - cx.set_key_focus(area); + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(fde) => { + let reaction_text_input = self + .view + .text_input(cx, ids!(reaction_input_view.reaction_text_input)); + if reaction_text_input.area().rect(cx).contains(fde.abs) { + reaction_text_input.set_key_focus(cx); + } else { + cx.set_key_focus(area); + } + false } - false - } - Hit::FingerUp(fue) if fue.is_over => { - !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + Hit::FingerUp(fue) if fue.is_over => !self + .view(cx, ids!(main_content)) + .area() + .rect(cx) + .contains(fue.abs), + Hit::FingerScroll(_) => true, + _ => false, } - Hit::FingerScroll(_) => true, - _ => false, - } }; if close_menu { self.close(cx); @@ -346,94 +355,100 @@ impl Widget for NewMessageContextMenu { impl WidgetMatchEvent for NewMessageContextMenu { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let Some(details) = self.details.as_ref() else { return }; + let Some(details) = self.details.as_ref() else { + return; + }; let mut close_menu = false; - let reaction_text_input = self.view.text_input(cx, ids!(reaction_input_view.reaction_text_input)); - let reaction_send_button = self.view.button(cx, ids!(reaction_input_view.reaction_send_button)); - if reaction_send_button.clicked(actions) - || reaction_text_input.returned(actions).is_some() + let reaction_text_input = self + .view + .text_input(cx, ids!(reaction_input_view.reaction_text_input)); + let reaction_send_button = self + .view + .button(cx, ids!(reaction_input_view.reaction_send_button)); + if reaction_send_button.clicked(actions) || reaction_text_input.returned(actions).is_some() { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::React { details: details.clone(), reaction: reaction_text_input.text(), }, ); close_menu = true; - } - else if reaction_text_input.escaped(actions) { + } else if reaction_text_input.escaped(actions) { close_menu = true; - } - else if self.button(cx, ids!(react_button)).clicked(actions) { + } else if self.button(cx, ids!(react_button)).clicked(actions) { // Show a box to allow the user to input the reaction. // In the future, we'll show an emoji chooser. - self.view.button(cx, ids!(react_button)).set_visible(cx, false); - self.view.view(cx, ids!(reaction_input_view)).set_visible(cx, true); - self.text_input(cx, ids!(reaction_input_view.reaction_text_input)).set_key_focus(cx); + self.view + .button(cx, ids!(react_button)) + .set_visible(cx, false); + self.view + .view(cx, ids!(reaction_input_view)) + .set_visible(cx, true); + self.text_input(cx, ids!(reaction_input_view.reaction_text_input)) + .set_key_focus(cx); self.redraw(cx); close_menu = false; - } - else if self.button(cx, ids!(reply_button)).clicked(actions) { + } else if self.button(cx, ids!(reply_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::Reply(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(edit_message_button)).clicked(actions) { + } else if self.button(cx, ids!(edit_message_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::Edit(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(pin_button)).clicked(actions) { + } else if self.button(cx, ids!(pin_button)).clicked(actions) { if details.abilities.contains(MessageAbilities::CanPin) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::Pin(details.clone()), ); } else if details.abilities.contains(MessageAbilities::CanUnpin) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::Unpin(details.clone()), ); } close_menu = true; - } - else if self.button(cx, ids!(copy_text_button)).clicked(actions) { + } else if self.button(cx, ids!(copy_text_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::CopyText(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(copy_html_button)).clicked(actions) { + } else if self.button(cx, ids!(copy_html_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::CopyHtml(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(copy_link_to_message_button)).clicked(actions) { + } else if self + .button(cx, ids!(copy_link_to_message_button)) + .clicked(actions) + { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::CopyLink(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(view_source_button)).clicked(actions) { + } else if self.button(cx, ids!(view_source_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::ViewSource(details.clone()), ); close_menu = true; - } - else if self.button(cx, ids!(jump_to_related_button)).clicked(actions) { + } else if self + .button(cx, ids!(jump_to_related_button)) + .clicked(actions) + { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::JumpToRelated(details.clone()), ); close_menu = true; @@ -452,7 +467,7 @@ impl WidgetMatchEvent for NewMessageContextMenu { // } else if self.button(cx, ids!(delete_button)).clicked(actions) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::Redact { details: details.clone(), // TODO: show a Modal to confirm deletion, and get the reason. @@ -493,7 +508,9 @@ impl NewMessageContextMenu { /// /// Returns the total height of all visible items. fn set_button_visibility(&mut self, cx: &mut Cx) -> f64 { - let Some(details) = self.details.as_ref() else { return 0.0 }; + let Some(details) = self.details.as_ref() else { + return 0.0; + }; let react_button = self.view.button(cx, ids!(react_button)); let reply_button = self.view.button(cx, ids!(reply_button)); @@ -525,10 +542,14 @@ impl NewMessageContextMenu { let show_divider_before_report_delete = show_delete; // || show_report; // Actually set the buttons' visibility. - self.view.view(cx, ids!(react_view)).set_visible(cx, show_react); + self.view + .view(cx, ids!(react_view)) + .set_visible(cx, show_react); react_button.set_visible(cx, show_react); reply_button.set_visible(cx, show_reply_to); - self.view.view(cx, ids!(divider_after_react_reply)).set_visible(cx, show_divider_after_react_reply); + self.view + .view(cx, ids!(divider_after_react_reply)) + .set_visible(cx, show_divider_after_react_reply); edit_button.set_visible(cx, show_edit); if details.abilities.contains(MessageAbilities::CanPin) { pin_button.set_text(cx, "Pin Message"); @@ -542,7 +563,9 @@ impl NewMessageContextMenu { pin_button.set_visible(cx, show_pin); copy_html_button.set_visible(cx, show_copy_html); jump_to_related_button.set_visible(cx, show_jump_to_related); - self.view.view(cx, ids!(divider_before_report_delete)).set_visible(cx, show_divider_before_report_delete); + self.view + .view(cx, ids!(divider_before_report_delete)) + .set_visible(cx, show_divider_before_report_delete); // report_button.set_visible(cx, show_report); delete_button.set_visible(cx, show_delete); @@ -560,13 +583,15 @@ impl NewMessageContextMenu { delete_button.reset_hover(cx); // Reset reaction input view stuff. - self.view.view(cx, ids!(reaction_input_view)).set_visible(cx, false); // hide until the react_button is clicked - self.text_input(cx, ids!(reaction_input_view.reaction_text_input)).set_text(cx, ""); + self.view + .view(cx, ids!(reaction_input_view)) + .set_visible(cx, false); // hide until the react_button is clicked + self.text_input(cx, ids!(reaction_input_view.reaction_text_input)) + .set_text(cx, ""); self.redraw(cx); - let num_visible_buttons = - show_react as u8 + let num_visible_buttons = show_react as u8 + show_reply_to as u8 + show_edit as u8 + show_pin as u8 @@ -583,7 +608,7 @@ impl NewMessageContextMenu { + if show_divider_after_react_reply { 10.0 } else { 0.0 } + if show_divider_before_report_delete { 10.0 } else { 0.0 } + 20.0 // top and bottom padding - + 1.0 // top and bottom border + + 1.0 // top and bottom border } fn close(&mut self, cx: &mut Cx) { @@ -597,13 +622,17 @@ impl NewMessageContextMenu { impl NewMessageContextMenuRef { /// See [`NewMessageContextMenu::is_currently_shown()`]. pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { return false }; + let Some(inner) = self.borrow() else { + return false; + }; inner.is_currently_shown(cx) } /// See [`NewMessageContextMenu::show()`]. pub fn show(&self, cx: &mut Cx, details: MessageDetails) -> DVec2 { - let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; + let Some(mut inner) = self.borrow_mut() else { + return DVec2::default(); + }; inner.show(cx, details) } } diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index c55b7fa54..4020ca502 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,7 +3,12 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{home::invite_modal::InviteModalAction, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; +use crate::{ + home::invite_modal::InviteModalAction, + shared::popup_list::{PopupKind, enqueue_popup_notification}, + sliding_sync::{MatrixRequest, submit_async_request}, + utils::RoomNameId, +}; const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; @@ -69,7 +74,7 @@ script_mod! { } priority_button := mod.widgets.RoomContextMenuButton { - draw_icon +: { svg: (ICON_TOMBSTONE) } + draw_icon +: { svg: (ICON_TOMBSTONE) } text: "Set Low Priority" } @@ -77,7 +82,7 @@ script_mod! { draw_icon +: { svg: (ICON_LINK) } text: "Copy Link to Room" } - + divider1 := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -137,9 +142,12 @@ pub enum RoomContextMenuAction { #[derive(Script, ScriptHook, Widget)] pub struct RoomContextMenu { - #[deref] view: View, - #[source] source: ScriptObjectRef, - #[rust] details: Option, + #[deref] + view: View, + #[source] + source: ScriptObjectRef, + #[rust] + details: Option, } impl Widget for RoomContextMenu { @@ -151,21 +159,25 @@ impl Widget for RoomContextMenu { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - if !self.visible { return; } + if !self.visible { + return; + } self.view.handle_event(cx, event, scope); // Close logic similar to NewMessageContextMenu let area = self.view.area(); let close_menu = { event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerUp(fue) if fue.is_over => { - !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerUp(fue) if fue.is_over => !self + .view(cx, ids!(main_content)) + .area() + .rect(cx) + .contains(fue.abs), + Hit::FingerScroll(_) => true, + _ => false, } - Hit::FingerScroll(_) => true, - _ => false, - } }; if close_menu { @@ -179,31 +191,30 @@ impl Widget for RoomContextMenu { impl WidgetMatchEvent for RoomContextMenu { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let Some(details) = self.details.as_ref() else { return }; + let Some(details) = self.details.as_ref() else { + return; + }; let mut close_menu = false; - + if self.button(cx, ids!(mark_unread_button)).clicked(actions) { submit_async_request(MatrixRequest::SetUnreadFlag { room_id: details.room_name_id.room_id().clone(), mark_as_unread: !details.is_marked_unread, }); close_menu = true; - } - else if self.button(cx, ids!(favorite_button)).clicked(actions) { + } else if self.button(cx, ids!(favorite_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsFavorite { room_id: details.room_name_id.room_id().clone(), is_favorite: !details.is_favorite, }); close_menu = true; - } - else if self.button(cx, ids!(priority_button)).clicked(actions) { + } else if self.button(cx, ids!(priority_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsLowPriority { room_id: details.room_name_id.room_id().clone(), is_low_priority: !details.is_low_priority, }); close_menu = true; - } - else if self.button(cx, ids!(copy_link_button)).clicked(actions) { + } else if self.button(cx, ids!(copy_link_button)).clicked(actions) { submit_async_request(MatrixRequest::GenerateMatrixLink { room_id: details.room_name_id.room_id().clone(), event_id: None, @@ -211,8 +222,7 @@ impl WidgetMatchEvent for RoomContextMenu { join_on_click: false, }); close_menu = true; - } - else if self.button(cx, ids!(room_settings_button)).clicked(actions) { + } else if self.button(cx, ids!(room_settings_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room settings page is not yet implemented.", @@ -220,8 +230,7 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } - else if self.button(cx, ids!(notifications_button)).clicked(actions) { + } else if self.button(cx, ids!(notifications_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room notifications page is not yet implemented.", @@ -229,12 +238,10 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } - else if self.button(cx, ids!(invite_button)).clicked(actions) { + } else if self.button(cx, ids!(invite_button)).clicked(actions) { cx.action(InviteModalAction::Open(details.room_name_id.clone())); close_menu = true; - } - else if self.button(cx, ids!(leave_button)).clicked(actions) { + } else if self.button(cx, ids!(leave_button)).clicked(actions) { use crate::join_leave_room_modal::{JoinLeaveRoomModalAction, JoinLeaveModalKind}; use crate::room::BasicRoomDetails; let room_details = BasicRoomDetails::Name(details.room_name_id.clone()); @@ -263,7 +270,7 @@ impl RoomContextMenu { cx.set_key_focus(self.view.area()); dvec2(MENU_WIDTH, height) } - + fn update_buttons(&mut self, cx: &mut Cx, details: &RoomContextMenuDetails) -> f64 { let mark_unread_button = self.button(cx, ids!(mark_unread_button)); if details.is_marked_unread { @@ -271,12 +278,12 @@ impl RoomContextMenu { } else { mark_unread_button.set_text(cx, "Mark as Unread"); } - + let favorite_button = self.button(cx, ids!(favorite_button)); if details.is_favorite { favorite_button.set_text(cx, "Un-favorite"); } else { - favorite_button.set_text(cx, "Favorite"); + favorite_button.set_text(cx, "Favorite"); } let priority_button = self.button(cx, ids!(priority_button)); @@ -285,7 +292,7 @@ impl RoomContextMenu { } else { priority_button.set_text(cx, "Set Low Priority"); } - + // Reset hover states mark_unread_button.reset_hover(cx); favorite_button.reset_hover(cx); @@ -295,9 +302,9 @@ impl RoomContextMenu { self.button(cx, ids!(notifications_button)).reset_hover(cx); self.button(cx, ids!(invite_button)).reset_hover(cx); self.button(cx, ids!(leave_button)).reset_hover(cx); - + self.redraw(cx); - + // Calculate height (rudimentary) - sum of visible buttons + padding // 8 buttons * 35.0 + 2 dividers * ~10.0 + padding (8.0 * BUTTON_HEIGHT) + 20.0 + 10.0 // approx @@ -313,12 +320,16 @@ impl RoomContextMenu { impl RoomContextMenuRef { pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { return false }; + let Some(inner) = self.borrow() else { + return false; + }; inner.is_currently_shown(cx) } pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 { - let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; + let Some(mut inner) = self.borrow_mut() else { + return DVec2::default(); + }; inner.show(cx, details) } } diff --git a/src/home/room_image_viewer.rs b/src/home/room_image_viewer.rs index 9bc11b6c4..9199d63e7 100644 --- a/src/home/room_image_viewer.rs +++ b/src/home/room_image_viewer.rs @@ -6,7 +6,10 @@ use matrix_sdk::{ }; use reqwest::StatusCode; -use crate::{media_cache::{MediaCache, MediaCacheEntry}, shared::image_viewer::{ImageViewerAction, ImageViewerError, LoadState}}; +use crate::{ + media_cache::{MediaCache, MediaCacheEntry}, + shared::image_viewer::{ImageViewerAction, ImageViewerError, LoadState}, +}; /// Populates the image viewer modal with the given media content. /// diff --git a/src/home/room_read_receipt.rs b/src/home/room_read_receipt.rs index d2bad9726..b85841b41 100644 --- a/src/home/room_read_receipt.rs +++ b/src/home/room_read_receipt.rs @@ -11,7 +11,6 @@ use matrix_sdk_ui::timeline::EventTimelineItem; use std::cmp; - /// The maximum number of items to display in the read receipts AvatarRow /// and its accompanying tooltip. pub const MAX_VISIBLE_AVATARS_IN_READ_RECEIPT: usize = 3; @@ -96,11 +95,10 @@ impl Widget for AvatarRow { let widget_rect = self.area.rect(cx); let should_hover_in = match event.hits(cx, self.area) { - Hit::FingerLongPress(_) - | Hit::FingerHoverIn(..) => true, + Hit::FingerLongPress(_) | Hit::FingerHoverIn(..) => true, Hit::FingerUp(fue) if fue.is_over && fue.is_primary_hit() => true, Hit::FingerHoverOut(_) => { - cx.widget_action(uid, RoomScreenTooltipActions::HoverOut); + cx.widget_action(uid, RoomScreenTooltipActions::HoverOut); false } _ => false, @@ -108,7 +106,7 @@ impl Widget for AvatarRow { if should_hover_in { if let Some(read_receipts) = &self.read_receipts { cx.widget_action( - uid, + uid, RoomScreenTooltipActions::HoverInReadReceipt { widget_rect, read_receipts: read_receipts.clone(), diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced9..b4be33658 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,40 +1,103 @@ //! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline //! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc}; +use std::{ + borrow::Cow, + cell::RefCell, + ops::{DerefMut, Range}, + sync::Arc, +}; use bytesize::ByteSize; use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ - EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ + OwnedServerName, RoomDisplayName, + media::{MediaFormat, MediaRequestParameters}, + room::RoomMember, + ruma::{ + EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, + events::{ receipt::Receipt, room::{ - ImageInfo, MediaSource, message::{ - AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent - } + ImageInfo, MediaSource, + message::{ + AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, + FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, + LocationMessageEventContent, MessageFormat, MessageType, + NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent, + }, }, sticker::{StickerEventContent, StickerMediaSource}, - }, matrix_uri::MatrixId, uint - } + }, + matrix_uri::MatrixId, + uint, + }, }; use matrix_sdk_ui::timeline::{ - self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem + self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, + MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, + PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, + TimelineItemContent, TimelineItemKind, VirtualTimelineItem, +}; +use ruma::{ + OwnedUserId, + api::client::receipt::create_receipt::v3::ReceiptType, + events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, + owned_room_id, }; -use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ - user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, + app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, + avatar_cache, + event_preview::{ + plaintext_body_of_timeline_item, text_preview_of_encrypted_message, + text_preview_of_member_profile_change, text_preview_of_other_message_like, + text_preview_of_other_state, text_preview_of_room_membership_change, + text_preview_of_timeline_item, + }, + home::{ + edited_indicator::EditedIndicatorWidgetRefExt, + link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, + loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, + room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, + rooms_list::{RoomsListAction, RoomsListRef}, + tombstone_footer::SuccessorRoomDetails, + }, + media_cache::{MediaCache, MediaCacheEntry}, + profile::{ + user_profile::{ + ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, + UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt, + }, user_profile_cache, }, - room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, typing_notice::TypingNoticeWidgetExt}, + room::{ + BasicRoomDetails, + room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, + typing_notice::TypingNoticeWidgetExt, + }, shared::{ - avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt + avatar::{AvatarState, AvatarWidgetRefExt}, + confirmation_modal::ConfirmationModalContent, + html_or_plaintext::{ + HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction, + }, + image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, + jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, + popup_list::{PopupKind, enqueue_popup_notification}, + restore_status_view::RestoreStatusViewWidgetExt, + styles::*, + text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, + timestamp::TimestampWidgetRefExt, + }, + sliding_sync::{ + BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, + TimelineKind, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, + take_timeline_endpoints, }, - sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} + utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime}, }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -43,7 +106,12 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction; use rangemap::RangeSet; -use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; +use super::{ + event_reaction_list::ReactionData, + loading_pane::LoadingPaneRef, + new_message_context_menu::{MessageAbilities, MessageDetails}, + room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}, +}; /// The maximum number of timeline items to search through /// when looking for a particular event. @@ -62,7 +130,6 @@ const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); /// #FFEACC const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -608,20 +675,27 @@ script_mod! { /// The main widget that displays a single Matrix room. #[derive(Script, Widget)] pub struct RoomScreen { - #[deref] view: View, + #[deref] + view: View, /// The name and ID of the currently-shown room, if any. - #[rust] room_name_id: Option, + #[rust] + room_name_id: Option, /// The timeline currently displayed by this RoomScreen, if any. - #[rust] timeline_kind: Option, + #[rust] + timeline_kind: Option, /// The persistent UI-relevant states for the room that this widget is currently displaying. - #[rust] tl_state: Option, + #[rust] + tl_state: Option, /// The set of pinned events in this room. - #[rust] pinned_events: Vec, + #[rust] + pinned_events: Vec, /// Whether this room has been successfully loaded (received from the homeserver). - #[rust] is_loaded: bool, + #[rust] + is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). - #[rust] all_rooms_loaded: bool, + #[rust] + all_rooms_loaded: bool, } impl Drop for RoomScreen { @@ -653,7 +727,8 @@ impl Widget for RoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { let room_screen_widget_uid = self.widget_uid(); let portal_list = self.portal_list(cx, ids!(timeline.list)); - let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); + let user_profile_sliding_pane = + self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); // Handle actions here before processing timeline updates. @@ -668,9 +743,13 @@ impl Widget for RoomScreen { if let RoomScreenTooltipActions::HoverInReactionButton { widget_rect, reaction_data, - } = reaction_list.hovered_in(actions) { - let Some(_tl_state) = self.tl_state.as_ref() else { continue }; - let tooltip_text_arr: Vec = reaction_data.reaction_senders + } = reaction_list.hovered_in(actions) + { + let Some(_tl_state) = self.tl_state.as_ref() else { + continue; + }; + let tooltip_text_arr: Vec = reaction_data + .reaction_senders .iter() .map(|(sender, _react_info)| { user_profile_cache::get_user_display_name_for_room( @@ -684,10 +763,13 @@ impl Widget for RoomScreen { }) .collect(); - let mut tooltip_text = utils::human_readable_list(&tooltip_text_arr, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT); + let mut tooltip_text = utils::human_readable_list( + &tooltip_text_arr, + MAX_VISIBLE_AVATARS_IN_READ_RECEIPT, + ); tooltip_text.push_str(&format!(" reacted with: {}", reaction_data.reaction)); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -701,24 +783,23 @@ impl Widget for RoomScreen { // Handle a hover-out action on the reaction list or avatar row. let avatar_row_ref = wr.avatar_row(cx, ids!(avatar_row)); - if reaction_list.hovered_out(actions) - || avatar_row_ref.hover_out(actions) - { - cx.widget_action( - room_screen_widget_uid, - TooltipAction::HoverOut, - ); + if reaction_list.hovered_out(actions) || avatar_row_ref.hover_out(actions) { + cx.widget_action(room_screen_widget_uid, TooltipAction::HoverOut); } // Handle a hover-in action on the avatar row: show a read receipts summary. if let RoomScreenTooltipActions::HoverInReadReceipt { widget_rect, - read_receipts - } = avatar_row_ref.hover_in(actions) { - let Some(room_id) = self.room_id() else { return; }; - let tooltip_text= room_read_receipt::populate_tooltip(cx, read_receipts, room_id); + read_receipts, + } = avatar_row_ref.hover_in(actions) + { + let Some(room_id) = self.room_id() else { + return; + }; + let tooltip_text = + room_read_receipt::populate_tooltip(cx, read_receipts, room_id); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -732,23 +813,27 @@ impl Widget for RoomScreen { // Handle an image within the message being clicked. let content_message = wr.text_or_image(cx, ids!(content.message)); - if let TextOrImageAction::Clicked(mxc_uri) = actions.find_widget_action(content_message.widget_uid()).cast() { + if let TextOrImageAction::Clicked(mxc_uri) = actions + .find_widget_action(content_message.widget_uid()) + .cast() + { let texture = content_message.get_texture(cx); - self.handle_image_click( - cx, - mxc_uri, - texture, - index, - ); + self.handle_image_click(cx, mxc_uri, texture, index); continue; } // Handle the invite_user_button (in a SmallStateEvent) being clicked. if wr.button(cx, ids!(invite_user_button)).clicked(actions) { - let Some(tl) = self.tl_state.as_ref() else { continue }; - if let Some(event_tl_item) = tl.items.get(index).and_then(|item| item.as_event()) { + let Some(tl) = self.tl_state.as_ref() else { + continue; + }; + if let Some(event_tl_item) = + tl.items.get(index).and_then(|item| item.as_event()) + { let user_id = event_tl_item.sender().to_owned(); - let username = if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { + let username = if let TimelineDetails::Ready(profile) = + event_tl_item.sender_profile() + { profile.display_name.as_deref().unwrap_or(user_id.as_str()) } else { user_id.as_str() @@ -756,14 +841,22 @@ impl Widget for RoomScreen { let room_id = tl.kind.room_id().clone(); let content = ConfirmationModalContent { title_text: "Send Invitation".into(), - body_text: format!("Are you sure you want to invite {username} to this room?").into(), + body_text: format!( + "Are you sure you want to invite {username} to this room?" + ) + .into(), accept_button_text: Some("Invite".into()), on_accept_clicked: Some(Box::new(move |_cx| { - submit_async_request(MatrixRequest::InviteUser { room_id, user_id }); + submit_async_request(MatrixRequest::InviteUser { + room_id, + user_id, + }); })), ..Default::default() }; - cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new(Some(content)))); + cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new( + Some(content), + ))); } } } @@ -772,11 +865,19 @@ impl Widget for RoomScreen { for action in actions { // Handle actions related to restoring the previously-saved state of rooms. - if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, ..}) = action.downcast_ref() { - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_name_id.room_id()) { + if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = + action.downcast_ref() + { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_name_id.room_id()) + { // `set_displayed_room()` does nothing if the room_name_id is unchanged, so we clear it first. self.room_name_id = None; - let thread_root_event_id = self.timeline_kind.as_ref() + let thread_root_event_id = self + .timeline_kind + .as_ref() .and_then(|k| k.thread_root_event_id().cloned()); self.set_displayed_room(cx, room_name_id, thread_root_event_id); return; @@ -786,7 +887,11 @@ impl Widget for RoomScreen { // Handle InviteResultAction to show popup notifications. if let Some(InviteResultAction::Sent { room_id, .. }) = action.downcast_ref() { // Only handle if this is for the current room. - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_id) + { enqueue_popup_notification( "Sent invite successfully.", PopupKind::Success, @@ -794,9 +899,15 @@ impl Widget for RoomScreen { ); } } - if let Some(InviteResultAction::Failed { room_id, error, .. }) = action.downcast_ref() { + if let Some(InviteResultAction::Failed { room_id, error, .. }) = + action.downcast_ref() + { // Only handle if this is for the current room. - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_id) + { enqueue_popup_notification( format!("Failed to send invite.\n\nError: {error}"), PopupKind::Error, @@ -806,11 +917,15 @@ impl Widget for RoomScreen { } // Handle the highlight animation for a message. - let Some(tl) = self.tl_state.as_mut() else { continue }; - if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { + let Some(tl) = self.tl_state.as_mut() else { + continue; + }; + if let MessageHighlightAnimationState::Pending { item_id } = + tl.message_highlight_animation_state + { if portal_list.smooth_scroll_reached(actions) { cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, MessageAction::HighlightMessage(item_id), ); tl.message_highlight_animation_state = MessageHighlightAnimationState::Off; @@ -834,22 +949,25 @@ impl Widget for RoomScreen { self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list); // Handle the jump to bottom button: update its visibility, and handle clicks. - self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)).update_from_actions( - cx, - &portal_list, - actions, - ); + self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)) + .update_from_actions(cx, &portal_list, actions); } // Currently, a Signal event is only used to tell this widget: // 1. to check if the room has been loaded from the homeserver yet, or // 2. that its timeline events have been updated in the background. if let Event::Signal = event { - if let (false, Some(room_name_id), true) = (self.is_loaded, self.room_name_id.as_ref(), cx.has_global::()) { + if let (false, Some(room_name_id), true) = ( + self.is_loaded, + self.room_name_id.as_ref(), + cx.has_global::(), + ) { let rooms_list_ref = cx.get_global::(); if rooms_list_ref.is_room_loaded(room_name_id.room_id()) { let room_name_clone = room_name_id.clone(); - let thread_root_event_id = self.timeline_kind.as_ref() + let thread_root_event_id = self + .timeline_kind + .as_ref() .and_then(|k| k.thread_root_event_id().cloned()); // This room has been loaded now, so we call `set_displayed_room()`. // We first clear the `room_name_id`, otherwise that function will do nothing. @@ -891,14 +1009,12 @@ impl Widget for RoomScreen { if is_interactive_hit { loading_pane.handle_event(cx, event, scope); } - } - else if user_profile_sliding_pane.is_currently_shown(cx) { + } else if user_profile_sliding_pane.is_currently_shown(cx) { is_pane_shown = true; if is_interactive_hit { user_profile_sliding_pane.handle_event(cx, event, scope); } - } - else { + } else { is_pane_shown = false; } @@ -917,10 +1033,12 @@ impl Widget for RoomScreen { // Fetch room data once to avoid duplicate expensive lookups let (room_display_name, room_avatar_url) = get_client() .and_then(|client| client.get_room(&room_id)) - .map(|room| ( - room.cached_display_name().unwrap_or(RoomDisplayName::Empty), - room.avatar_url() - )) + .map(|room| { + ( + room.cached_display_name().unwrap_or(RoomDisplayName::Empty), + room.avatar_url(), + ) + }) .unwrap_or((RoomDisplayName::Empty, None)); RoomScreenProps { @@ -935,7 +1053,9 @@ impl Widget for RoomScreen { RoomScreenProps { room_screen_widget_uid, room_name_id: room_name.clone(), - timeline_kind: self.timeline_kind.clone() + timeline_kind: self + .timeline_kind + .clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, room_avatar_url: None, @@ -945,7 +1065,9 @@ impl Widget for RoomScreen { if !is_pane_shown || !is_interactive_hit { return; } - log!("RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling"); + log!( + "RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling" + ); // Use a dummy room props for non-room-specific events let room_id = owned_room_id!("!dummy:matrix.org"); RoomScreenProps { @@ -958,13 +1080,11 @@ impl Widget for RoomScreen { }; let mut room_scope = Scope::with_props(&room_props); - // Forward the event to the inner timeline view, but capture any actions it produces // such that we can handle the ones relevant to only THIS RoomScreen widget right here and now, // ensuring they are not mistakenly handled by other RoomScreen widget instances. - let mut actions_generated_within_this_room_screen = cx.capture_actions(|cx| - self.view.handle_event(cx, event, &mut room_scope) - ); + let mut actions_generated_within_this_room_screen = + cx.capture_actions(|cx| self.view.handle_event(cx, event, &mut room_scope)); // Here, we handle and remove any general actions that are relevant to only this RoomScreen. // Removing the handled actions ensures they are not mistakenly handled by other RoomScreen widget instances. actions_generated_within_this_room_screen.retain(|action| { @@ -973,16 +1093,18 @@ impl Widget for RoomScreen { } // Handle the action that requests to show the user profile sliding pane. - if let ShowUserProfileAction::ShowUserProfile(profile_and_room_id) = action.as_widget_action().cast() { + if let ShowUserProfileAction::ShowUserProfile(profile_and_room_id) = + action.as_widget_action().cast() + { self.show_user_profile( cx, &user_profile_sliding_pane, UserProfilePaneInfo { profile_and_room_id, - room_name: self.room_name_id.as_ref().map_or_else( - || UNNAMED_ROOM.to_string(), - |r| r.to_string(), - ), + room_name: self + .room_name_id + .as_ref() + .map_or_else(|| UNNAMED_ROOM.to_string(), |r| r.to_string()), room_member: None, }, ); @@ -1033,7 +1155,6 @@ impl Widget for RoomScreen { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // If the room isn't loaded yet, we show the restore status label only. if !self.is_loaded { @@ -1041,7 +1162,8 @@ impl Widget for RoomScreen { // No room selected yet, nothing to show. return DrawStep::done(); }; - let mut restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); + let mut restore_status_view = + self.view.restore_status_view(cx, ids!(restore_status_view)); restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); return restore_status_view.draw(cx, scope); } @@ -1051,13 +1173,14 @@ impl Widget for RoomScreen { return DrawStep::done(); } - let room_screen_widget_uid = self.widget_uid(); while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // Here, we only need to handle drawing the portal list. let portal_list_ref = subview.as_portal_list(); let Some(mut list_ref) = portal_list_ref.borrow_mut() else { - error!("!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else"); + error!( + "!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else" + ); continue; }; let Some(tl_state) = self.tl_state.as_mut() else { @@ -1095,13 +1218,17 @@ impl Widget for RoomScreen { && msg_like_content.thread_root.is_some() { // Hide threaded replies from the main room timeline UI. - (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::both_drawn()) + ( + list.item(cx, item_id, id!(Empty)), + ItemDrawnStatus::both_drawn(), + ) } else { match &msg_like_content.kind { MsgLikeKind::Message(_) | MsgLikeKind::Sticker(_) | MsgLikeKind::Redacted => { - let prev_event = tl_idx.checked_sub(1).and_then(|i| tl_items.get(i)); + let prev_event = + tl_idx.checked_sub(1).and_then(|i| tl_items.get(i)); populate_message_view( cx, list, @@ -1119,26 +1246,30 @@ impl Widget for RoomScreen { item_drawn_status, room_screen_widget_uid, ) - }, + } // TODO: properly implement `Poll` as a regular Message-like timeline item. - MsgLikeKind::Poll(poll_state) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - poll_state, - item_drawn_status, - ), - MsgLikeKind::UnableToDecrypt(utd) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - utd, - item_drawn_status, - ), + MsgLikeKind::Poll(poll_state) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + poll_state, + item_drawn_status, + ) + } + MsgLikeKind::UnableToDecrypt(utd) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + utd, + item_drawn_status, + ) + } MsgLikeKind::Other(other) => populate_small_state_event( cx, list, @@ -1150,25 +1281,29 @@ impl Widget for RoomScreen { ), } } - }, - TimelineItemContent::MembershipChange(membership_change) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - membership_change, - item_drawn_status, - ), - TimelineItemContent::ProfileChange(profile_change) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - profile_change, - item_drawn_status, - ), + } + TimelineItemContent::MembershipChange(membership_change) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + membership_change, + item_drawn_status, + ) + } + TimelineItemContent::ProfileChange(profile_change) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + profile_change, + item_drawn_status, + ) + } TimelineItemContent::OtherState(other) => populate_small_state_event( cx, list, @@ -1180,10 +1315,11 @@ impl Widget for RoomScreen { ), unhandled => { let item = list.item(cx, item_id, id!(SmallStateEvent)); - item.label(cx, ids!(content)).set_text(cx, &format!("[Unsupported] {:?}", unhandled)); + item.label(cx, ids!(content)) + .set_text(cx, &format!("[Unsupported] {:?}", unhandled)); (item, ItemDrawnStatus::both_drawn()) } - } + }, TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(millis)) => { let item = list.item(cx, item_id, id!(DateDivider)); let text = unix_time_millis_to_datetime(*millis) @@ -1205,10 +1341,14 @@ impl Widget for RoomScreen { // Now that we've drawn the item, add its index to the set of drawn items. if item_new_draw_status.content_drawn { - tl_state.content_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); + tl_state + .content_drawn_since_last_update + .insert(tl_idx..tl_idx + 1); } if item_new_draw_status.profile_drawn { - tl_state.profile_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); + tl_state + .profile_drawn_since_last_update + .insert(tl_idx..tl_idx + 1); } item }; @@ -1218,7 +1358,10 @@ impl Widget for RoomScreen { // If the list is not filling the viewport, we need to back paginate the timeline // until we have enough events items to fill the viewport. if !tl_state.fully_paginated && !list.is_filling_viewport() { - log!("Automatically paginating timeline to fill viewport for room {:?}", self.room_name_id); + log!( + "Automatically paginating timeline to fill viewport for room {:?}", + self.room_name_id + ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -1243,7 +1386,9 @@ impl RoomScreen { let jump_to_bottom_button = self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)); let curr_first_id = portal_list.first_id(); let ui = self.widget_uid(); - let Some(tl) = self.tl_state.as_mut() else { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; let mut done_loading = false; let mut should_continue_backwards_pagination = false; @@ -1264,10 +1409,19 @@ impl RoomScreen { tl.items = initial_items; done_loading = true; } - TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { + TimelineUpdate::NewItems { + new_items, + changed_indices, + is_append, + clear_cache, + } => { if new_items.is_empty() { if !tl.items.is_empty() { - log!("process_timeline_updates(): timeline (had {} items) was cleared for room {}", tl.items.len(), tl.kind.room_id()); + log!( + "process_timeline_updates(): timeline (had {} items) was cleared for room {}", + tl.items.len(), + tl.kind.room_id() + ); // For now, we paginate a cleared timeline in order to be able to show something at least. // A proper solution would be what's described below, which would be to save a few event IDs // and then either focus on them (if we're not close to the end of the timeline) @@ -1301,9 +1455,12 @@ impl RoomScreen { if new_items.len() == tl.items.len() { // log!("process_timeline_updates(): no jump necessary for updated timeline of same length: {}", items.len()); - } - else if curr_first_id > new_items.len() { - log!("process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", curr_first_id, new_items.len()); + } else if curr_first_id > new_items.len() { + log!( + "process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", + curr_first_id, + new_items.len() + ); portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0); portal_list.set_tail_range(true); jump_to_bottom_button.update_visibility(cx, true); @@ -1312,19 +1469,28 @@ impl RoomScreen { // in the timeline viewport so that we can maintain the scroll position of that item, // which ensures that the timeline doesn't jump around unexpectedly and ruin the user's experience. else if let Some((curr_item_idx, new_item_idx, new_item_scroll, _event_id)) = - prior_items_changed.then(|| - find_new_item_matching_current_item(cx, portal_list, curr_first_id, &tl.items, &new_items) - ) - .flatten() + prior_items_changed + .then(|| { + find_new_item_matching_current_item( + cx, + portal_list, + curr_first_id, + &tl.items, + &new_items, + ) + }) + .flatten() { if curr_item_idx != new_item_idx { - log!("process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}"); + log!( + "process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}" + ); portal_list.set_first_id_and_scroll(new_item_idx, new_item_scroll); tl.prev_first_index = Some(new_item_idx); // Set scrolled_past_read_marker false when we jump to a new event tl.scrolled_past_read_marker = false; // Hide the tooltip when the timeline jumps, as a hover-out event won't occur. - cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); + cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); } } // @@ -1340,8 +1506,9 @@ impl RoomScreen { // because the matrix SDK doesn't currently support querying unread message counts for threads. if matches!(tl.kind, TimelineKind::MainRoom { .. }) { // Immediately show the unread badge with no count while we fetch the actual count in the background. - jump_to_bottom_button.show_unread_message_badge(cx, UnreadMessageCount::Unknown); - submit_async_request(MatrixRequest::GetNumberUnreadMessages{ + jump_to_bottom_button + .show_unread_message_badge(cx, UnreadMessageCount::Unknown); + submit_async_request(MatrixRequest::GetNumberUnreadMessages { timeline_kind: tl.kind.clone(), }); } @@ -1355,10 +1522,15 @@ impl RoomScreen { let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); let mut loading_pane_state = loading_pane.take_state(); if let LoadingPaneState::BackwardsPaginateUntilEvent { - events_paginated, target_event_id, .. - } = &mut loading_pane_state { + events_paginated, + target_event_id, + .. + } = &mut loading_pane_state + { *events_paginated += new_items.len().saturating_sub(tl.items.len()); - log!("While finding target event {target_event_id}, we have now loaded {events_paginated} messages..."); + log!( + "While finding target event {target_event_id}, we have now loaded {events_paginated} messages..." + ); // Here, we assume that we have not yet found the target event, // so we need to continue paginating backwards. // If the target event has already been found, it will be handled @@ -1375,8 +1547,10 @@ impl RoomScreen { tl.profile_drawn_since_last_update.clear(); tl.fully_paginated = false; } else { - tl.content_drawn_since_last_update.remove(changed_indices.clone()); - tl.profile_drawn_since_last_update.remove(changed_indices.clone()); + tl.content_drawn_since_last_update + .remove(changed_indices.clone()); + tl.profile_drawn_since_last_update + .remove(changed_indices.clone()); // log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update); } tl.items = new_items; @@ -1389,7 +1563,10 @@ impl RoomScreen { jump_to_bottom_button.show_unread_message_badge(cx, unread_messages_count); } } - TimelineUpdate::TargetEventFound { target_event_id, index } => { + TimelineUpdate::TargetEventFound { + target_event_id, + index, + } => { // log!("Target event found in room {}: {target_event_id}, index: {index}", tl.kind.room_id()); tl.request_sender.send_if_modified(|requests| { requests.retain(|r| &r.room_id != tl.kind.room_id()); @@ -1399,10 +1576,10 @@ impl RoomScreen { // sanity check: ensure the target event is in the timeline at the given `index`. let item = tl.items.get(index); - let is_valid = item.is_some_and(|item| + let is_valid = item.is_some_and(|item| { item.as_event() .is_some_and(|ev| ev.event_id() == Some(&target_event_id)) - ); + }); let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); // log!("TargetEventFound: is_valid? {is_valid}. room {}, event {target_event_id}, index {index} of {}\n --> item: {item:?}", tl.kind.room_id(), tl.items.len()); @@ -1421,19 +1598,24 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { - item_id: index - }; - } - else { + tl.message_highlight_animation_state = + MessageHighlightAnimationState::Pending { item_id: index }; + } else { // Here, the target event was not found in the current timeline, // or we found it previously but it is no longer in the timeline (or has moved), // which means we encountered an error and are unable to jump to the target event. - error!("Target event index {index} of {} is out of bounds for room {}", tl.items.len(), tl.kind.room_id()); + error!( + "Target event index {index} of {} is out of bounds for room {}", + tl.items.len(), + tl.kind.room_id() + ); // Show this error in the loading pane, which should already be open. - loading_pane.set_state(cx, LoadingPaneState::Error( - String::from("Unable to find related message; it may have been deleted.") - )); + loading_pane.set_state( + cx, + LoadingPaneState::Error(String::from( + "Unable to find related message; it may have been deleted.", + )), + ); } should_continue_backwards_pagination = false; @@ -1450,16 +1632,25 @@ impl RoomScreen { } } TimelineUpdate::PaginationError { error, direction } => { - error!("Pagination error ({direction}) in {:?}: {error:?}", self.room_name_id); + error!( + "Pagination error ({direction}) in {:?}: {error:?}", + self.room_name_id + ); let room_name = self.room_name_id.as_ref().map(|r| r.to_string()); enqueue_popup_notification( - utils::stringify_pagination_error(&error, room_name.as_deref().unwrap_or(UNNAMED_ROOM)), + utils::stringify_pagination_error( + &error, + room_name.as_deref().unwrap_or(UNNAMED_ROOM), + ), PopupKind::Error, Some(10.0), ); done_loading = true; } - TimelineUpdate::PaginationIdle { fully_paginated, direction } => { + TimelineUpdate::PaginationIdle { + fully_paginated, + direction, + } => { if direction == PaginationDirection::Backwards { // Don't set `done_loading` to `true` here, because we want to keep the top space visible // (with the "loading" message) until the corresponding `NewItems` update is received. @@ -1471,9 +1662,12 @@ impl RoomScreen { error!("Unexpected PaginationIdle update in the Forwards direction"); } } - TimelineUpdate::EventDetailsFetched {event_id, result } => { + TimelineUpdate::EventDetailsFetched { event_id, result } => { if let Err(_e) = result { - error!("Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", tl.kind.room_id()); + error!( + "Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", + tl.kind.room_id() + ); } // Here, to be most efficient, we could redraw only the updated event, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. @@ -1484,7 +1678,8 @@ impl RoomScreen { num_replies, latest_reply_preview_text, } => { - tl.pending_thread_summary_fetches.remove(&thread_root_event_id); + tl.pending_thread_summary_fetches + .remove(&thread_root_event_id); tl.fetched_thread_summaries.insert( thread_root_event_id.clone(), FetchedThreadSummary { @@ -1492,14 +1687,15 @@ impl RoomScreen { latest_reply_preview_text, }, ); - let event_id_matches_at_index = tl.items + let event_id_matches_at_index = tl + .items .get(timeline_item_index) .and_then(|item| item.as_event()) .and_then(|ev| ev.event_id()) .is_some_and(|id| id == thread_root_event_id); if event_id_matches_at_index { tl.content_drawn_since_last_update - .remove(timeline_item_index .. timeline_item_index + 1); + .remove(timeline_item_index..timeline_item_index + 1); } else { tl.content_drawn_since_last_update.clear(); } @@ -1512,9 +1708,12 @@ impl RoomScreen { TimelineUpdate::RoomMembersListFetched { members } => { // Store room members directly in TimelineUiState tl.room_members = Some(Arc::new(members)); - }, + } TimelineUpdate::MediaFetched(request) => { - log!("process_timeline_updates(): media fetched for room {}", tl.kind.room_id()); + log!( + "process_timeline_updates(): media fetched for room {}", + tl.kind.room_id() + ); // Set Image to image viewer modal if the media is not a thumbnail. if let (MediaFormat::File, media_source) = (request.format, request.source) { populate_matrix_image_modal(cx, media_source, &mut tl.media_cache); @@ -1522,26 +1721,39 @@ impl RoomScreen { // Here, to be most efficient, we could redraw only the media items in the timeline, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. } - TimelineUpdate::MessageEdited { timeline_event_item_id: timeline_event_id, result } => { - self.view.room_input_bar(cx, ids!(room_input_bar)) + TimelineUpdate::MessageEdited { + timeline_event_item_id: timeline_event_id, + result, + } => { + self.view + .room_input_bar(cx, ids!(room_input_bar)) .handle_edit_result(cx, timeline_event_id, result); } TimelineUpdate::PinResult { result, pin, .. } => { let (message, auto_dismissal_duration, kind) = match &result { Ok(true) => ( - format!("Successfully {} event.", if pin { "pinned" } else { "unpinned" }), + format!( + "Successfully {} event.", + if pin { "pinned" } else { "unpinned" } + ), Some(4.0), - PopupKind::Success + PopupKind::Success, ), Ok(false) => ( - format!("Message was already {}.", if pin { "pinned" } else { "unpinned" }), + format!( + "Message was already {}.", + if pin { "pinned" } else { "unpinned" } + ), Some(4.0), - PopupKind::Info + PopupKind::Info, ), Err(e) => ( - format!("Failed to {} event. Error: {e}", if pin { "pin" } else { "unpin" }), + format!( + "Failed to {} event. Error: {e}", + if pin { "pin" } else { "unpin" } + ), None, - PopupKind::Error + PopupKind::Error, ), }; enqueue_popup_notification(message, kind, auto_dismissal_duration); @@ -1565,7 +1777,8 @@ impl RoomScreen { } TimelineUpdate::UserPowerLevels(user_power_levels) => { tl.user_power = user_power_levels; - self.view.room_input_bar(cx, ids!(room_input_bar)) + self.view + .room_input_bar(cx, ids!(room_input_bar)) .update_user_power_levels(cx, user_power_levels); // Update the @room mention capability based on the user's power level cx.action(MentionableTextInputAction::PowerLevelsUpdated { @@ -1581,8 +1794,13 @@ impl RoomScreen { tl.latest_own_user_receipt = Some(receipt); } TimelineUpdate::Tombstoned(successor_room_details) => { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .update_tombstone_footer(cx, tl.kind.room_id(), Some(&successor_room_details)); + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .update_tombstone_footer( + cx, + tl.kind.room_id(), + Some(&successor_room_details), + ); tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} @@ -1613,7 +1831,6 @@ impl RoomScreen { } } - /// Handles a link being clicked in any child widgets of this RoomScreen. /// /// Returns `true` if the given `action` was handled as a link click. @@ -1657,7 +1874,11 @@ impl RoomScreen { true } MatrixId::Room(room_id) => { - if self.room_name_id.as_ref().is_some_and(|r| r.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|r| r.room_id() == room_id) + { enqueue_popup_notification( "You are already viewing that room.", PopupKind::Info, @@ -1665,7 +1886,9 @@ impl RoomScreen { ); return true; } - if let Some(room_name_id) = cx.get_global::().get_room_name(room_id) { + if let Some(room_name_id) = + cx.get_global::().get_room_name(room_id) + { cx.action(AppStateAction::NavigateToRoom { room_to_close: None, destination_room: BasicRoomDetails::Name(room_name_id), @@ -1699,8 +1922,7 @@ impl RoomScreen { let mut link_was_handled = false; if let Ok(matrix_to_uri) = MatrixToUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_to_uri.id(), matrix_to_uri.via()); - } - else if let Ok(matrix_uri) = MatrixUri::parse(&url) { + } else if let Ok(matrix_uri) = MatrixUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_uri.id(), matrix_uri.via()); } @@ -1716,8 +1938,13 @@ impl RoomScreen { } } true - } - else if let RobrixHtmlLinkAction::ClickedMatrixLink { url, matrix_id, via, .. } = action.as_widget_action().cast() { + } else if let RobrixHtmlLinkAction::ClickedMatrixLink { + url, + matrix_id, + via, + .. + } = action.as_widget_action().cast() + { let link_was_handled = handle_matrix_link(&matrix_id, &via); if !link_was_handled { log!("Opening URL \"{}\"", url); @@ -1731,8 +1958,7 @@ impl RoomScreen { } } true - } - else { + } else { false } } @@ -1748,8 +1974,13 @@ impl RoomScreen { let Some(media_source) = mxc_uri else { return; }; - let Some(tl_state) = self.tl_state.as_mut() else { return }; - let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) else { return }; + let Some(tl_state) = self.tl_state.as_mut() else { + return; + }; + let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) + else { + return; + }; let timestamp_millis = event_tl_item.timestamp(); let (image_name, image_file_size) = get_image_name_and_filesize(event_tl_item); @@ -1759,10 +1990,7 @@ impl RoomScreen { image_name, image_file_size, timestamp: unix_time_millis_to_datetime(timestamp_millis), - avatar_parameter: Some(( - tl_state.kind.clone(), - event_tl_item.clone(), - )), + avatar_parameter: Some((tl_state.kind.clone(), event_tl_item.clone())), }), ))); @@ -1783,13 +2011,15 @@ impl RoomScreen { details: &MessageDetails, ) -> Option<&'a EventTimelineItem> { let target_event_id = details.event_id()?; - if let Some(event) = items.get(details.item_id) + if let Some(event) = items + .get(details.item_id) .and_then(|item| item.as_event()) .filter(|ev| ev.event_id().is_some_and(|id| id == target_event_id)) { return Some(event); } - items.iter() + items + .iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .filter_map(|item| item.as_event()) @@ -1806,9 +2036,15 @@ impl RoomScreen { ) { let room_screen_widget_uid = self.widget_uid(); for action in actions { - match action.as_widget_action().widget_uid_eq(room_screen_widget_uid).cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast_ref() + { MessageAction::React { details, reaction } => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; submit_async_request(MatrixRequest::ToggleReaction { timeline_kind: tl.kind.clone(), timeline_event_id: details.timeline_event_id.clone(), @@ -1816,19 +2052,24 @@ impl RoomScreen { }); } MessageAction::Reply(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; - if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details).cloned() { + let Some(tl) = self.tl_state.as_ref() else { + return; + }; + if let Some(event_tl_item) = + Self::find_event_in_timeline(&tl.items, details).cloned() + { let replied_to_info = EmbeddedEvent::from_timeline_item(&event_tl_item); - self.view.room_input_bar(cx, ids!(room_input_bar)) + self.view + .room_input_bar(cx, ids!(room_input_bar)) .show_replying_to(cx, (event_tl_item, replied_to_info), &tl.kind); - } - else { + } else { enqueue_popup_notification( "Could not find message in timeline to reply to. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", + error!( + "MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -1836,22 +2077,21 @@ impl RoomScreen { } } MessageAction::Edit(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane( - cx, - event_tl_item.clone(), - tl.kind.clone(), - ); - } - else { + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane(cx, event_tl_item.clone(), tl.kind.clone()); + } else { enqueue_popup_notification( "Could not find message in timeline to edit. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", + error!( + "MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -1859,21 +2099,20 @@ impl RoomScreen { } } MessageAction::EditLatest => { - let Some(tl) = self.tl_state.as_ref() else { return }; - if let Some(latest_sent_msg) = tl.items + let Some(tl) = self.tl_state.as_ref() else { + return; + }; + if let Some(latest_sent_msg) = tl + .items .iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .find_map(|item| item.as_event().filter(|ev| ev.is_editable()).cloned()) { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane( - cx, - latest_sent_msg, - tl.kind.clone(), - ); - } - else { + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane(cx, latest_sent_msg, tl.kind.clone()); + } else { enqueue_popup_notification( "No recent message available to edit. Please manually select a message to edit.", PopupKind::Warning, @@ -1882,7 +2121,9 @@ impl RoomScreen { } } MessageAction::Pin(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -1898,7 +2139,9 @@ impl RoomScreen { } } MessageAction::Unpin(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -1914,17 +2157,19 @@ impl RoomScreen { } } MessageAction::CopyText(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { cx.copy_to_clipboard(&plaintext_body_of_timeline_item(event_tl_item)); - } - else { + } else { enqueue_popup_notification( "Could not find message in timeline to copy text from. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", + error!( + "MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1932,22 +2177,49 @@ impl RoomScreen { } } MessageAction::CopyHtml(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; // The logic for getting the formatted body of a message is the same // as the logic used in `populate_message_view()`. let mut success = false; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { if let Some(message) = event_tl_item.content().as_message() { match message.msgtype() { - MessageType::Text(TextMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Notice(NoticeMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Emote(EmoteMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Image(ImageMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::File(FileMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Audio(AudioMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Video(VideoMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::VerificationRequest(KeyVerificationRequestEventContent { formatted: Some(FormattedBody { body, .. }), .. }) => - { + MessageType::Text(TextMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Notice(NoticeMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Emote(EmoteMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Image(ImageMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::File(FileMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Audio(AudioMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Video(VideoMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::VerificationRequest( + KeyVerificationRequestEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }, + ) => { cx.copy_to_clipboard(body); success = true; } @@ -1961,7 +2233,8 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", + error!( + "MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1969,7 +2242,9 @@ impl RoomScreen { } } MessageAction::CopyLink(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { let matrix_to_uri = tl.kind.room_id().matrix_to_event_uri(event_id.clone()); cx.copy_to_clipboard(&matrix_to_uri.to_string()); @@ -1979,7 +2254,8 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", + error!( + "MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1987,8 +2263,11 @@ impl RoomScreen { } } MessageAction::ViewSource(details) => { - let Some(tl) = self.tl_state.as_ref() else { continue }; - let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) else { + let Some(tl) = self.tl_state.as_ref() else { + continue; + }; + let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) + else { enqueue_popup_notification( "Could not find message in timeline to view source.", PopupKind::Error, @@ -2012,7 +2291,9 @@ impl RoomScreen { } MessageAction::JumpToRelated(details) => { let Some(related_event_id) = details.related_event_id.as_ref() else { - error!("BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}"); + error!( + "BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}" + ); enqueue_popup_notification( "Could not find related message or event in timeline.", PopupKind::Error, @@ -2025,25 +2306,21 @@ impl RoomScreen { related_event_id, Some(details.item_id), portal_list, - loading_pane + loading_pane, ); } MessageAction::JumpToEvent(event_id) => { - self.jump_to_event( - cx, - event_id, - None, - portal_list, - loading_pane - ); + self.jump_to_event(cx, event_id, None, portal_list, loading_pane); } MessageAction::OpenThread(thread_root_event_id) => { let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { - error!("### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!"); - continue + error!( + "### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!" + ); + continue; }; cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, RoomsListAction::Selected(SelectedRoom::Thread { room_name_id, thread_root_event_id: thread_root_event_id.clone(), @@ -2051,13 +2328,17 @@ impl RoomScreen { ); } MessageAction::Redact { details, reason } => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; let timeline_event_id = details.timeline_event_id.clone(); let timeline_kind = tl.kind.clone(); let reason = reason.clone(); let content = ConfirmationModalContent { title_text: "Delete Message".into(), - body_text: "Are you sure you want to delete this message? This cannot be undone.".into(), + body_text: + "Are you sure you want to delete this message? This cannot be undone." + .into(), accept_button_text: Some("Delete".into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::RedactMessage { @@ -2075,14 +2356,14 @@ impl RoomScreen { // } // This is handled within the Message widget itself. - MessageAction::HighlightMessage(..) => { } + MessageAction::HighlightMessage(..) => {} // This is handled by the top-level App itself. - MessageAction::OpenMessageContextMenu { .. } => { } + MessageAction::OpenMessageContextMenu { .. } => {} // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarOpen { .. } => { } + MessageAction::ActionBarOpen { .. } => {} // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarClose => { } - MessageAction::None => { } + MessageAction::ActionBarClose => {} + MessageAction::None => {} } } } @@ -2100,14 +2381,17 @@ impl RoomScreen { portal_list: &PortalListRef, loading_pane: &LoadingPaneRef, ) { - let Some(tl) = self.tl_state.as_mut() else { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; let max_tl_idx = max_tl_idx.unwrap_or_else(|| tl.items.len()); // Attempt to find the index of replied-to message in the timeline. // Start from the current item's index (`tl_idx`) and search backwards, // since we know the related message must come before the current item. let mut num_items_searched = 0; - let related_msg_tl_index = tl.items + let related_msg_tl_index = tl + .items .focus() .narrow(..max_tl_idx) .into_iter() @@ -2130,11 +2414,13 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { - item_id: index - }; + tl.message_highlight_animation_state = + MessageHighlightAnimationState::Pending { item_id: index }; } else { - log!("The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", tl.kind.room_id()); + log!( + "The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", + tl.kind.room_id() + ); // Here, we set the state of the loading pane and display it to the user. // The main logic will be handled in `process_timeline_updates()`, which is the only // place where we can receive updates to the timeline from the background tasks. @@ -2187,7 +2473,9 @@ impl RoomScreen { /// Invoke this when this timeline is being shown, /// e.g., when the user navigates to this timeline. fn show_timeline(&mut self, cx: &mut Cx) { - let kind = self.timeline_kind.clone() + let kind = self + .timeline_kind + .clone() .expect("BUG: Timeline::show_timeline(): no timeline_kind was set."); let room_id = kind.room_id().clone(); @@ -2204,8 +2492,10 @@ impl RoomScreen { return; } if !self.is_loaded && self.all_rooms_loaded { - panic!("BUG: timeline {kind} is not loaded, but its RoomScreen \ - was not waiting for its timeline to be loaded either."); + panic!( + "BUG: timeline {kind} is not loaded, but its RoomScreen \ + was not waiting for its timeline to be loaded either." + ); } return; }; @@ -2278,14 +2568,19 @@ impl RoomScreen { self.is_loaded = is_loaded_now; } - self.view.restore_status_view(cx, ids!(restore_status_view)).set_visible(cx, !self.is_loaded); + self.view + .restore_status_view(cx, ids!(restore_status_view)) + .set_visible(cx, !self.is_loaded); // Kick off a back pagination request if it's the first time loading this room, // because we want to show the user some messages as soon as possible // when they first open the room, and there might not be any messages yet. if is_first_time_being_loaded { if !tl_state.fully_paginated { - log!("Sending a first-time backwards pagination request for {}", tl_state.kind); + log!( + "Sending a first-time backwards pagination request for {}", + tl_state.kind + ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -2354,7 +2649,9 @@ impl RoomScreen { /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown. fn hide_timeline(&mut self) { - let Some(timeline_kind) = self.timeline_kind.clone() else { return }; + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; self.save_state(); @@ -2417,7 +2714,12 @@ impl RoomScreen { // 1. Restore the position of the timeline. let portal_list = self.portal_list(cx, ids!(timeline.list)); if let Some((first_index, scroll_from_first_id)) = first_index_and_scroll { - log!("Restoring state for room {:?}: first_id: {:?}, scroll: {}", self.room_name_id, first_index, scroll_from_first_id); + log!( + "Restoring state for room {:?}: first_id: {:?}, scroll: {}", + self.room_name_id, + first_index, + scroll_from_first_id + ); portal_list.set_first_id_and_scroll(*first_index, *scroll_from_first_id); portal_list.set_tail_range(false); } else { @@ -2463,7 +2765,11 @@ impl RoomScreen { // If this timeline is already displayed, we don't need to do anything major, // but we do need update the `room_name_id` in case it has changed, or it has been cleared. - if self.timeline_kind.as_ref().is_some_and(|kind| kind == &timeline_kind) { + if self + .timeline_kind + .as_ref() + .is_some_and(|kind| kind == &timeline_kind) + { self.room_name_id = Some(room_name_id.clone()); return; } @@ -2498,7 +2804,9 @@ impl RoomScreen { return; } let first_index = portal_list.first_id(); - let Some(tl_state) = self.tl_state.as_mut() else { return }; + let Some(tl_state) = self.tl_state.as_mut() else { + return; + }; if let Some(ref mut index) = tl_state.prev_first_index { // to detect change of scroll when scroll ends @@ -2509,7 +2817,7 @@ impl RoomScreen { .items .get(std::cmp::min( first_index + portal_list.visible_items(), - tl_state.items.len().saturating_sub(1) + tl_state.items.len().saturating_sub(1), )) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) @@ -2529,17 +2837,20 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } else { - if let Some(own_user_receipt_timestamp) = &tl_state.latest_own_user_receipt.clone() - .and_then(|receipt| receipt.ts) { + if let Some(own_user_receipt_timestamp) = &tl_state + .latest_own_user_receipt + .clone() + .and_then(|receipt| receipt.ts) + { let Some((_first_event_id, first_timestamp)) = tl_state .items .get(first_index) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) - else { - *index = first_index; - return; - }; + else { + *index = first_index; + return; + }; if own_user_receipt_timestamp >= &first_timestamp && own_user_receipt_timestamp <= &last_timestamp { @@ -2550,7 +2861,6 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } - } } } @@ -2569,14 +2879,22 @@ impl RoomScreen { actions: &ActionsBuf, portal_list: &PortalListRef, ) { - let Some(tl) = self.tl_state.as_mut() else { return }; - if tl.fully_paginated { return }; - if !portal_list.scrolled(actions) { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; + if tl.fully_paginated { + return; + }; + if !portal_list.scrolled(actions) { + return; + }; let first_index = portal_list.first_id(); if first_index == 0 && tl.last_scrolled_index > 0 { - log!("Scrolled up from item {} --> 0, sending back pagination request for room {}", - tl.last_scrolled_index, tl.kind, + log!( + "Scrolled up from item {} --> 0, sending back pagination request for room {}", + tl.last_scrolled_index, + tl.kind, ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl.kind.clone(), @@ -2596,7 +2914,9 @@ impl RoomScreenRef { room_name_id: &RoomNameId, thread_root_event_id: Option, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_displayed_room(cx, room_name_id, thread_root_event_id); } } @@ -2611,7 +2931,6 @@ pub struct RoomScreenProps { pub room_avatar_url: Option, } - /// Actions for the room screen's tooltip. #[derive(Clone, Debug, Default)] pub enum RoomScreenTooltipActions { @@ -2710,9 +3029,7 @@ pub enum TimelineUpdate { /// includes a complete list of room members that can be shared across components. /// This is different from RoomMembersSynced which only indicates members were fetched /// but doesn't provide the actual data. - RoomMembersListFetched { - members: Vec, - }, + RoomMembersListFetched { members: Vec }, /// A notice with an option of Media Request Parameters that one or more requested media items (images, videos, etc.) /// that should be displayed in this timeline have now been fetched and are available. MediaFetched(MediaRequestParameters), @@ -2744,7 +3061,7 @@ thread_local! { /// The global set of all timeline states, one entry per room. /// /// This is only useful when accessed from the main UI thread. - static TIMELINE_STATES: RefCell> = + static TIMELINE_STATES: RefCell> = RefCell::new(HashMap::new()); } @@ -2860,7 +3177,9 @@ struct TimelineUiState { #[derive(Default, Debug)] enum MessageHighlightAnimationState { - Pending { item_id: usize }, + Pending { + item_id: usize, + }, #[default] Off, } @@ -2897,9 +3216,8 @@ fn find_new_item_matching_current_item( ) -> Option<(usize, usize, f64, OwnedEventId)> { let mut curr_item_focus = curr_items.focus(); let mut idx_curr = starting_at_curr_idx; - let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = Vec::with_capacity( - portal_list.visible_items() - ); + let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = + Vec::with_capacity(portal_list.visible_items()); // Find all items with real event IDs that are currently visible in the portal list. // TODO: if this is slow, we could limit it to 3-5 events at the most. @@ -2928,7 +3246,9 @@ fn find_new_item_matching_current_item( // some may be zeroed-out, so we need to account for that possibility by only // using events that have a real non-zero area if let Some(pos_offset) = portal_list.position_of_item(cx, *idx_curr) { - log!("Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}"); + log!( + "Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}" + ); return Some((*idx_curr, idx_new, pos_offset, event_id.to_owned())); } } @@ -3002,7 +3322,8 @@ fn populate_message_view( TimelineItemContent::MsgLike(_msg_like_content) => { let prev_msg_sender = prev_event_tl_item.sender(); prev_msg_sender == event_tl_item.sender() - && ts_millis.0 + && ts_millis + .0 .checked_sub(prev_event_tl_item.timestamp().0) .is_some_and(|d| d < uint!(600000)) // 10 mins in millis } @@ -3019,8 +3340,12 @@ fn populate_message_view( let (item, used_cached_item) = match &msg_like_content.kind { MsgLikeKind::Message(msg) => { match msg.msgtype() { - MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Text(TextMessageEventContent { + body, formatted, .. + }) => { + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3048,9 +3373,13 @@ fn populate_message_view( } // A notice message is just a message sent by an automated bot, // so we treat it just like a message but use a different font color. - MessageType::Notice(NoticeMessageEventContent{body, formatted, ..}) => { + MessageType::Notice(NoticeMessageEventContent { + body, formatted, .. + }) => { is_notice = true; - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3060,7 +3389,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = + item.html_or_plaintext(cx, ids!(content.message)); // Apply gray color to all text styles for notice messages. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -3090,7 +3420,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = + item.html_or_plaintext(cx, ids!(content.message)); // Apply red color to all text styles for server notices. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -3105,10 +3436,12 @@ fn populate_message_view( "Server notice: {}\n\nNotice type:: {}{}{}", sn.body, sn.server_notice_type.as_str(), - sn.limit_type.as_ref() + sn.limit_type + .as_ref() .map(|l| format!("\nLimit type: {}", l.as_str())) .unwrap_or_default(), - sn.admin_contact.as_ref() + sn.admin_contact + .as_ref() .map(|c| format!("\nAdmin contact: {}", c)) .unwrap_or_default(), ); @@ -3131,8 +3464,12 @@ fn populate_message_view( } // An emote is just like a message but is prepended with the user's name // to indicate that it's an "action" that the user is performing. - MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Emote(EmoteMessageEventContent { + body, formatted, .. + }) => { + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3143,14 +3480,16 @@ fn populate_message_view( (item, true) } else { // Draw the profile up front here because we need the username for the emote body. - let (username, profile_drawn) = item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ); + let (username, profile_drawn) = item + .avatar(cx, ids!(profile.avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ); // Prepend a "* " to the emote body, as suggested by the Matrix spec. let (body, formatted) = if let Some(fb) = formatted.as_ref() { @@ -3159,7 +3498,7 @@ fn populate_message_view( Some(FormattedBody { format: fb.format.clone(), body: format!("* {} {}", &username, &fb.body), - }) + }), ) } else { (Cow::from(format!("* {} {}", &username, body)), None) @@ -3183,7 +3522,9 @@ fn populate_message_view( } } MessageType::Image(image) => { - has_html_body = image.formatted.as_ref() + has_html_body = image + .formatted + .as_ref() .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedImageMessage) @@ -3221,17 +3562,17 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - let is_location_fully_drawn = populate_location_message_content( - cx, - &html_or_plaintext_ref, - location, - ); + let is_location_fully_drawn = + populate_location_message_content(cx, &html_or_plaintext_ref, location); new_drawn_status.content_drawn = is_location_fully_drawn; (item, false) } } MessageType::File(file_content) => { - has_html_body = file_content.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = file_content + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3243,16 +3584,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_file_message_content( - cx, - &html_or_plaintext_ref, - file_content, - ); + new_drawn_status.content_drawn = + populate_file_message_content(cx, &html_or_plaintext_ref, file_content); (item, false) } } MessageType::Audio(audio) => { - has_html_body = audio.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = audio + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3264,16 +3605,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_audio_message_content( - cx, - &html_or_plaintext_ref, - audio, - ); + new_drawn_status.content_drawn = + populate_audio_message_content(cx, &html_or_plaintext_ref, audio); (item, false) } } MessageType::Video(video) => { - has_html_body = video.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = video + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3285,16 +3626,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_video_message_content( - cx, - &html_or_plaintext_ref, - video, - ); + new_drawn_status.content_drawn = + populate_video_message_content(cx, &html_or_plaintext_ref, video); (item, false) } } MessageType::VerificationRequest(verification) => { - has_html_body = verification.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = verification + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = id!(Message); let (item, existed) = list.item_with_existed(cx, item_id, template); if existed && item_drawn_status.content_drawn { @@ -3306,7 +3647,8 @@ fn populate_message_view( body: format!( "Sent a verification request to {}.
(Supported methods: {})
", verification.to, - verification.methods + verification + .methods .iter() .map(|m| m.as_str()) .collect::>() @@ -3336,10 +3678,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)).set_text( - cx, - &format!("[Unsupported {:?}]", msg_like_content.kind), - ); + item.label(cx, ids!(content.message)) + .set_text(cx, &format!("[Unsupported {:?}]", msg_like_content.kind)); new_drawn_status.content_drawn = true; (item, false) } @@ -3349,7 +3689,9 @@ fn populate_message_view( // Handle sticker messages that are static images. MsgLikeKind::Sticker(sticker) => { has_html_body = false; - let StickerEventContent { body, info, source, .. } = sticker.content(); + let StickerEventContent { + body, info, source, .. + } = sticker.content(); let template = if use_compact_view { id!(CondensedImageMessage) @@ -3378,7 +3720,7 @@ fn populate_message_view( (item, true) } } - } + } // Handle messages that have been redacted (deleted). MsgLikeKind::Redacted => { has_html_body = false; @@ -3417,10 +3759,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)).set_text( - cx, - &format!("[Unsupported {:?}] ", other), - ); + item.label(cx, ids!(content.message)) + .set_text(cx, &format!("[Unsupported {:?}] ", other)); new_drawn_status.content_drawn = true; (item, false) } @@ -3432,13 +3772,14 @@ fn populate_message_view( // If we didn't use a cached item, we need to draw all other message content: // the reactions, the read receipts avatar row, the reply preview. if !used_cached_item { - item.reaction_list(cx, ids!(content.reaction_list)).set_list( - cx, - event_tl_item.content().reactions(), - timeline_kind.clone(), - timeline_event_id.clone(), - item_id, - ); + item.reaction_list(cx, ids!(content.reaction_list)) + .set_list( + cx, + event_tl_item.content().reactions(), + timeline_kind.clone(), + timeline_event_id.clone(), + item_id, + ); populate_read_receipts(&item, cx, timeline_kind, event_tl_item); let is_reply_fully_drawn = draw_replied_to_message( cx, @@ -3465,17 +3806,21 @@ fn populate_message_view( new_drawn_status.content_drawn &= is_thread_summary_fully_drawn; } - // We must always re-set the message details, even when re-using a cached portallist item, // because the item type might be the same but for a different message entirely. let message_details = MessageDetails { thread_root_event_id: msg_like_content.thread_root.clone().or_else(|| { - msg_like_content.thread_summary.as_ref() + msg_like_content + .thread_summary + .as_ref() .and_then(|_| event_tl_item.event_id().map(|id| id.to_owned())) }), timeline_event_id, item_id, - related_event_id: msg_like_content.in_reply_to.as_ref().map(|r| r.event_id.clone()), + related_event_id: msg_like_content + .in_reply_to + .as_ref() + .map(|r| r.event_id.clone()), room_screen_widget_uid, abilities: MessageAbilities::from_user_power_and_event( user_power_levels, @@ -3488,7 +3833,6 @@ fn populate_message_view( }; item.as_message().set_data(message_details); - // If `used_cached_item` is false, we should always redraw the profile, even if profile_drawn is true. let skip_draw_profile = use_compact_view || (used_cached_item && item_drawn_status.profile_drawn); @@ -3499,17 +3843,20 @@ fn populate_message_view( // log!("\t --> populate_message_view(): DRAWING profile draw for item_id: {item_id}"); let mut username_label = item.label(cx, ids!(content.username)); - if !is_server_notice { // the normal case - let (username, profile_drawn) = set_username_and_get_avatar_retval.unwrap_or_else(|| - item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ) - ); + if !is_server_notice { + // the normal case + let (username, profile_drawn) = + set_username_and_get_avatar_retval.unwrap_or_else(|| { + item.avatar(cx, ids!(profile.avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ) + }); if is_notice { script_apply_eval!(cx, username_label, { draw_text +: { @@ -3519,8 +3866,7 @@ fn populate_message_view( } username_label.set_text(cx, &username); new_drawn_status.profile_drawn = profile_drawn; - } - else { + } else { // Server notices are drawn with a red color avatar background and username. let avatar = item.avatar(cx, ids!(profile.avatar)); avatar.show_text(cx, Some(COLOR_FG_DANGER_RED), None, "⚠"); @@ -3541,33 +3887,46 @@ fn populate_message_view( // Set the timestamp. if let Some(dt) = unix_time_millis_to_datetime(ts_millis) { - item.timestamp(cx, ids!(profile.timestamp)).set_date_time(cx, dt); + item.timestamp(cx, ids!(profile.timestamp)) + .set_date_time(cx, dt); } // Set the "edited" indicator if this message was edited. if msg_like_content.as_message().is_some_and(|m| m.is_edited()) { - item.edited_indicator(cx, ids!(profile.edited_indicator)).set_latest_edit( - cx, - event_tl_item, - ); + item.edited_indicator(cx, ids!(profile.edited_indicator)) + .set_latest_edit(cx, event_tl_item); } - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { use matrix_sdk::ruma::serde::Base64; - use crate::tsp::{self, tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}}; + use crate::tsp::{ + self, + tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}, + }; - if let Some(mut tsp_sig) = event_tl_item.latest_json() + if let Some(mut tsp_sig) = event_tl_item + .latest_json() .and_then(|raw| raw.get_field::("content").ok()) .flatten() .and_then(|content_obj| content_obj.get("org.robius.tsp_signature").cloned()) .and_then(|tsp_sig_value| serde_json::from_value::(tsp_sig_value).ok()) .map(|b64| b64.into_inner()) { - log!("Found event {:?} with TSP signature.", event_tl_item.event_id()); - let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref().lock().unwrap() + log!( + "Found event {:?} with TSP signature.", + event_tl_item.event_id() + ); + let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref() + .lock() + .unwrap() .get_verified_vid_for(event_tl_item.sender()) { - log!("Found verified VID for sender {}: \"{}\"", event_tl_item.sender(), sender_vid.identifier()); + log!( + "Found verified VID for sender {}: \"{}\"", + event_tl_item.sender(), + sender_vid.identifier() + ); tsp_sdk::crypto::verify(&*sender_vid, &mut tsp_sig).map_or( TspSignState::WrongSignature, |(msg, msg_type)| { @@ -3579,7 +3938,11 @@ fn populate_message_view( TspSignState::Unknown }; - log!("TSP signature state for event {:?} is {:?}", event_tl_item.event_id(), tsp_sign_state); + log!( + "TSP signature state for event {:?} is {:?}", + event_tl_item.event_id(), + tsp_sign_state + ); item.tsp_sign_indicator(cx, ids!(profile.tsp_sign_indicator)) .show_with_state(cx, tsp_sign_state); } @@ -3602,7 +3965,8 @@ fn populate_text_message_content( ) -> bool { // The message was HTML-formatted rich text. let mut links = Vec::new(); - if let Some(fb) = formatted_body.as_ref() + if let Some(fb) = formatted_body + .as_ref() .and_then(|fb| (fb.format == MessageFormat::Html).then_some(fb)) { let linkified_html = utils::linkify_get_urls( @@ -3622,7 +3986,7 @@ fn populate_text_message_content( }; // Populate link previews if all required parameters are provided - if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = + if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = (link_preview_ref, media_cache, link_preview_cache) { link_preview_ref.populate_below_message( @@ -3650,7 +4014,8 @@ fn populate_image_message_content( ) -> bool { // We don't use thumbnails, as their resolution is too low to be visually useful. // We also don't trust the provided mimetype, as it can be incorrect. - let (mimetype, _width, _height) = image_info_source.as_ref() + let (mimetype, _width, _height) = image_info_source + .as_ref() .map(|info| (info.mimetype.as_deref(), info.width, info.height)) .unwrap_or_default(); @@ -3658,10 +4023,7 @@ fn populate_image_message_content( // then show a message about it being unsupported (e.g., for animated gifs). if let Some(mime) = mimetype.as_ref() { if ImageFormat::from_mimetype(mime).is_none() { - text_or_image_ref.show_text( - cx, - format!("{body}\n\nUnsupported type {mime:?}"), - ); + text_or_image_ref.show_text(cx, format!("{body}\n\nUnsupported type {mime:?}")); return true; // consider this as fully drawn } } @@ -3670,102 +4032,132 @@ fn populate_image_message_content( // A closure that fetches and shows the image from the given `mxc_uri`, // marking it as fully drawn if the image was available. - let mut fetch_and_show_image_uri = |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { - match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { - (MediaCacheEntry::Loaded(data), _media_format) => { - let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)),|cx, img| { - utils::load_png_or_jpg(&img, cx, &data) - .map(|()| img.size_in_pixels(cx).unwrap_or_default()) - }); - if let Err(e) = show_image_result { - let err_str = format!("{body}\n\nFailed to display image: {e:?}"); - error!("{err_str}"); - text_or_image_ref.show_text(cx, &err_str); - } - - // We're done drawing the image, so mark it as fully drawn. - fully_drawn = true; - } - (MediaCacheEntry::Requested, _media_format) => { - // If the image is being fetched, we try to show its blurhash. - if let (Some(ref blurhash), Some(width), Some(height)) = (image_info.blurhash.clone(), image_info.width, image_info.height) { - let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)), |cx, img| { - let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) else { - return Err(image_cache::ImageError::EmptyData) - }; - let (width, height): (u32, u32) = (width, height); - if width == 0 || height == 0 { - warning!("Image had an invalid aspect ratio (width or height of 0)."); - return Err(image_cache::ImageError::EmptyData); - } - let aspect_ratio: f32 = width as f32 / height as f32; - // Cap the blurhash to a max size of 500 pixels in each dimension - // because the `blurhash::decode()` function can be rather expensive. - let (mut capped_width, mut capped_height) = (width, height); - if capped_height > BLURHASH_IMAGE_MAX_SIZE { - capped_height = BLURHASH_IMAGE_MAX_SIZE; - capped_width = (capped_height as f32 * aspect_ratio).floor() as u32; - } - if capped_width > BLURHASH_IMAGE_MAX_SIZE { - capped_width = BLURHASH_IMAGE_MAX_SIZE; - capped_height = (capped_width as f32 / aspect_ratio).floor() as u32; - } - - match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { - Ok(data) => { - ImageBuffer::new(&data, capped_width as usize, capped_height as usize).map(|img_buff| { - let texture = Some(img_buff.into_new_texture(cx)); - img.set_texture(cx, texture); - img.size_in_pixels(cx).unwrap_or_default() - }) - } - Err(e) => { - error!("Failed to decode blurhash {e:?}"); - Err(image_cache::ImageError::EmptyData) - } - } - }); + let mut fetch_and_show_image_uri = + |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { + match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { + (MediaCacheEntry::Loaded(data), _media_format) => { + let show_image_result = text_or_image_ref.show_image( + cx, + Some(MediaSource::Plain(mxc_uri)), + |cx, img| { + utils::load_png_or_jpg(&img, cx, &data) + .map(|()| img.size_in_pixels(cx).unwrap_or_default()) + }, + ); if let Err(e) = show_image_result { let err_str = format!("{body}\n\nFailed to display image: {e:?}"); error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } + + // We're done drawing the image, so mark it as fully drawn. + fully_drawn = true; } - fully_drawn = false; - } - (MediaCacheEntry::Failed(_status_code), _media_format) => { - if text_or_image_ref.view(cx, ids!(default_image_view)).visible() { + (MediaCacheEntry::Requested, _media_format) => { + // If the image is being fetched, we try to show its blurhash. + if let (Some(ref blurhash), Some(width), Some(height)) = ( + image_info.blurhash.clone(), + image_info.width, + image_info.height, + ) { + let show_image_result = text_or_image_ref.show_image( + cx, + Some(MediaSource::Plain(mxc_uri)), + |cx, img| { + let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) + else { + return Err(image_cache::ImageError::EmptyData); + }; + let (width, height): (u32, u32) = (width, height); + if width == 0 || height == 0 { + warning!( + "Image had an invalid aspect ratio (width or height of 0)." + ); + return Err(image_cache::ImageError::EmptyData); + } + let aspect_ratio: f32 = width as f32 / height as f32; + // Cap the blurhash to a max size of 500 pixels in each dimension + // because the `blurhash::decode()` function can be rather expensive. + let (mut capped_width, mut capped_height) = (width, height); + if capped_height > BLURHASH_IMAGE_MAX_SIZE { + capped_height = BLURHASH_IMAGE_MAX_SIZE; + capped_width = + (capped_height as f32 * aspect_ratio).floor() as u32; + } + if capped_width > BLURHASH_IMAGE_MAX_SIZE { + capped_width = BLURHASH_IMAGE_MAX_SIZE; + capped_height = + (capped_width as f32 / aspect_ratio).floor() as u32; + } + + match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { + Ok(data) => ImageBuffer::new( + &data, + capped_width as usize, + capped_height as usize, + ) + .map(|img_buff| { + let texture = Some(img_buff.into_new_texture(cx)); + img.set_texture(cx, texture); + img.size_in_pixels(cx).unwrap_or_default() + }), + Err(e) => { + error!("Failed to decode blurhash {e:?}"); + Err(image_cache::ImageError::EmptyData) + } + } + }, + ); + if let Err(e) = show_image_result { + let err_str = format!("{body}\n\nFailed to display image: {e:?}"); + error!("{err_str}"); + text_or_image_ref.show_text(cx, &err_str); + } + } + fully_drawn = false; + } + (MediaCacheEntry::Failed(_status_code), _media_format) => { + if text_or_image_ref + .view(cx, ids!(default_image_view)) + .visible() + { + fully_drawn = true; + return; + } + text_or_image_ref.show_text( + cx, + format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri), + ); + // For now, we consider this as being "complete". In the future, we could support + // retrying to fetch thumbnail of the image on a user click/tap. fully_drawn = true; - return; } - text_or_image_ref - .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri)); - // For now, we consider this as being "complete". In the future, we could support - // retrying to fetch thumbnail of the image on a user click/tap. - fully_drawn = true; } - } - }; + }; - let mut fetch_and_show_media_source = |cx: &mut Cx, media_source: MediaSource, image_info: Box| { - match media_source { - MediaSource::Encrypted(encrypted) => { - // We consider this as "fully drawn" since we don't yet support encryption. - text_or_image_ref.show_text( - cx, - format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) - ); - }, - MediaSource::Plain(mxc_uri) => { - fetch_and_show_image_uri(cx, mxc_uri, image_info) + let mut fetch_and_show_media_source = + |cx: &mut Cx, media_source: MediaSource, image_info: Box| { + match media_source { + MediaSource::Encrypted(encrypted) => { + // We consider this as "fully drawn" since we don't yet support encryption. + text_or_image_ref.show_text( + cx, + format!( + "{body}\n\n[TODO] fetch encrypted image at {:?}", + encrypted.url + ), + ); + } + MediaSource::Plain(mxc_uri) => fetch_and_show_image_uri(cx, mxc_uri, image_info), } - } - }; + }; match image_info_source { Some(image_info) => { // Use the provided thumbnail URI if it exists; otherwise use the original URI. - let media_source = image_info.thumbnail_source.clone() + let media_source = image_info + .thumbnail_source + .clone() .unwrap_or(original_source); fetch_and_show_media_source(cx, media_source, image_info); } @@ -3778,7 +4170,6 @@ fn populate_image_message_content( fully_drawn } - /// Draws a file message's content into the given `message_content_widget`. /// /// Returns whether the file message content was fully drawn. @@ -3795,7 +4186,8 @@ fn populate_file_message_content( .and_then(|info| info.size) .map(|bytes| format!(" ({})", ByteSize::b(bytes.into()))) .unwrap_or_default(); - let caption = file_content.formatted_caption() + let caption = file_content + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| file_content.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3822,20 +4214,23 @@ fn populate_audio_message_content( let (duration, mime, size) = audio .info .as_ref() - .map(|info| ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - )) + .map(|info| { + ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + ) + }) .unwrap_or_default(); - let caption = audio.formatted_caption() + let caption = audio + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| audio.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3849,7 +4244,6 @@ fn populate_audio_message_content( true } - /// Draws a video message's content into the given `message_content_widget`. /// /// Returns whether the video message content was fully drawn. @@ -3863,23 +4257,26 @@ fn populate_video_message_content( let (duration, mime, size, dimensions) = video .info .as_ref() - .map(|info| ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - info.width.and_then(|width| - info.height.map(|height| format!(" {width}x{height},")) - ).unwrap_or_default(), - )) + .map(|info| { + ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + info.width + .and_then(|width| info.height.map(|height| format!(" {width}x{height},"))) + .unwrap_or_default(), + ) + }) .unwrap_or_default(); - let caption = video.formatted_caption() + let caption = video + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| video.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3893,8 +4290,6 @@ fn populate_video_message_content( true } - - /// Draws the given location message's content into the `message_content_widget`. /// /// Returns whether the location message content was fully drawn. @@ -3903,8 +4298,9 @@ fn populate_location_message_content( message_content_widget: &HtmlOrPlaintextRef, location: &LocationMessageEventContent, ) -> bool { - let coords = location.geo_uri - .get(utils::GEO_URI_SCHEME.len() ..) + let coords = location + .geo_uri + .get(utils::GEO_URI_SCHEME.len()..) .and_then(|s| { let mut iter = s.split(','); if let (Some(lat), Some(long)) = (iter.next(), iter.next()) { @@ -3914,8 +4310,14 @@ fn populate_location_message_content( } }); if let Some((lat, long)) = coords { - let short_lat = lat.find('.').and_then(|dot| lat.get(..dot + 7)).unwrap_or(lat); - let short_long = long.find('.').and_then(|dot| long.get(..dot + 7)).unwrap_or(long); + let short_lat = lat + .find('.') + .and_then(|dot| lat.get(..dot + 7)) + .unwrap_or(lat); + let short_long = long + .find('.') + .and_then(|dot| long.get(..dot + 7)) + .unwrap_or(long); let safe_lat = htmlize::escape_attribute(lat); let safe_long = htmlize::escape_attribute(long); let safe_geo_uri = htmlize::escape_attribute(&location.geo_uri); @@ -3934,7 +4336,10 @@ fn populate_location_message_content( } else { message_content_widget.show_html( cx, - format!("[Location invalid] {}", htmlize::escape_text(&location.body)) + format!( + "[Location invalid] {}", + htmlize::escape_text(&location.body) + ), ); } @@ -3944,7 +4349,6 @@ fn populate_location_message_content( true } - /// Draws the given redacted message's content into the `message_content_widget`. /// /// Returns whether the redacted message content was fully drawn. @@ -3957,16 +4361,13 @@ fn populate_redacted_message_content( let fully_drawn: bool; let mut redactor_id_and_reason = None; if let Some(redacted_msg) = event_tl_item.latest_json() { - if let Ok(AnySyncTimelineEvent::MessageLike( - AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Redacted(redaction) - ) - )) = redacted_msg.deserialize() { + if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(redaction), + ))) = redacted_msg.deserialize() + { if let Ok(redacted_because) = redaction.unsigned.redacted_because.deserialize() { - redactor_id_and_reason = Some(( - redacted_because.sender, - redacted_because.content.reason, - )); + redactor_id_and_reason = + Some((redacted_because.sender, redacted_because.content.reason)); } } } @@ -3975,7 +4376,10 @@ fn populate_redacted_message_content( if redactor == event_tl_item.sender() { fully_drawn = true; match reason { - Some(r) => format!("⛔ Deleted their own message. Reason: \"{}\".", htmlize::escape_text(r)), + Some(r) => format!( + "⛔ Deleted their own message. Reason: \"{}\".", + htmlize::escape_text(r) + ), None => String::from("⛔ Deleted their own message."), } } else { @@ -3987,9 +4391,11 @@ fn populate_redacted_message_content( true, ); fully_drawn = redactor_name.was_found(); - let redactor_name_esc = htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); + let redactor_name_esc = + htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); match reason { - Some(r) => format!("⛔ {} deleted this message. Reason: \"{}\".", + Some(r) => format!( + "⛔ {} deleted this message. Reason: \"{}\".", redactor_name_esc, htmlize::escape_text(r), ), @@ -4004,7 +4410,6 @@ fn populate_redacted_message_content( fully_drawn } - /// Draws a ReplyPreview above a message if it was in-reply to another message. /// /// ## Arguments @@ -4031,24 +4436,24 @@ fn draw_replied_to_message( show_reply = true; match &in_reply_to_details.event { TimelineDetails::Ready(replied_to_event) => { - let (in_reply_to_username, is_avatar_fully_drawn) = - replied_to_message_view - .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) - .set_avatar_and_get_username( - cx, - timeline_kind, - &replied_to_event.sender, - Some(&replied_to_event.sender_profile), - Some(in_reply_to_details.event_id.as_ref()), - true, - ); + let (in_reply_to_username, is_avatar_fully_drawn) = replied_to_message_view + .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + &replied_to_event.sender, + Some(&replied_to_event.sender_profile), + Some(in_reply_to_details.event_id.as_ref()), + true, + ); fully_drawn = is_avatar_fully_drawn; replied_to_message_view .label(cx, ids!(replied_to_message_content.reply_preview_username)) .set_text(cx, in_reply_to_username.as_str()); - let msg_body = replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); + let msg_body = + replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); populate_preview_of_timeline_item( cx, &msg_body, @@ -4160,7 +4565,8 @@ fn populate_thread_root_summary( &embedded_event.content, &embedded_event.sender, sender_username, - ).format_with(sender_username, true); + ) + .format_with(sender_username, true); match utils::replace_linebreaks_separators(&preview, true) { Cow::Borrowed(_) => Cow::Owned(preview), Cow::Owned(replaced) => Cow::Owned(replaced), @@ -4171,9 +4577,11 @@ fn populate_thread_root_summary( if td.is_unavailable() && let Some(thread_root_event_id) = thread_root_event_id.clone() { - let needs_refresh = fetched_summary - .is_none_or(|fs| fs.latest_reply_preview_text.is_none()); - if needs_refresh && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) { + let needs_refresh = + fetched_summary.is_none_or(|fs| fs.latest_reply_preview_text.is_none()); + if needs_refresh + && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) + { submit_async_request(MatrixRequest::FetchThreadSummaryDetails { timeline_kind: timeline_kind.clone(), thread_root_event_id, @@ -4181,7 +4589,8 @@ fn populate_thread_root_summary( }); } } - fetched_summary.and_then(|fs| fs.latest_reply_preview_text.as_deref()) + fetched_summary + .and_then(|fs| fs.latest_reply_preview_text.as_deref()) .unwrap_or("Loading latest reply...") .into() } @@ -4193,7 +4602,7 @@ fn populate_thread_root_summary( let replies_count_text = match replies_count { 1 => Cow::Borrowed("1 reply"), - n => Cow::Owned(format!("{n} replies")) + n => Cow::Owned(format!("{n} replies")), }; item.label(cx, ids!(thread_summary_count)) .set_text(cx, &replies_count_text); @@ -4213,23 +4622,32 @@ pub fn populate_preview_of_timeline_item( ) { if let Some(m) = timeline_item_content.as_message() { match m.msgtype() { - MessageType::Text(TextMessageEventContent { body, formatted, .. }) - | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { - let _ = populate_text_message_content(cx, widget_out, body, formatted.as_ref(), None, None, None); + MessageType::Text(TextMessageEventContent { + body, formatted, .. + }) + | MessageType::Notice(NoticeMessageEventContent { + body, formatted, .. + }) => { + let _ = populate_text_message_content( + cx, + widget_out, + body, + formatted.as_ref(), + None, + None, + None, + ); return; } - _ => { } // fall through to the general case for all timeline items below. + _ => {} // fall through to the general case for all timeline items below. } } - let html = text_preview_of_timeline_item( - timeline_item_content, - sender_user_id, - sender_username, - ).format_with(sender_username, true); + let html = + text_preview_of_timeline_item(timeline_item_content, sender_user_id, sender_username) + .format_with(sender_username, true); widget_out.show_html(cx, html); } - /// A trait for abstracting over the different types of timeline events /// that can be displayed in a `SmallStateEvent` widget. trait SmallStateEventContent { @@ -4320,7 +4738,9 @@ impl SmallStateEventContent for PollState { ) -> (WidgetRef, ItemDrawnStatus) { item.label(cx, ids!(content)).set_text( cx, - self.fallback_text().unwrap_or_else(|| self.results().question).as_str(), + self.fallback_text() + .unwrap_or_else(|| self.results().question) + .as_str(), ); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -4389,20 +4809,15 @@ impl SmallStateEventContent for RoomMembershipChange { ) -> (WidgetRef, ItemDrawnStatus) { let Some(preview) = text_preview_of_room_membership_change(self, false) else { // Don't actually display anything for nonexistent/unimportant membership changes. - return ( - list.item(cx, item_id, id!(Empty)), - ItemDrawnStatus::new(), - ); + return (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::new()); }; item.label(cx, ids!(content)) .set_text(cx, &preview.format_with(username, false)); // The invite_user_button is only used for "Knocked" membership change events. - item.button(cx, ids!(invite_user_button)).set_visible( - cx, - matches!(self.change(), Some(MembershipChange::Knocked)), - ); + item.button(cx, ids!(invite_user_button)) + .set_visible(cx, matches!(self.change(), Some(MembershipChange::Knocked))); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -4454,7 +4869,8 @@ fn populate_small_state_event( ); // Draw the timestamp as part of the profile. if let Some(dt) = unix_time_millis_to_datetime(event_tl_item.timestamp()) { - item.timestamp(cx, ids!(left_container.timestamp)).set_date_time(cx, dt); + item.timestamp(cx, ids!(left_container.timestamp)) + .set_date_time(cx, dt); } new_drawn_status.profile_drawn = profile_drawn; username @@ -4473,7 +4889,6 @@ fn populate_small_state_event( ) } - /// Returns the display name of the sender of the given `event_tl_item`, if available. fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option { if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { @@ -4483,7 +4898,6 @@ fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option } } - /// Actions related to invites within a room. /// /// These are NOT widget actions, just regular actions. @@ -4518,7 +4932,6 @@ pub enum InviteResultAction { }, } - /// Actions related to a specific message within a room timeline. #[derive(Clone, Default, Debug)] pub enum MessageAction { @@ -4563,7 +4976,6 @@ pub enum MessageAction { // /// The user clicked the "report" button on a message. // Report(MessageDetails), - /// The message at the given item index in the timeline should be highlighted. HighlightMessage(usize), /// The user requested that we show a context menu with actions @@ -4597,11 +5009,15 @@ impl ActionDefaultRef for MessageAction { /// A widget representing a single message of any kind within a room timeline. #[derive(Script, ScriptHook, Widget, Animator)] pub struct Message { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - - #[rust] details: Option, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + + #[rust] + details: Option, } impl Widget for Message { @@ -4616,7 +5032,9 @@ impl Widget for Message { self.animator_play(cx, ids!(highlight.off)); } - let Some(details) = self.details.clone() else { return }; + let Some(details) = self.details.clone() else { + return; + }; // We first handle a click on the replied-to message preview, if present, // because we don't want any widgets within the replied-to message to be @@ -4625,31 +5043,31 @@ impl Widget for Message { Hit::FingerDown(fe) => { if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } // If the hit occurred on the replied-to message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::JumpToRelated(details.clone()), ); } - _ => { } + _ => {} } // Handle clicks on the thread summary shown beneath a thread-root message. @@ -4666,11 +5084,11 @@ impl Widget for Message { apply_hover(cx, COLOR_THREAD_SUMMARY_BG_HOVER); if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } @@ -4682,23 +5100,23 @@ impl Widget for Message { } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } Hit::FingerUp(fe) => { apply_hover(cx, COLOR_THREAD_SUMMARY_BG); if fe.is_over && fe.is_primary_hit() && fe.was_tap() { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenThread(thread_root_event_id.clone()), ); } } - _ => { } + _ => {} } } @@ -4717,21 +5135,21 @@ impl Widget for Message { // A right click means we should display the context menu. if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } Hit::FingerHoverIn(..) => { @@ -4742,12 +5160,16 @@ impl Widget for Message { self.animator_play(cx, ids!(hover.off)); // TODO: here, hide the "action bar" buttons upon hover-out } - _ => { } + _ => {} } if let Event::Actions(actions) = event { for action in actions { - match action.as_widget_action().widget_uid_eq(details.room_screen_widget_uid).cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(details.room_screen_widget_uid) + .cast_ref() + { MessageAction::HighlightMessage(id) if id == &details.item_id => { self.animator_play(cx, ids!(highlight.on)); self.redraw(cx); @@ -4759,7 +5181,11 @@ impl Widget for Message { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - if self.details.as_ref().is_some_and(|d| d.should_be_highlighted) { + if self + .details + .as_ref() + .is_some_and(|d| d.should_be_highlighted) + { script_apply_eval!(cx, self, { draw_bg +: { color: #ffffd1, @@ -4780,7 +5206,9 @@ impl Message { impl MessageRef { fn set_data(&self, details: MessageDetails) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_data(details); } } @@ -4789,7 +5217,7 @@ impl MessageRef { /// /// This function requires passing in a reference to `Cx`, /// which isn't used, but acts as a guarantee that this function -/// must only be called by the main UI thread. +/// must only be called by the main UI thread. pub fn clear_timeline_states(_cx: &mut Cx) { // Clear timeline states cache TIMELINE_STATES.with_borrow_mut(|states| { diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 7cf7d5106..0d08156fd 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -16,30 +16,50 @@ //! so you can use it from other widgets or functions on the main UI thread //! that need to query basic info about a particular room or space. -use std::{cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, rc::Rc, sync::Arc}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + rc::Rc, + sync::Arc, +}; use crossbeam_queue::SegQueue; use makepad_widgets::*; use matrix_sdk_ui::spaces::room_list::SpaceRoomListPaginationState; use ruma::events::tag::TagName; use tokio::sync::mpsc::UnboundedSender; -use matrix_sdk::{RoomState, ruma::{events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId}}; +use matrix_sdk::{ + RoomState, + ruma::{ + events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, + }, +}; use crate::{ app::{AppState, SelectedRoom}, home::{ - navigation_tab_bar::{NavigationBarAction, SelectedTab}, room_context_menu::RoomContextMenuDetails, rooms_list_entry::RoomsListEntryAction, space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt} + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room_context_menu::RoomContextMenuDetails, + rooms_list_entry::RoomsListEntryAction, + space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt}, }, room::{ FetchedRoomAvatar, - room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn}, + room_display_filter::{ + RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn, + }, }, shared::{ - collapsible_header::{CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory}, + collapsible_header::{ + CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory, + }, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction, }, - sliding_sync::{MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request}, - space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, utils::{RoomNameId, VecDiff}, + sliding_sync::{ + MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request, + }, + space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, + utils::{RoomNameId, VecDiff}, }; /// Whether to pre-paginate visible rooms at least once in order to @@ -71,11 +91,10 @@ pub fn get_invited_rooms(_cx: &mut Cx) -> Rc }, + LoadedRooms { max_rooms: Option }, /// Add a new room to the list of rooms the user has been invited to. /// This will be maintained and displayed separately from joined rooms. AddInvitedRoom(InvitedRoomInfo), @@ -171,9 +189,7 @@ pub enum RoomsListUpdate { unread_mentions: u64, }, /// Update the displayable name for the given room. - UpdateRoomName { - new_room_name: RoomNameId, - }, + UpdateRoomName { new_room_name: RoomNameId }, /// Update the avatar (image) for the given room. UpdateRoomAvatar { room_id: OwnedRoomId, @@ -196,21 +212,15 @@ pub enum RoomsListUpdate { new_tags: Tags, }, /// Update the status label at the bottom of the list of all rooms. - Status { - status: String, - }, + Status { status: String }, /// Mark the given room as tombstoned. - TombstonedRoom { - room_id: OwnedRoomId - }, + TombstonedRoom { room_id: OwnedRoomId }, /// Hide the given room from being displayed. /// /// This is useful for temporarily preventing a room from being shown, /// e.g., after a room has been left but before the homeserver has registered /// that we left it and removed it via the RoomListService. - HideRoom { - room_id: OwnedRoomId, - }, + HideRoom { room_id: OwnedRoomId }, /// Scroll to the given room. ScrollToRoom(OwnedRoomId), /// The background space service is now listening for requests, @@ -237,9 +247,7 @@ pub enum RoomsListAction { /// A new room was joined from an accepted invite, /// meaning that the existing `InviteScreen` should be converted /// to a `RoomScreen` to display the now-joined room. - InviteAccepted { - room_name_id: RoomNameId, - }, + InviteAccepted { room_name_id: RoomNameId }, /// Instructs the top-level app to show the context menu for the given room. /// /// Emitted by the RoomsList when the user right-clicks or long-presses @@ -259,7 +267,6 @@ impl ActionDefaultRef for RoomsListAction { } } - /// UI-related info about a joined room. /// /// This includes info needed display a preview of that room in the RoomsList @@ -298,7 +305,6 @@ pub struct JoinedRoomInfo { pub is_direct: bool, /// Whether this room is tombstoned (shut down and replaced with a successor room). pub is_tombstoned: bool, - // TODO: we could store the parent chain(s) of this room, i.e., which spaces // they are children of. One room can be in multiple spaces. } @@ -390,28 +396,34 @@ struct SpaceMapValue { #[derive(Script, Widget)] pub struct RoomsList { - #[deref] view: View, + #[deref] + view: View, /// The list of all rooms that the user has been invited to. /// /// This is a shared reference to the thread-local [`ALL_INVITED_ROOMS`] variable. - #[rust] invited_rooms: Rc>>, + #[rust] + invited_rooms: Rc>>, /// The set of all joined rooms and their cached info. /// This includes both direct rooms and regular rooms, but not invited rooms. - #[rust] all_joined_rooms: HashMap, + #[rust] + all_joined_rooms: HashMap, /// The list of all room IDs in display order, matching the order from the room list service. - #[rust] all_known_rooms_order: VecDeque, + #[rust] + all_known_rooms_order: VecDeque, /// The space that is currently selected as a display filter for the rooms list, if any. /// * If `None` (default), no space is selected, and all rooms can be shown. /// * If `Some`, the rooms list is in "space" mode. A special "Space Lobby" entry /// is shown at the top, and only child rooms within this space will be displayed. - #[rust] selected_space: Option, + #[rust] + selected_space: Option, /// The sender used to send Space-related requests to the background service. - #[rust] space_request_sender: Option>, + #[rust] + space_request_sender: Option>, /// A flattened map of all spaces known to the client. /// @@ -419,50 +431,66 @@ pub struct RoomsList { /// and nested subspaces *directly* within that space. /// /// This can include both joined and non-joined spaces. - #[rust] space_map: HashMap, + #[rust] + space_map: HashMap, /// Rooms that are explicitly hidden and should never be shown in the rooms list. - #[rust] hidden_rooms: HashSet, + #[rust] + hidden_rooms: HashSet, /// The currently-active filter function for the list of rooms. /// /// ## Important Notes /// 1. Do not use this directly. Instead, use the `should_display_room!()` macro. /// 2. This does *not* get auto-applied when it changes, for performance reasons. - #[rust] display_filter: RoomDisplayFilter, + #[rust] + display_filter: RoomDisplayFilter, /// The currently-active sort function for the list of rooms. - #[rust] sort_fn: Option>, + #[rust] + sort_fn: Option>, /// The list of invited rooms currently displayed in the UI. - #[rust] displayed_invited_rooms: Vec, - #[rust(false)] is_invited_rooms_header_expanded: bool, - #[rust] invited_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_invited_rooms: Vec, + #[rust(false)] + is_invited_rooms_header_expanded: bool, + #[rust] + invited_rooms_indexes: RoomCategoryIndexes, /// The list of direct rooms currently displayed in the UI. - #[rust] displayed_direct_rooms: Vec, - #[rust(false)] is_direct_rooms_header_expanded: bool, - #[rust] direct_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_direct_rooms: Vec, + #[rust(false)] + is_direct_rooms_header_expanded: bool, + #[rust] + direct_rooms_indexes: RoomCategoryIndexes, /// The list of regular (non-direct) joined rooms currently displayed in the UI. /// /// **Direct rooms are excluded** from this; they are in `displayed_direct_rooms`. - #[rust] displayed_regular_rooms: Vec, - #[rust(true)] is_regular_rooms_header_expanded: bool, - #[rust] regular_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_regular_rooms: Vec, + #[rust(true)] + is_regular_rooms_header_expanded: bool, + #[rust] + regular_rooms_indexes: RoomCategoryIndexes, /// The latest status message that should be displayed in the bottom status label. - #[rust] status: String, + #[rust] + status: String, /// The currently-selected room. - #[rust] current_active_room: Option, + #[rust] + current_active_room: Option, /// The maximum number of rooms that will ever be loaded. /// /// This should not be used to determine whether all requested rooms have been loaded, /// because we will likely never receive this many rooms due to the room list service /// excluding rooms that we have filtered out (e.g., left or tombstoned rooms, spaces, etc). - #[rust] max_known_rooms: Option, + #[rust] + max_known_rooms: Option, // /// Whether the room list service has loaded all requested rooms from the homeserver. // #[rust] all_rooms_loaded: bool, } @@ -485,15 +513,16 @@ macro_rules! should_display_room { ($self:expr, $room_id:expr, $room:expr) => { !$self.hidden_rooms.contains($room_id) && ($self.display_filter)($room) - && $self.selected_space.as_ref() + && $self + .selected_space + .as_ref() .is_none_or(|space| $self.is_room_indirectly_in_space(space.room_id(), $room_id)) }; } - impl RoomsList { /// Returns whether the homeserver has finished syncing all of the rooms - /// that should be synced to our client based on the currently-specified room list filter. + /// that should be synced to our client based on the currently-specified room list filter. pub fn all_rooms_loaded(&self) -> bool { // TODO: fix this: figure out a way to determine if // all requested rooms have been received from the homeserver. @@ -522,7 +551,10 @@ impl RoomsList { RoomsListUpdate::AddInvitedRoom(invited_room) => { let room_id = invited_room.room_name_id.room_id().clone(); let should_display = should_display_room!(self, &room_id, &invited_room); - let _replaced = self.invited_rooms.borrow_mut().insert(room_id.clone(), invited_room); + let _replaced = self + .invited_rooms + .borrow_mut() + .insert(room_id.clone(), invited_room); if should_display { self.displayed_invited_rooms.push(room_id); } @@ -548,24 +580,29 @@ impl RoomsList { // 3. Emit an action to inform other widgets that the InviteScreen // displaying the invite to this room should be converted to a // RoomScreen displaying the now-joined room. - if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) { + if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) + { log!("Removed room {room_id} from the list of invited rooms"); - self.displayed_invited_rooms.iter() + self.displayed_invited_rooms + .iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); if let Some(room) = self.all_joined_rooms.get(&room_id) { cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::InviteAccepted { room_name_id: room.room_name_id.clone(), - } + }, ); } } self.update_status(); SignalToUI::set_ui_signal(); // signal the RoomScreen to update itself } - RoomsListUpdate::UpdateRoomAvatar { room_id, room_avatar } => { + RoomsListUpdate::UpdateRoomAvatar { + room_id, + room_avatar, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.room_avatar = room_avatar; } else if let Some(room) = self.invited_rooms.borrow_mut().get_mut(&room_id) { @@ -574,14 +611,23 @@ impl RoomsList { error!("Error: couldn't find room {room_id} to update avatar"); } } - RoomsListUpdate::UpdateLatestEvent { room_id, timestamp, latest_message_text } => { + RoomsListUpdate::UpdateLatestEvent { + room_id, + timestamp, + latest_message_text, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.latest = Some((timestamp, latest_message_text)); } else { error!("Error: couldn't find room {room_id} to update latest event"); } } - RoomsListUpdate::UpdateNumUnreadMessages { room_id, is_marked_unread, unread_messages, unread_mentions } => { + RoomsListUpdate::UpdateNumUnreadMessages { + room_id, + is_marked_unread, + unread_messages, + unread_mentions, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.num_unread_messages = match unread_messages { UnreadMessageCount::Unknown => 0, @@ -590,11 +636,13 @@ impl RoomsList { room.num_unread_mentions = unread_mentions; room.is_marked_unread = is_marked_unread; } else { - warning!("Warning: couldn't find room {} to update unread messages count", room_id); + warning!( + "Warning: couldn't find room {} to update unread messages count", + room_id + ); } } RoomsListUpdate::UpdateRoomName { new_room_name } => { - // TODO: broadcast a new AppState action to ensure that this room's or space's new name // gets updated in all of the `SelectedRoom` instances throughout Robrix, // e.g., the name of the room in the Dock Tab or the StackNav header. @@ -607,12 +655,16 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms.iter().position(|r| r == &room_id), + self.displayed_direct_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms.iter().position(|r| r == &room_id), + self.displayed_regular_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -630,7 +682,9 @@ impl RoomsList { if let Some(invited_room) = invited_rooms.get_mut(&room_id) { invited_room.room_name_id = new_room_name; let should_display = should_display_room!(self, &room_id, invited_room); - let pos_in_list = self.displayed_invited_rooms.iter() + let pos_in_list = self + .displayed_invited_rooms + .iter() .position(|r| r == &room_id); if should_display { if pos_in_list.is_none() { @@ -640,7 +694,9 @@ impl RoomsList { pos_in_list.map(|i| self.displayed_invited_rooms.remove(i)); } } else { - warning!("Warning: couldn't find room {new_room_name} to update its name."); + warning!( + "Warning: couldn't find room {new_room_name} to update its name." + ); } } } @@ -651,7 +707,8 @@ impl RoomsList { continue; } enqueue_popup_notification( - format!("{} was changed from {} to {}.", + format!( + "{} was changed from {} to {}.", room.room_name_id, if room.is_direct { "direct" } else { "regular" }, if is_direct { "direct" } else { "regular" } @@ -666,7 +723,8 @@ impl RoomsList { } else { &mut self.displayed_regular_rooms }; - list_to_remove_from.iter() + list_to_remove_from + .iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); @@ -690,19 +748,23 @@ impl RoomsList { // and then options/buttons for the user to re-join it if desired. if let Some(removed) = self.all_joined_rooms.remove(&room_id) { - log!("Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}"); + log!( + "Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}" + ); let list_to_remove_from = if removed.is_direct { &mut self.displayed_direct_rooms } else { &mut self.displayed_regular_rooms }; - list_to_remove_from.iter() + list_to_remove_from + .iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); - } - else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { + } else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) + { log!("Removed room {room_id} from the list of all invited rooms"); - self.displayed_invited_rooms.iter() + self.displayed_invited_rooms + .iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); } @@ -723,7 +785,7 @@ impl RoomsList { } RoomsListUpdate::LoadedRooms { max_rooms } => { self.max_known_rooms = max_rooms; - }, + } RoomsListUpdate::Tags { room_id, new_tags } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.tags = new_tags; @@ -743,12 +805,16 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms.iter().position(|r| r == &room_id), + self.displayed_direct_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms.iter().position(|r| r == &room_id), + self.displayed_regular_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -760,20 +826,32 @@ impl RoomsList { pos_in_list.map(|i| displayed_list.remove(i)); } } else { - warning!("Warning: couldn't find room {room_id} to update the tombstone status"); + warning!( + "Warning: couldn't find room {room_id} to update the tombstone status" + ); } } RoomsListUpdate::HideRoom { room_id } => { self.hidden_rooms.insert(room_id.clone()); // Hiding a regular room is the most common case (e.g., after its successor is joined), // so we check that list first. - if let Some(i) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { + if let Some(i) = self + .displayed_regular_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_regular_rooms.remove(i); - } - else if let Some(i) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { + } else if let Some(i) = self + .displayed_direct_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_direct_rooms.remove(i); - } - else if let Some(i) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { + } else if let Some(i) = self + .displayed_invited_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_invited_rooms.remove(i); } } @@ -782,75 +860,89 @@ impl RoomsList { self.recalculate_indexes(); let portal_list = self.view.portal_list(cx, ids!(list)); let speed = 50.0; - let portal_list_index = if let Some(regular_index) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { + let portal_list_index = if let Some(regular_index) = self + .displayed_regular_rooms + .iter() + .position(|r| r == &room_id) + { self.regular_rooms_indexes.first_room_index + regular_index - } - else if let Some(direct_index) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { + } else if let Some(direct_index) = self + .displayed_direct_rooms + .iter() + .position(|r| r == &room_id) + { self.direct_rooms_indexes.first_room_index + direct_index - } - else if let Some(invited_index) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { + } else if let Some(invited_index) = self + .displayed_invited_rooms + .iter() + .position(|r| r == &room_id) + { self.invited_rooms_indexes.first_room_index + invited_index - } - else { continue }; + } else { + continue; + }; // Scroll to just above the room to make it more obviously visible. - portal_list.smooth_scroll_to(cx, portal_list_index.saturating_sub(1), speed, Some(15)); + portal_list.smooth_scroll_to( + cx, + portal_list_index.saturating_sub(1), + speed, + Some(15), + ); } RoomsListUpdate::SpaceRequestSender(sender) => { self.space_request_sender = Some(sender); - num_updates -= 1; // this does not require a redraw. + num_updates -= 1; // this does not require a redraw. } - RoomsListUpdate::RoomOrderUpdate(diff) => { - match diff { - VecDiff::Append { values } => { - self.all_known_rooms_order.extend(values); - needs_sort = true; - } - VecDiff::Clear => { - self.all_known_rooms_order.clear(); - needs_sort = true; - } - VecDiff::PushFront { value } => { - self.all_known_rooms_order.push_front(value); - needs_sort = true; - } - VecDiff::PushBack { value } => { - self.all_known_rooms_order.push_back(value); - needs_sort = true; - } - VecDiff::PopFront => { - self.all_known_rooms_order.pop_front(); - needs_sort = true; - } - VecDiff::PopBack => { - self.all_known_rooms_order.pop_back(); + RoomsListUpdate::RoomOrderUpdate(diff) => match diff { + VecDiff::Append { values } => { + self.all_known_rooms_order.extend(values); + needs_sort = true; + } + VecDiff::Clear => { + self.all_known_rooms_order.clear(); + needs_sort = true; + } + VecDiff::PushFront { value } => { + self.all_known_rooms_order.push_front(value); + needs_sort = true; + } + VecDiff::PushBack { value } => { + self.all_known_rooms_order.push_back(value); + needs_sort = true; + } + VecDiff::PopFront => { + self.all_known_rooms_order.pop_front(); + needs_sort = true; + } + VecDiff::PopBack => { + self.all_known_rooms_order.pop_back(); + needs_sort = true; + } + VecDiff::Insert { index, value } => { + if index <= self.all_known_rooms_order.len() { + self.all_known_rooms_order.insert(index, value); needs_sort = true; } - VecDiff::Insert { index, value } => { - if index <= self.all_known_rooms_order.len() { - self.all_known_rooms_order.insert(index, value); - needs_sort = true; - } - } - VecDiff::Set { index, value } => { - if let Some(existing) = self.all_known_rooms_order.get_mut(index) { - if *existing != value { - *existing = value; - needs_sort = true; - } - } - } - VecDiff::Remove { index } => { - if index < self.all_known_rooms_order.len() { - self.all_known_rooms_order.remove(index); + } + VecDiff::Set { index, value } => { + if let Some(existing) = self.all_known_rooms_order.get_mut(index) { + if *existing != value { + *existing = value; needs_sort = true; } } - VecDiff::Truncate { length } => { - self.all_known_rooms_order.truncate(length); + } + VecDiff::Remove { index } => { + if index < self.all_known_rooms_order.len() { + self.all_known_rooms_order.remove(index); needs_sort = true; } } - } + VecDiff::Truncate { length } => { + self.all_known_rooms_order.truncate(length); + needs_sort = true; + } + }, } } if needs_sort { @@ -875,9 +967,9 @@ impl RoomsList { + self.displayed_regular_rooms.len(); let mut text = match (self.display_filter.is_none(), num_rooms) { - (true, 0) => "No joined or invited rooms found".to_string(), - (true, 1) => "Loaded 1 room".to_string(), - (true, n) => format!("Loaded {n} rooms"), + (true, 0) => "No joined or invited rooms found".to_string(), + (true, 1) => "Loaded 1 room".to_string(), + (true, n) => format!("Loaded {n} rooms"), (false, 0) => "No matching rooms found".to_string(), (false, 1) => "Found 1 matching room".to_string(), (false, n) => format!("Found {n} matching rooms"), @@ -926,7 +1018,6 @@ impl RoomsList { self.redraw(cx); } - /// Generates a tuple of three kinds of displayed rooms (accounting for the current `display_filter`): /// 1. displayed_invited_rooms /// 2. displayed_regular_rooms @@ -934,7 +1025,7 @@ impl RoomsList { /// /// If `self.sort_fn` is `Some`, the rooms are ordered based on that function. /// Otherwise, the rooms are ordered based on `self.all_known_rooms_order` (the default). - fn generate_displayed_rooms(&self) -> (Vec,Vec, Vec) { + fn generate_displayed_rooms(&self) -> (Vec, Vec, Vec) { let mut new_displayed_invited_rooms = Vec::new(); let mut new_displayed_regular_rooms = Vec::new(); let mut new_displayed_direct_rooms = Vec::new(); @@ -952,7 +1043,9 @@ impl RoomsList { // If a sort function was provided, use it. if let Some(sort_fn) = self.sort_fn.as_deref() { - let mut filtered_joined_rooms = self.all_joined_rooms.iter() + let mut filtered_joined_rooms = self + .all_joined_rooms + .iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_joined_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -960,7 +1053,8 @@ impl RoomsList { push_joined_room(room_id, jr) } - let mut filtered_invited_rooms = invited_rooms_ref.iter() + let mut filtered_invited_rooms = invited_rooms_ref + .iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_invited_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -983,7 +1077,11 @@ impl RoomsList { } } - (new_displayed_invited_rooms, new_displayed_regular_rooms, new_displayed_direct_rooms) + ( + new_displayed_invited_rooms, + new_displayed_regular_rooms, + new_displayed_direct_rooms, + ) } /// Calculates the indexes in the PortalList where the headers and rooms should be drawn. @@ -996,35 +1094,35 @@ impl RoomsList { // Based on the various displayed room lists and is_expanded state of each room header, // calculate the indexes in the PortalList where the headers and rooms should be drawn. let should_show_invited_rooms_header = !self.displayed_invited_rooms.is_empty(); - let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); + let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); let should_show_regular_rooms_header = !self.displayed_regular_rooms.is_empty(); let index_of_invited_rooms_header = should_show_invited_rooms_header.then_some(0); let index_of_first_invited_room = should_show_invited_rooms_header as usize; - let index_after_invited_rooms = index_of_first_invited_room + - if self.is_invited_rooms_header_expanded { + let index_after_invited_rooms = index_of_first_invited_room + + if self.is_invited_rooms_header_expanded { self.displayed_invited_rooms.len() } else { 0 }; - let index_of_direct_rooms_header = should_show_direct_rooms_header - .then_some(index_after_invited_rooms); - let index_of_first_direct_room = index_after_invited_rooms + - should_show_direct_rooms_header as usize; - let index_after_direct_rooms = index_of_first_direct_room + - if self.is_direct_rooms_header_expanded { + let index_of_direct_rooms_header = + should_show_direct_rooms_header.then_some(index_after_invited_rooms); + let index_of_first_direct_room = + index_after_invited_rooms + should_show_direct_rooms_header as usize; + let index_after_direct_rooms = index_of_first_direct_room + + if self.is_direct_rooms_header_expanded { self.displayed_direct_rooms.len() } else { 0 }; - let index_of_regular_rooms_header = should_show_regular_rooms_header - .then_some(index_after_direct_rooms); - let index_of_first_regular_room = index_after_direct_rooms + - should_show_regular_rooms_header as usize; - let index_after_regular_rooms = index_of_first_regular_room + - if self.is_regular_rooms_header_expanded { + let index_of_regular_rooms_header = + should_show_regular_rooms_header.then_some(index_after_direct_rooms); + let index_of_first_regular_room = + index_after_direct_rooms + should_show_regular_rooms_header as usize; + let index_after_regular_rooms = index_of_first_regular_room + + if self.is_regular_rooms_header_expanded { self.displayed_regular_rooms.len() } else { 0 @@ -1050,32 +1148,43 @@ impl RoomsList { /// Handle any incoming updates to spaces' room lists and pagination state. fn handle_space_room_list_action(&mut self, cx: &mut Cx, action: &SpaceRoomListAction) { match action { - SpaceRoomListAction::UpdatedChildren { space_id, parent_chain, direct_child_rooms, direct_subspaces } => { + SpaceRoomListAction::UpdatedChildren { + space_id, + parent_chain, + direct_child_rooms, + direct_subspaces, + } => { match self.space_map.entry(space_id.clone()) { Entry::Occupied(mut occ) => { let occ_mut = occ.get_mut(); occ_mut.parent_chain = parent_chain.clone(); occ_mut.direct_child_rooms = Arc::clone(direct_child_rooms); - occ_mut.direct_subspaces = Arc::clone(direct_subspaces); + occ_mut.direct_subspaces = Arc::clone(direct_subspaces); } Entry::Vacant(vac) => { vac.insert_entry(SpaceMapValue { is_fully_paginated: false, parent_chain: parent_chain.clone(), direct_child_rooms: Arc::clone(direct_child_rooms), - direct_subspaces: Arc::clone(direct_subspaces), + direct_subspaces: Arc::clone(direct_subspaces), }); } } - if self.selected_space.as_ref().is_some_and(|sel_space| - sel_space.room_id() == space_id - || parent_chain.contains(sel_space.room_id()) - ) { + if self.selected_space.as_ref().is_some_and(|sel_space| { + sel_space.room_id() == space_id || parent_chain.contains(sel_space.room_id()) + }) { self.update_displayed_rooms(cx, false); } } - SpaceRoomListAction::PaginationState { space_id, parent_chain, state } => { - let is_fully_paginated = matches!(state, SpaceRoomListPaginationState::Idle { end_reached: true }); + SpaceRoomListAction::PaginationState { + space_id, + parent_chain, + state, + } => { + let is_fully_paginated = matches!( + state, + SpaceRoomListPaginationState::Idle { end_reached: true } + ); // Only re-fetch the list of rooms in this space if it was not already fully paginated. let should_fetch_rooms: bool; match self.space_map.entry(space_id.clone()) { @@ -1094,15 +1203,22 @@ impl RoomsList { } } let Some(sender) = self.space_request_sender.as_ref() else { - error!("BUG: RoomsList: no space request sender was available after pagination state update."); + error!( + "BUG: RoomsList: no space request sender was available after pagination state update." + ); return; }; if should_fetch_rooms { - if sender.send(SpaceRequest::GetChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send GetRooms request for space {space_id}."); + if sender + .send(SpaceRequest::GetChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send GetRooms request for space {space_id}." + ); } } @@ -1112,11 +1228,16 @@ impl RoomsList { // all of its children, such that we can see if any of them are subspaces, // and then we'll paginate those as well. if !is_fully_paginated { - if sender.send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send pagination request for space {space_id}."); + if sender + .send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send pagination request for space {space_id}." + ); } } } @@ -1128,7 +1249,10 @@ impl RoomsList { None, ); } - SpaceRoomListAction::LeaveSpaceResult { space_name_id, result } => match result { + SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result, + } => match result { Ok(()) => { enqueue_popup_notification( format!("Successfully left space \"{}\".", space_name_id), @@ -1136,7 +1260,11 @@ impl RoomsList { Some(4.0), ); // If the space we left was the currently-selected one, go back to the main Home view. - if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { + if self + .selected_space + .as_ref() + .is_some_and(|s| s.room_id() == space_name_id.room_id()) + { cx.action(NavigationBarAction::GoToHome); } } @@ -1151,14 +1279,18 @@ impl RoomsList { }, // Details-related space actions are handled by SpaceLobbyScreen, not RoomsList. SpaceRoomListAction::DetailedChildren { .. } - | SpaceRoomListAction::TopLevelSpaceDetails(_) => { } + | SpaceRoomListAction::TopLevelSpaceDetails(_) => {} } } /// Returns whether the given target room or space is indirectly within the given parent space. /// /// This will recursively search all nested spaces within the given `parent_space`. - fn is_room_indirectly_in_space(&self, parent_space: &OwnedRoomId, target: &OwnedRoomId) -> bool { + fn is_room_indirectly_in_space( + &self, + parent_space: &OwnedRoomId, + target: &OwnedRoomId, + ) -> bool { if let Some(smv) = self.space_map.get(parent_space) { if smv.direct_child_rooms.contains(target) { return true; @@ -1186,12 +1318,14 @@ impl Widget for RoomsList { let props = RoomsListScopeProps { was_scrolling: self.view.portal_list(cx, ids!(list)).was_scrolling(), }; - let rooms_list_actions = cx.capture_actions( - |cx| self.view.handle_event(cx, event, &mut Scope::with_props(&props)) - ); + let rooms_list_actions = cx.capture_actions(|cx| { + self.view + .handle_event(cx, event, &mut Scope::with_props(&props)) + }); for action in rooms_list_actions { // Handle a regular room (joined or invited) being clicked. - if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() { + if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() + { let new_selected_room = if let Some(jr) = self.all_joined_rooms.get(&room_id) { SelectedRoom::JoinedRoom { room_name_id: jr.room_name_id.clone(), @@ -1207,13 +1341,15 @@ impl Widget for RoomsList { self.current_active_room = Some(new_selected_room.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_room), ); self.redraw(cx); } // Handle a room being right-clicked or long-pressed by opening the room context menu. - else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = action.as_widget_action().cast() { + else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = + action.as_widget_action().cast() + { // Determine details for the context menu let Some(jr) = self.all_joined_rooms.get(&room_id) else { error!("BUG: couldn't find right-clicked room details for room {room_id}"); @@ -1226,29 +1362,35 @@ impl Widget for RoomsList { is_marked_unread: jr.is_marked_unread, }; cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::OpenRoomContextMenu { details, pos }, ); } // Handle the space lobby being clicked. else if let Some(SpaceLobbyAction::SpaceLobbyEntryClicked) = action.downcast_ref() { - let Some(space_name_id) = self.selected_space.clone() else { continue }; + let Some(space_name_id) = self.selected_space.clone() else { + continue; + }; let new_selected_space = SelectedRoom::Space { space_name_id }; self.current_active_room = Some(new_selected_space.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_space), ); self.redraw(cx); } // Handle a collapsible header being clicked. - else if let CollapsibleHeaderAction::Toggled { category } = action.as_widget_action().cast() { + else if let CollapsibleHeaderAction::Toggled { category } = + action.as_widget_action().cast() + { match category { HeaderCategory::Invites => { - self.is_invited_rooms_header_expanded = !self.is_invited_rooms_header_expanded; + self.is_invited_rooms_header_expanded = + !self.is_invited_rooms_header_expanded; } HeaderCategory::RegularRooms => { - self.is_regular_rooms_header_expanded = !self.is_regular_rooms_header_expanded; + self.is_regular_rooms_header_expanded = + !self.is_regular_rooms_header_expanded; } HeaderCategory::DirectRooms => { self.is_direct_rooms_header_expanded = @@ -1273,47 +1415,73 @@ impl Widget for RoomsList { if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { match tab { SelectedTab::Space { space_name_id } => { - if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { + if self + .selected_space + .as_ref() + .is_some_and(|s| s.room_id() == space_name_id.room_id()) + { continue; } self.selected_space = Some(space_name_id.clone()); - self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, true); + self.view + .space_lobby_entry(cx, ids!(space_lobby_entry)) + .set_visible(cx, true); // If we don't have the full list of children in this newly-selected space, then fetch it. - let (is_fully_paginated, parent_chain) = self.space_map + let (is_fully_paginated, parent_chain) = self + .space_map .get(space_name_id.room_id()) .map(|smv| (smv.is_fully_paginated, smv.parent_chain.clone())) .unwrap_or_default(); if !is_fully_paginated { let Some(sender) = self.space_request_sender.as_ref() else { - error!("BUG: RoomsList: no space request sender was available."); + error!( + "BUG: RoomsList: no space request sender was available." + ); continue; }; - if sender.send(SpaceRequest::SubscribeToSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}."); + if sender + .send(SpaceRequest::SubscribeToSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}." + ); } - if sender.send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}."); + if sender + .send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}." + ); } - if sender.send(SpaceRequest::GetChildren { - space_id: space_name_id.room_id().clone(), - parent_chain, - }).is_err() { - error!("BUG: RoomsList: failed to send GetRooms request for space {space_name_id}."); + if sender + .send(SpaceRequest::GetChildren { + space_id: space_name_id.room_id().clone(), + parent_chain, + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send GetRooms request for space {space_name_id}." + ); } } } _ => { self.selected_space = None; - self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, false); + self.view + .space_lobby_entry(cx, ids!(space_lobby_entry)) + .set_visible(cx, false); } } @@ -1372,25 +1540,31 @@ impl Widget for RoomsList { let total_count = status_label_id + 1; let get_invited_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.invited_rooms_indexes.first_room_index) - .and_then(|index| self.is_invited_rooms_header_expanded - .then(|| self.displayed_invited_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.invited_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_invited_rooms_header_expanded + .then(|| self.displayed_invited_rooms.get(index)) + }) .flatten() }; let get_direct_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.direct_rooms_indexes.first_room_index) - .and_then(|index| self.is_direct_rooms_header_expanded - .then(|| self.displayed_direct_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.direct_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_direct_rooms_header_expanded + .then(|| self.displayed_direct_rooms.get(index)) + }) .flatten() }; let get_regular_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.regular_rooms_indexes.first_room_index) - .and_then(|index| self.is_regular_rooms_header_expanded - .then(|| self.displayed_regular_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.regular_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_regular_rooms_header_expanded + .then(|| self.displayed_regular_rooms.get(index)) + }) .flatten() }; @@ -1402,7 +1576,9 @@ impl Widget for RoomsList { portal_list_ref.set_first_id_and_scroll(status_label_id, 0.0); } // We only care about drawing the portal list. - let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + let Some(mut list) = portal_list_ref.borrow_mut() else { + continue; + }; list.set_item_range(cx, 0, total_count); @@ -1418,12 +1594,13 @@ impl Widget for RoomsList { self.displayed_invited_rooms.len() as u64, ); item.draw_all(cx, &mut scope); - } - else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { + } else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { let mut invited_rooms_mut = self.invited_rooms.borrow_mut(); if let Some(invited_room) = invited_rooms_mut.get_mut(invited_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - invited_room.is_selected = self.current_active_room.as_ref() + invited_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == invited_room_id); // Pass the room info down to the RoomsListEntry widget via Scope. scope = Scope::with_props(&*invited_room); @@ -1432,8 +1609,7 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } - else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { + } else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1444,11 +1620,12 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } - else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { + } else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { if let Some(direct_room) = self.all_joined_rooms.get_mut(direct_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - direct_room.is_selected = self.current_active_room.as_ref() + direct_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == direct_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1469,8 +1646,7 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } - else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { + } else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1481,11 +1657,12 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } - else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { + } else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { if let Some(regular_room) = self.all_joined_rooms.get_mut(regular_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - regular_room.is_selected = self.current_active_room.as_ref() + regular_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == regular_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1503,7 +1680,8 @@ impl Widget for RoomsList { scope = Scope::with_props(&*regular_room); item.draw_all(cx, &mut scope); } else { - list.item(cx, portal_list_index, id!(empty)).draw_all(cx, &mut scope); + list.item(cx, portal_list_index, id!(empty)) + .draw_all(cx, &mut scope); } } // Draw the status label as the bottom entry. @@ -1527,7 +1705,9 @@ impl Widget for RoomsList { impl RoomsListRef { /// See [`RoomsList::all_rooms_loaded()`]. pub fn all_rooms_loaded(&self) -> bool { - let Some(inner) = self.borrow() else { return false; }; + let Some(inner) = self.borrow() else { + return false; + }; inner.all_rooms_loaded() } @@ -1544,14 +1724,17 @@ impl RoomsListRef { /// Returns the name of the given room, if it is known and loaded. pub fn get_room_name(&self, room_id: &OwnedRoomId) -> Option { let inner = self.borrow()?; - inner.all_joined_rooms + inner + .all_joined_rooms .get(room_id) .map(|jr| jr.room_name_id.clone()) - .or_else(|| - inner.invited_rooms.borrow() + .or_else(|| { + inner + .invited_rooms + .borrow() .get(room_id) .map(|ir| ir.room_name_id.clone()) - ) + }) } /// Returns the currently-selected space (the one selected in the SpacesBar). @@ -1561,7 +1744,10 @@ impl RoomsListRef { /// Same as [`Self::get_selected_space()`], but only returns the space ID. pub fn get_selected_space_id(&self) -> Option { - self.borrow()?.selected_space.as_ref().map(|ss| ss.room_id().clone()) + self.borrow()? + .selected_space + .as_ref() + .map(|ss| ss.room_id().clone()) } /// Returns a clone of the space request sender channel, if available. diff --git a/src/home/rooms_list_entry.rs b/src/home/rooms_list_entry.rs index d421a12ac..d8eef8139 100644 --- a/src/home/rooms_list_entry.rs +++ b/src/home/rooms_list_entry.rs @@ -2,10 +2,12 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use crate::{ - room::FetchedRoomAvatar, shared::{ - avatar::AvatarWidgetExt, - html_or_plaintext::HtmlOrPlaintextWidgetExt, unread_badge::UnreadBadgeWidgetExt as _, - }, utils::{self, relative_format} + room::FetchedRoomAvatar, + shared::{ + avatar::AvatarWidgetExt, html_or_plaintext::HtmlOrPlaintextWidgetExt, + unread_badge::UnreadBadgeWidgetExt as _, + }, + utils::{self, relative_format}, }; use super::rooms_list::{InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListScopeProps}; @@ -197,8 +199,10 @@ script_mod! { /// An entry in the rooms list. #[derive(Script, Widget)] pub struct RoomsListEntry { - #[deref] view: View, - #[rust] room_id: Option, + #[deref] + view: View, + #[rust] + room_id: Option, } impl ScriptHook for RoomsListEntry { @@ -247,21 +251,26 @@ impl Widget for RoomsListEntry { cx.set_key_focus(area); if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - uid, + uid, RoomsListEntryAction::SecondaryClicked(room_id.clone(), fe.abs), ); } } Hit::FingerLongPress(fe) => { cx.widget_action( - uid, + uid, RoomsListEntryAction::SecondaryClicked(room_id.clone(), fe.abs), ); } - Hit::FingerUp(fe) if !rooms_list_props.was_scrolling && fe.is_over && fe.is_primary_hit() && fe.was_tap() => { - cx.widget_action(uid, RoomsListEntryAction::PrimaryClicked(room_id.clone())); + Hit::FingerUp(fe) + if !rooms_list_props.was_scrolling + && fe.is_over + && fe.is_primary_hit() + && fe.was_tap() => + { + cx.widget_action(uid, RoomsListEntryAction::PrimaryClicked(room_id.clone())); } - _ => { } + _ => {} } } @@ -271,8 +280,7 @@ impl Widget for RoomsListEntry { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { if let Some(room_info) = scope.props.get::() { self.room_id = Some(room_info.room_name_id.room_id().clone()); - } - else if let Some(room_info) = scope.props.get::() { + } else if let Some(room_info) = scope.props.get::() { self.room_id = Some(room_info.room_name_id.room_id().clone()); } @@ -282,9 +290,12 @@ impl Widget for RoomsListEntry { #[derive(Script, ScriptHook, Widget, Animator)] pub struct RoomsListEntryContent { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for RoomsListEntryContent { @@ -308,12 +319,10 @@ impl Widget for RoomsListEntryContent { impl RoomsListEntryContent { /// Populates this RoomsListEntry with info about a joined room. - pub fn draw_joined_room( - &mut self, - cx: &mut Cx, - room_info: &JoinedRoomInfo, - ) { - self.view.label(cx, ids!(room_name)).set_text(cx, &room_info.room_name_id.to_string()); + pub fn draw_joined_room(&mut self, cx: &mut Cx, room_info: &JoinedRoomInfo) { + self.view + .label(cx, ids!(room_name)) + .set_text(cx, &room_info.room_name_id.to_string()); if let Some((ts, msg)) = room_info.latest.as_ref() { if let Some(human_readable_date) = relative_format(*ts) { self.view @@ -325,35 +334,51 @@ impl RoomsListEntryContent { .show_html(cx, msg); } - self.view.unread_badge(cx, ids!(unread_badge)).update_counts( - room_info.is_marked_unread, - room_info.num_unread_mentions, - room_info.num_unread_messages, - ); + self.view + .unread_badge(cx, ids!(unread_badge)) + .update_counts( + room_info.is_marked_unread, + room_info.num_unread_mentions, + room_info.num_unread_messages, + ); self.draw_common(cx, &room_info.room_avatar, room_info.is_selected); // Show tombstone icon if the room is tombstoned - self.view.view(cx, ids!(tombstone_icon)).set_visible(cx, room_info.is_tombstoned); + self.view + .view(cx, ids!(tombstone_icon)) + .set_visible(cx, room_info.is_tombstoned); } /// Populates this RoomsListEntry with info about an invited room. - pub fn draw_invited_room( - &mut self, - cx: &mut Cx, - room_info: &InvitedRoomInfo, - ) { - self.view.label(cx, ids!(room_name)).set_text(cx, &room_info.room_name_id.to_string()); + pub fn draw_invited_room(&mut self, cx: &mut Cx, room_info: &InvitedRoomInfo) { + self.view + .label(cx, ids!(room_name)) + .set_text(cx, &room_info.room_name_id.to_string()); // Hide the timestamp field, and use the latest message field to show the inviter. self.view.label(cx, ids!(timestamp)).set_text(cx, ""); let inviter_string = match &room_info.inviter_info { - Some(InviterInfo { user_id, display_name: Some(dn), .. }) => format!("Invited by {} ({})", htmlize::escape_text(dn), htmlize::escape_text(user_id.as_str())), - Some(InviterInfo { user_id, .. }) => format!("Invited by {}", htmlize::escape_text(user_id.as_str())), + Some(InviterInfo { + user_id, + display_name: Some(dn), + .. + }) => format!( + "Invited by {} ({})", + htmlize::escape_text(dn), + htmlize::escape_text(user_id.as_str()) + ), + Some(InviterInfo { user_id, .. }) => { + format!("Invited by {}", htmlize::escape_text(user_id.as_str())) + } None => String::from("You were invited"), }; - self.view.html_or_plaintext(cx, ids!(latest_message)).show_html(cx, &inviter_string); + self.view + .html_or_plaintext(cx, ids!(latest_message)) + .show_html(cx, &inviter_string); match room_info.room_avatar { FetchedRoomAvatar::Text(ref text) => { - self.view.avatar(cx, ids!(avatar)).show_text(cx, None, None, text); + self.view + .avatar(cx, ids!(avatar)) + .show_text(cx, None, None, text); } FetchedRoomAvatar::Image(ref img_bytes) => { let _ = self.view.avatar(cx, ids!(avatar)).show_image( @@ -372,15 +397,12 @@ impl RoomsListEntryContent { } /// Populates the widgets common to both invited and joined rooms list entries. - pub fn draw_common( - &mut self, - cx: &mut Cx, - room_avatar: &FetchedRoomAvatar, - is_selected: bool, - ) { + pub fn draw_common(&mut self, cx: &mut Cx, room_avatar: &FetchedRoomAvatar, is_selected: bool) { match room_avatar { FetchedRoomAvatar::Text(text) => { - self.view.avatar(cx, ids!(avatar)).show_text(cx, None, None, text); + self.view + .avatar(cx, ids!(avatar)) + .show_text(cx, None, None, text); } FetchedRoomAvatar::Image(img_bytes) => { let _ = self.view.avatar(cx, ids!(avatar)).show_image( @@ -422,7 +444,13 @@ impl RoomsListEntryContent { } // Toggle the background color via the animator (handles selected/deselected bg). - self.animator_toggle(cx, is_selected, Animate::No, ids!(selected.on), ids!(selected.off)); + self.animator_toggle( + cx, + is_selected, + Animate::No, + ids!(selected.on), + ids!(selected.off), + ); // Update text colors for room name. let mut room_name_label = self.view.label(cx, ids!(room_name)); @@ -456,13 +484,18 @@ impl RoomsListEntryContent { // When not selected, restore the default blue link color. self.view .html_or_plaintext(cx, ids!(latest_message)) - .set_link_color(cx, if is_selected { - None - } else { - Some(vec4(0., 0., 0.933, 1.0)) // #0000EE, default HtmlLink color - }); - - let mut pt_label = self.view.label(cx, ids!(latest_message.plaintext_view.pt_label)); + .set_link_color( + cx, + if is_selected { + None + } else { + Some(vec4(0., 0., 0.933, 1.0)) // #0000EE, default HtmlLink color + }, + ); + + let mut pt_label = self + .view + .label(cx, ids!(latest_message.plaintext_view.pt_label)); script_apply_eval!(cx, pt_label, { draw_text +: { color: #(message_text_color) diff --git a/src/home/rooms_list_header.rs b/src/home/rooms_list_header.rs index eac4372a4..3b02c58de 100644 --- a/src/home/rooms_list_header.rs +++ b/src/home/rooms_list_header.rs @@ -1,6 +1,6 @@ //! The RoomsListHeader contains the title label and loading spinner for rooms list. //! -//! This widget is designed to be reused across both Desktop and Mobile variants +//! This widget is designed to be reused across both Desktop and Mobile variants //! of the RoomsSideBar to avoid code duplication. use std::mem::discriminant; @@ -85,9 +85,11 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct RoomsListHeader { - #[deref] view: View, + #[deref] + view: View, - #[rust(State::Idle)] sync_state: State, + #[rust(State::Idle)] + sync_state: State, } impl Widget for RoomsListHeader { @@ -101,9 +103,15 @@ impl Widget for RoomsListHeader { if matches!(self.sync_state, State::Offline) { continue; } - self.view.view(cx, ids!(loading_spinner)).set_visible(cx, *is_syncing); - self.view.view(cx, ids!(synced_icon)).set_visible(cx, !*is_syncing); - self.view.view(cx, ids!(offline_icon)).set_visible(cx, false); + self.view + .view(cx, ids!(loading_spinner)) + .set_visible(cx, *is_syncing); + self.view + .view(cx, ids!(synced_icon)) + .set_visible(cx, !*is_syncing); + self.view + .view(cx, ids!(offline_icon)) + .set_visible(cx, false); self.redraw(cx); continue; } @@ -112,7 +120,9 @@ impl Widget for RoomsListHeader { continue; } if matches!(new_state, State::Offline) { - self.view.view(cx, ids!(loading_spinner)).set_visible(cx, false); + self.view + .view(cx, ids!(loading_spinner)) + .set_visible(cx, false); self.view.view(cx, ids!(synced_icon)).set_visible(cx, false); self.view.view(cx, ids!(offline_icon)).set_visible(cx, true); enqueue_popup_notification( @@ -121,7 +131,9 @@ impl Widget for RoomsListHeader { None, ); // Since there is no timeout for fetching media, send an action to ImageViewer when syncing is offline. - cx.action(ImageViewerAction::Show(LoadState::Error(ImageViewerError::Offline))); + cx.action(ImageViewerAction::Show(LoadState::Error( + ImageViewerError::Offline, + ))); } self.sync_state = new_state.clone(); self.redraw(cx); @@ -145,9 +157,21 @@ impl Widget for RoomsListHeader { // Show tooltips for the sync status icons. for (view, text, bg_color) in [ - (self.view.view(cx, ids!(loading_spinner)), "Syncing...", vec4(0.059, 0.533, 0.996, 1.0)), // COLOR_ACTIVE_PRIMARY #0f88fe - (self.view.view(cx, ids!(offline_icon)), "Offline", vec4(0.863, 0.0, 0.020, 1.0)), // COLOR_FG_DANGER_RED #DC0005 - (self.view.view(cx, ids!(synced_icon)), "Fully synced", vec4(0.075, 0.533, 0.031, 1.0)), // COLOR_FG_ACCEPT_GREEN #138808 + ( + self.view.view(cx, ids!(loading_spinner)), + "Syncing...", + vec4(0.059, 0.533, 0.996, 1.0), + ), // COLOR_ACTIVE_PRIMARY #0f88fe + ( + self.view.view(cx, ids!(offline_icon)), + "Offline", + vec4(0.863, 0.0, 0.020, 1.0), + ), // COLOR_FG_DANGER_RED #DC0005 + ( + self.view.view(cx, ids!(synced_icon)), + "Fully synced", + vec4(0.075, 0.533, 0.031, 1.0), + ), // COLOR_FG_ACCEPT_GREEN #138808 ] { if !view.visible() { continue; diff --git a/src/home/rooms_sidebar.rs b/src/home/rooms_sidebar.rs index c50ca5695..ee4fa6087 100644 --- a/src/home/rooms_sidebar.rs +++ b/src/home/rooms_sidebar.rs @@ -35,7 +35,7 @@ script_mod! { Mobile := View { width: Fill, height: Fill flow: Down, - + RoundedShadowView { width: Fill, height: Fit padding: Inset{top: 15, left: 15, right: 15, bottom: 10} @@ -62,7 +62,7 @@ script_mod! { height: 45, flow: Right padding: Inset{top: 5, bottom: 2} - spacing: 5 + spacing: 5 align: Align{y: 0.5} CachedWidget { @@ -93,7 +93,8 @@ script_mod! { /// (because the search bar is at the top of the HomeScreen). #[derive(Script, Widget)] pub struct RoomsSideBar { - #[deref] view: AdaptiveView, + #[deref] + view: AdaptiveView, } impl ScriptHook for RoomsSideBar { diff --git a/src/home/search_messages.rs b/src/home/search_messages.rs index 5228ca129..dd9fe0a0a 100644 --- a/src/home/search_messages.rs +++ b/src/home/search_messages.rs @@ -1,4 +1,3 @@ - //! UI widgets for searching messages in one or more rooms. use makepad_widgets::*; @@ -41,12 +40,13 @@ script_mod! { } } - + } #[derive(Script, ScriptHook, Widget)] pub struct SearchMessagesButton { - #[deref] button: Button, + #[deref] + button: Button, } impl Widget for SearchMessagesButton { diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index 42bca8635..5690d5c68 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -21,10 +21,7 @@ use crate::utils::replace_linebreaks_separators; use crate::{ app::AppStateAction, avatar_cache::{self, AvatarCacheEntry}, - home::{ - invite_modal::InviteModalAction, - rooms_list::RoomsListRef, - }, + home::{invite_modal::InviteModalAction, rooms_list::RoomsListRef}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::BasicRoomDetails, shared::avatar::{AvatarWidgetExt, AvatarWidgetRefExt}, @@ -32,7 +29,6 @@ use crate::{ utils::{self, RoomNameId}, }; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -213,7 +209,7 @@ script_mod! { // Dumb approach, but it works. for i in 0..20 { if f32(i) > self.level { break; } - + if f32(i) < self.level { // Check mask for parent levels let mask_bit = modf(floor(self.parent_mask / pow(2.0, f32(i))), 2.0); @@ -236,7 +232,7 @@ script_mod! { c = vec4(0.8, 0.8, 0.8, 1.0); break; } - + // Vertical line (L shape) if abs(pos.x - (f32(i) * indent + half_indent)) < half_line && pos.y < (self.rect_size.y * (1.0 - 0.5 * self.is_last)) { c = vec4(0.8, 0.8, 0.8, 1.0); @@ -456,20 +452,20 @@ script_mod! { } text: "Welcome to the space:" } - + parent_space_row := View { width: Fill, height: Fit, flow: Right, align: Align{ y: 0.5 } padding: Inset{ top: 8 } - + parent_avatar := Avatar { width: 36, height: 36, margin: Inset{ right: 12 } } - + parent_name := Label { width: Fill, height: Fit, @@ -515,7 +511,6 @@ script_mod! { } } - thread_local! { /// A cache of UI states for each SpaceLobbyScreen, keyed by the space's room ID. /// This allows preserving the expanded/collapsed state of subspaces across screen changes. @@ -531,13 +526,15 @@ struct SpaceLobbyUiState { expanded_spaces: HashSet, } - /// A clickable entry shown in the RoomsList that will show the space lobby when clicked. #[derive(Script, ScriptHook, Widget, Animator)] pub struct SpaceLobbyEntry { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for SpaceLobbyEntry { @@ -567,7 +564,7 @@ impl Widget for SpaceLobbyEntry { Hit::FingerUp(fe) if !fe.is_over => { self.animator_play(cx, ids!(hover.off)); } - Hit::FingerMove(_fe) => { } + Hit::FingerMove(_fe) => {} _ => {} } } @@ -577,7 +574,6 @@ impl Widget for SpaceLobbyEntry { } } - #[derive(Debug)] pub enum SpaceLobbyAction { SpaceLobbyEntryClicked, @@ -586,44 +582,59 @@ pub enum SpaceLobbyAction { #[derive(Script, ScriptHook)] #[repr(C)] pub struct DrawTreeLine { - #[deref] draw_super: DrawQuad, - #[live] indent_width: f32, - #[live] level: f32, - #[live] is_last: f32, - #[live] parent_mask: f32, + #[deref] + draw_super: DrawQuad, + #[live] + indent_width: f32, + #[live] + level: f32, + #[live] + is_last: f32, + #[live] + parent_mask: f32, } #[derive(Script, ScriptHook, Widget)] pub struct TreeLines { - #[uid] uid: WidgetUid, - #[redraw] #[live] draw_bg: DrawTreeLine, - #[walk] walk: Walk, + #[uid] + uid: WidgetUid, + #[redraw] + #[live] + draw_bg: DrawTreeLine, + #[walk] + walk: Walk, } impl Widget for TreeLines { - fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) { } + fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) {} fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { let indent_pixel = (self.draw_bg.level + 1.0) * self.draw_bg.indent_width; let mut walk = walk; walk.width = Size::Fixed(indent_pixel as f64); - + self.draw_bg.draw_walk(cx, walk); DrawStep::done() } } - /// A clickable entry for a child subspace. #[derive(Script, ScriptHook, Widget, Animator)] pub struct SubspaceEntry { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - #[rust] room_id: Option, - #[rust] is_space: bool, - #[rust] show_buttons_view: bool, - #[rust] is_expanded: bool, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + #[rust] + room_id: Option, + #[rust] + is_space: bool, + #[rust] + show_buttons_view: bool, + #[rust] + is_expanded: bool, } /// Actions emitted when a `SubspaceEntry` or its buttons are clicked. @@ -631,11 +642,23 @@ pub struct SubspaceEntry { /// These *are* all widget actions. #[derive(Clone, Debug, Default)] pub enum SubspaceEntryAction { - SpaceClicked { space_id: OwnedRoomId }, - RoomClicked { room_id: OwnedRoomId }, - JoinClicked { room_id: OwnedRoomId, is_space: bool }, - LeaveClicked { room_id: OwnedRoomId, is_space: bool }, - ViewClicked { room_id: OwnedRoomId }, + SpaceClicked { + space_id: OwnedRoomId, + }, + RoomClicked { + room_id: OwnedRoomId, + }, + JoinClicked { + room_id: OwnedRoomId, + is_space: bool, + }, + LeaveClicked { + room_id: OwnedRoomId, + is_space: bool, + }, + ViewClicked { + room_id: OwnedRoomId, + }, #[default] None, } @@ -666,7 +689,9 @@ impl Widget for SubspaceEntry { self.animator_play(cx, ids!(hover.on)); if !self.show_buttons_view { self.show_buttons_view = true; - self.view.child_by_path(ids!(buttons_view)).set_visible(cx, true); + self.view + .child_by_path(ids!(buttons_view)) + .set_visible(cx, true); self.redraw(cx); } } @@ -675,7 +700,9 @@ impl Widget for SubspaceEntry { Hit::FingerHoverOver(_) if !self.show_buttons_view => { self.animator_play(cx, ids!(hover.on)); self.show_buttons_view = true; - self.view.child_by_path(ids!(buttons_view)).set_visible(cx, true); + self.view + .child_by_path(ids!(buttons_view)) + .set_visible(cx, true); self.redraw(cx); } Hit::FingerHoverOut(fe) => { @@ -683,11 +710,14 @@ impl Widget for SubspaceEntry { // Makepad emits a HoverOut hit, but we don't want that to actually count as a hover-out // because the mouse is still hovering over the buttons_view. let entry_rect = self.view.area().rect(cx); - let is_over_buttons_view = self.show_buttons_view && buttons_view_rect.contains(fe.abs); + let is_over_buttons_view = + self.show_buttons_view && buttons_view_rect.contains(fe.abs); if !entry_rect.contains(fe.abs) && !is_over_buttons_view { self.animator_play(cx, ids!(hover.off)); self.show_buttons_view = false; - self.view.child_by_path(ids!(buttons_view)).set_visible(cx, false); + self.view + .child_by_path(ids!(buttons_view)) + .set_visible(cx, false); self.redraw(cx); } } @@ -696,23 +726,36 @@ impl Widget for SubspaceEntry { } Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { let is_within_buttons_view = self.show_buttons_view - && self.view.child_by_path(ids!(buttons_view)).area().rect(cx).contains(fe.abs); + && self + .view + .child_by_path(ids!(buttons_view)) + .area() + .rect(cx) + .contains(fe.abs); if !is_within_buttons_view { if let Some(room_id) = self.room_id.as_ref() { if self.is_space { // Toggle expansion and animate the arrow self.is_expanded = !self.is_expanded; - if let Some(mut arrow) = self.view.child_by_path(ids!(expand_icon)).borrow_mut::() { + if let Some(mut arrow) = self + .view + .child_by_path(ids!(expand_icon)) + .borrow_mut::() + { arrow.set_is_open(cx, self.is_expanded, Animate::Yes); } cx.widget_action( self.widget_uid(), - SubspaceEntryAction::SpaceClicked { space_id: room_id.clone() }, + SubspaceEntryAction::SpaceClicked { + space_id: room_id.clone(), + }, ); } else { cx.widget_action( self.widget_uid(), - SubspaceEntryAction::RoomClicked { room_id: room_id.clone() }, + SubspaceEntryAction::RoomClicked { + room_id: room_id.clone(), + }, ); } } @@ -724,16 +767,28 @@ impl Widget for SubspaceEntry { self.view.handle_event(cx, event, scope); if let Event::Actions(actions) = event { - let join_button = self.view.child_by_path(ids!(buttons_view.join_button)).as_button(); - let leave_button = self.view.child_by_path(ids!(buttons_view.leave_button)).as_button(); - let view_button = self.view.child_by_path(ids!(buttons_view.view_button)).as_button(); + let join_button = self + .view + .child_by_path(ids!(buttons_view.join_button)) + .as_button(); + let leave_button = self + .view + .child_by_path(ids!(buttons_view.leave_button)) + .as_button(); + let view_button = self + .view + .child_by_path(ids!(buttons_view.view_button)) + .as_button(); if join_button.clicked(actions) { if let Some(room_id) = self.room_id.clone() { join_button.reset_hover(cx); cx.widget_action( self.widget_uid(), - SubspaceEntryAction::JoinClicked { room_id, is_space: self.is_space }, + SubspaceEntryAction::JoinClicked { + room_id, + is_space: self.is_space, + }, ); } } @@ -742,7 +797,10 @@ impl Widget for SubspaceEntry { leave_button.reset_hover(cx); cx.widget_action( self.widget_uid(), - SubspaceEntryAction::LeaveClicked { room_id, is_space: self.is_space }, + SubspaceEntryAction::LeaveClicked { + room_id, + is_space: self.is_space, + }, ); } } @@ -787,9 +845,10 @@ impl From<&SpaceRoom> for SpaceRoomInfo { SpaceRoomInfo { id: space_room.room_id.clone(), name: space_room.display_name.clone(), - topic: space_room.topic.as_ref().map(|t| { - replace_linebreaks_separators(t.trim(), false).into_owned() - }), + topic: space_room + .topic + .as_ref() + .map(|t| replace_linebreaks_separators(t.trim(), false).into_owned()), avatar: AvatarState::Known(space_room.avatar_url.clone()), num_joined_members: space_room.num_joined_members, state: space_room.state, @@ -804,9 +863,9 @@ impl From for SpaceRoomInfo { children_count: space_room.is_space().then_some(space_room.children_count), id: space_room.room_id, name: space_room.display_name, - topic: space_room.topic.map(|t| { - replace_linebreaks_separators(t.trim(), false).into_owned() - }), + topic: space_room + .topic + .map(|t| replace_linebreaks_separators(t.trim(), false).into_owned()), avatar: AvatarState::Known(space_room.avatar_url), num_joined_members: space_room.num_joined_members, state: space_room.state, @@ -841,31 +900,41 @@ enum TreeEntry { /// The view showing the lobby/homepage for a given space. #[derive(Script, ScriptHook, Widget)] pub struct SpaceLobbyScreen { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, /// The space that is currently being displayed. - #[rust] space_name_id: Option, - #[rust] space_avatar_state: AvatarState, + #[rust] + space_name_id: Option, + #[rust] + space_avatar_state: AvatarState, /// The sender channel to submit space requests to the background service. - #[rust] space_request_sender: Option>, + #[rust] + space_request_sender: Option>, /// Cache of detailed children for each space we've fetched. /// Key is the space_id, value is the list of its direct children. - #[rust] children_cache: HashMap>, + #[rust] + children_cache: HashMap>, /// The set of space IDs that are currently expanded (showing their children). - #[rust] expanded_spaces: HashSet, + #[rust] + expanded_spaces: HashSet, /// The ordered list of children to display in the space tree. - #[rust] tree_entries: Vec, + #[rust] + tree_entries: Vec, /// The set of space IDs that are currently loading their children. - #[rust] loading_subspaces: HashSet, + #[rust] + loading_subspaces: HashSet, /// Whether we are currently loading the initial data. - #[rust] is_loading: bool, + #[rust] + is_loading: bool, } impl Widget for SpaceLobbyScreen { @@ -882,34 +951,53 @@ impl Widget for SpaceLobbyScreen { if let Event::Actions(actions) = event { for action in actions { match action.downcast_ref() { - Some(SpaceRoomListAction::DetailedChildren { space_id, children, .. }) => { + Some(SpaceRoomListAction::DetailedChildren { + space_id, children, .. + }) => { self.update_children_in_space(cx, space_id, children); } // Handle receiving top-level space details (join rule, member count). Some(SpaceRoomListAction::TopLevelSpaceDetails(sr)) => { - if self.space_name_id.as_ref().is_some_and(|sni| sni.room_id() == &sr.room_id) { + if self + .space_name_id + .as_ref() + .is_some_and(|sni| sni.room_id() == &sr.room_id) + { self.space_avatar_state = AvatarState::Known(sr.avatar_url.clone()); self.space_avatar_state.update_from_cache(cx); // prefetch the avatar image - self.view.label(cx, ids!(header.space_info_label)).set_text(cx, &format!( - "{} · {} {}", - match sr.join_rule { - Some(JoinRuleSummary::Public) => "🌐 Public space", - _ => "🔒 Private space", - }, - sr.num_joined_members, - if sr.num_joined_members == 1 { "member" } else { "members" } - )); + self.view.label(cx, ids!(header.space_info_label)).set_text( + cx, + &format!( + "{} · {} {}", + match sr.join_rule { + Some(JoinRuleSummary::Public) => "🌐 Public space", + _ => "🔒 Private space", + }, + sr.num_joined_members, + if sr.num_joined_members == 1 { + "member" + } else { + "members" + } + ), + ); self.redraw(cx); } } // Handle a change to the set of children in this space or any of its child subspaces. - Some(SpaceRoomListAction::UpdatedChildren { space_id, parent_chain, .. }) => { - if self.space_name_id.as_ref().is_some_and(|sni| + Some(SpaceRoomListAction::UpdatedChildren { + space_id, + parent_chain, + .. + }) => { + if self.space_name_id.as_ref().is_some_and(|sni| { sni.room_id() == space_id - || parent_chain.iter().any(|ancestor_id| sni.room_id() == ancestor_id) - ) { + || parent_chain + .iter() + .any(|ancestor_id| sni.room_id() == ancestor_id) + }) { if let Some(sender) = &self.space_request_sender { let _ = sender.send(SpaceRequest::GetDetailedChildren { space_id: space_id.clone(), @@ -918,7 +1006,7 @@ impl Widget for SpaceLobbyScreen { } } } - _ => { } + _ => {} } // Handle SubspaceEntry clicks @@ -953,7 +1041,7 @@ impl Widget for SpaceLobbyScreen { } else { cx.action(JoinLeaveRoomModalAction::Open { kind: JoinLeaveModalKind::LeaveRoom( - self.basic_room_details_for(room_id) + self.basic_room_details_for(room_id), ), show_tip: false, }); @@ -965,12 +1053,16 @@ impl Widget for SpaceLobbyScreen { destination_room: self.basic_room_details_for(room_id), }); } - SubspaceEntryAction::None => { } + SubspaceEntryAction::None => {} } } // Handle the invite button being clicked in the header. - if self.view.button(cx, ids!(header.parent_space_row.invite_button)).clicked(actions) { + if self + .view + .button(cx, ids!(header.parent_space_row.invite_button)) + .clicked(actions) + { if let Some(space_name_id) = self.space_name_id.as_ref() { cx.action(InviteModalAction::Open(space_name_id.clone())); } @@ -981,21 +1073,28 @@ impl Widget for SpaceLobbyScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // Draw parent avatar from the SpaceRoom's avatar URL, or show initials. let parent_avatar_ref = self.view.avatar(cx, ids!(parent_avatar)); - if self.space_avatar_state.update_from_cache(cx).is_none_or(|data| { - parent_avatar_ref.show_image( - cx, - None, - |cx, img| utils::load_png_or_jpg(&img, cx, data), - ).is_err() - }) { - let first_char = self.space_name_id.as_ref().and_then(|sni| sni.name_for_avatar()) + if self + .space_avatar_state + .update_from_cache(cx) + .is_none_or(|data| { + parent_avatar_ref + .show_image(cx, None, |cx, img| utils::load_png_or_jpg(&img, cx, data)) + .is_err() + }) + { + let first_char = self + .space_name_id + .as_ref() + .and_then(|sni| sni.name_for_avatar()) .and_then(|name| utils::user_name_first_letter(name)); parent_avatar_ref.show_text(cx, None, None, first_char.unwrap_or("S")); } - + while let Some(widget_to_draw) = self.view.draw_walk(cx, scope, walk).step() { let portal_list_ref = widget_to_draw.as_portal_list(); - let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + let Some(mut list) = portal_list_ref.borrow_mut() else { + continue; + }; let entry_count = self.tree_entries.len(); let total_count = if self.is_loading || entry_count == 0 { @@ -1014,20 +1113,30 @@ impl Widget for SpaceLobbyScreen { // Draw loading indicator let item = if self.is_loading && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); - item.child_by_path(ids!(label)).as_label().set_text(cx, "Loading rooms and spaces..."); + item.child_by_path(ids!(label)) + .as_label() + .set_text(cx, "Loading rooms and spaces..."); item } // No entries found else if entry_count == 0 && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); - item.child_by_path(ids!(label)).as_label().set_text(cx, "No rooms or spaces found."); - item.child_by_path(ids!(loading_spinner)).set_visible(cx, false); + item.child_by_path(ids!(label)) + .as_label() + .set_text(cx, "No rooms or spaces found."); + item.child_by_path(ids!(loading_spinner)) + .set_visible(cx, false); item } // Draw a regular entry else if let Some(entry) = self.tree_entries.get_mut(item_id) { match entry { - TreeEntry::Item { info, level, is_last, parent_mask } => { + TreeEntry::Item { + info, + level, + is_last, + parent_mask, + } => { let show_join_button = !matches!(info.state, Some(RoomState::Joined)); let show_leave_button = !show_join_button; let show_view_button = show_leave_button && !info.is_space(); @@ -1047,11 +1156,15 @@ impl Widget for SpaceLobbyScreen { } show_buttons_view = inner.show_buttons_view; } - item.child_by_path(ids!(buttons_view)).set_visible(cx, show_buttons_view); + item.child_by_path(ids!(buttons_view)) + .set_visible(cx, show_buttons_view); // Snap expand arrow to correct state without animation // when item is reused or state changed externally if need_snap { - if let Some(mut arrow) = item.child_by_path(ids!(expand_icon)).borrow_mut::() { + if let Some(mut arrow) = item + .child_by_path(ids!(expand_icon)) + .borrow_mut::() + { arrow.set_is_open(cx, is_expanded, Animate::No); } } @@ -1068,16 +1181,22 @@ impl Widget for SpaceLobbyScreen { } show_buttons_view = inner.show_buttons_view; } - item.child_by_path(ids!(buttons_view)).set_visible(cx, show_buttons_view); + item.child_by_path(ids!(buttons_view)) + .set_visible(cx, show_buttons_view); item }; - item.child_by_path(ids!(buttons_view.join_button)).set_visible(cx, show_join_button); - item.child_by_path(ids!(buttons_view.leave_button)).set_visible(cx, show_leave_button); - item.child_by_path(ids!(buttons_view.view_button)).set_visible(cx, show_view_button); + item.child_by_path(ids!(buttons_view.join_button)) + .set_visible(cx, show_join_button); + item.child_by_path(ids!(buttons_view.leave_button)) + .set_visible(cx, show_leave_button); + item.child_by_path(ids!(buttons_view.view_button)) + .set_visible(cx, show_view_button); // Below, draw things that are common to child rooms and subspaces. - item.child_by_path(ids!(content.name_label)).as_label().set_text(cx, &info.name); + item.child_by_path(ids!(content.name_label)) + .as_label() + .set_text(cx, &info.name); // Display avatar from stored data, or fetch from cache, or show initials let avatar_ref = item.child_by_path(ids!(avatar)).as_avatar(); @@ -1086,36 +1205,39 @@ impl Widget for SpaceLobbyScreen { match &info.avatar { AvatarState::Loaded(data) => { - drew_avatar = avatar_ref.show_image( - cx, - None, - |cx, img| utils::load_png_or_jpg(&img, cx, data), - ).is_ok(); + drew_avatar = avatar_ref + .show_image(cx, None, |cx, img| { + utils::load_png_or_jpg(&img, cx, data) + }) + .is_ok(); } AvatarState::Known(Some(uri)) => { match avatar_cache::get_or_fetch_avatar(cx, uri) { AvatarCacheEntry::Loaded(data) => { - drew_avatar = avatar_ref.show_image( - cx, - None, - |cx, img| utils::load_png_or_jpg(&img, cx, &data), - ).is_ok(); + drew_avatar = avatar_ref + .show_image(cx, None, |cx, img| { + utils::load_png_or_jpg(&img, cx, &data) + }) + .is_ok(); info.avatar = AvatarState::Loaded(data); } AvatarCacheEntry::Failed => { info.avatar = AvatarState::Failed; } - AvatarCacheEntry::Requested => { } + AvatarCacheEntry::Requested => {} } } - _ => { } + _ => {} }; // Fallback to text initials. if !drew_avatar { avatar_ref.show_text(cx, None, None, first_char.unwrap_or("#")); } - if let Some(mut lines) = item.child_by_path(ids!(tree_lines)).borrow_mut::() { + if let Some(mut lines) = item + .child_by_path(ids!(tree_lines)) + .borrow_mut::() + { lines.draw_bg.level = *level as f32; lines.draw_bg.is_last = if *is_last { 1.0 } else { 0.0 }; lines.draw_bg.parent_mask = *parent_mask as f32; @@ -1124,7 +1246,8 @@ impl Widget for SpaceLobbyScreen { // Build the info label with join status, member count, and topic // Note: Public/Private is intentionally not shown per-item to reduce clutter - let info_label = item.child_by_path(ids!(content.info_label)).as_label(); + let info_label = + item.child_by_path(ids!(content.info_label)).as_label(); let mut info_parts = Vec::new(); // Add join status for rooms we haven't joined @@ -1142,7 +1265,11 @@ impl Widget for SpaceLobbyScreen { info_parts.push(format!( "{} {}", info.num_joined_members, - if info.num_joined_members == 1 { "member" } else { "members" } + if info.num_joined_members == 1 { + "member" + } else { + "members" + } )); // Add children count for spaces @@ -1169,7 +1296,10 @@ impl Widget for SpaceLobbyScreen { // Draw loading indicator for subspace let item = list.item(cx, item_id, id!(subspace_loading)); // Configure tree lines - if let Some(mut lines) = item.child_by_path(ids!(tree_lines)).borrow_mut::() { + if let Some(mut lines) = item + .child_by_path(ids!(tree_lines)) + .borrow_mut::() + { lines.draw_bg.level = *level as f32; lines.draw_bg.is_last = 1.0; lines.draw_bg.parent_mask = *parent_mask as f32; @@ -1203,12 +1333,22 @@ impl SpaceLobbyScreen { } /// Handle receiving detailed children for a space. - fn update_children_in_space(&mut self, cx: &mut Cx, space_id: &OwnedRoomId, children: &Vector) { - self.children_cache.insert(space_id.clone(), children.clone()); + fn update_children_in_space( + &mut self, + cx: &mut Cx, + space_id: &OwnedRoomId, + children: &Vector, + ) { + self.children_cache + .insert(space_id.clone(), children.clone()); self.loading_subspaces.remove(space_id); // If this is for our displayed space, mark as loaded and rebuild tree - if self.space_name_id.as_ref().is_some_and(|sni| sni.room_id() == space_id) { + if self + .space_name_id + .as_ref() + .is_some_and(|sni| sni.room_id() == space_id) + { self.is_loading = false; // Auto-expand the top-level space (we don't show it, just its children) self.expanded_spaces.insert(space_id.clone()); @@ -1230,7 +1370,8 @@ impl SpaceLobbyScreen { if !self.children_cache.contains_key(space_id) { self.loading_subspaces.insert(space_id.clone()); if let Some(sender) = &self.space_request_sender { - let parent_chain = cx.get_global::() + let parent_chain = cx + .get_global::() .get_space_parent_chain(space_id) .unwrap_or_default(); let _ = sender.send(SpaceRequest::GetDetailedChildren { @@ -1247,7 +1388,9 @@ impl SpaceLobbyScreen { /// Rebuild the flattened tree entries based on the current expansion state. fn rebuild_tree_entries(&mut self) { - let Some(space_name_id) = &self.space_name_id else { return }; + let Some(space_name_id) = &self.space_name_id else { + return; + }; let root_space_id = space_name_id.room_id().clone(); // Build tree starting from root let mut new_tree_entries = Vec::new(); @@ -1278,23 +1421,25 @@ impl SpaceLobbyScreen { level: usize, parent_mask: u32, ) { - let Some(children) = children_cache.get(space_id) else { return }; + let Some(children) = children_cache.get(space_id) else { + return; + }; // Sort: spaces first, then rooms, both alphabetically let mut sorted_children: Vec<_> = children.iter().collect(); - sorted_children.sort_by(|a, b| { - match (a.is_space(), b.is_space()) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()), - } + sorted_children.sort_by(|a, b| match (a.is_space(), b.is_space()) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a + .display_name + .to_lowercase() + .cmp(&b.display_name.to_lowercase()), }); - let count = sorted_children.len(); for (i, child) in sorted_children.into_iter().enumerate() { let is_last = i == count - 1; - + tree_entries.push(TreeEntry::Item { info: SpaceRoomInfo::from(child), level, @@ -1326,7 +1471,7 @@ impl SpaceLobbyScreen { ); } else if loading_subspaces.contains(&child.room_id) { // Show loading indicator - tree_entries.push(TreeEntry::Loading { + tree_entries.push(TreeEntry::Loading { level: level + 1, parent_mask: child_mask, }); @@ -1351,12 +1496,18 @@ impl SpaceLobbyScreen { pub fn set_displayed_space(&mut self, cx: &mut Cx, space_name_id: &RoomNameId) { let space_name = space_name_id.to_string(); - let parent_name = self.view.label(cx, ids!(header.parent_space_row.parent_name)); + let parent_name = self + .view + .label(cx, ids!(header.parent_space_row.parent_name)); parent_name.set_text(cx, &space_name); // If this space is already being displayed, then the only thing we may need to do // is update its name in the top-level header (already done above). - if self.space_name_id.as_ref().is_some_and(|sni| sni.room_id() == space_name_id.room_id()) { + if self + .space_name_id + .as_ref() + .is_some_and(|sni| sni.room_id() == space_name_id.room_id()) + { return; } @@ -1380,7 +1531,9 @@ impl SpaceLobbyScreen { // Clear the main content until we receive the async space info responses. self.tree_entries.clear(); - self.view.label(cx, ids!(header.space_info_label)).set_text(cx, ""); + self.view + .label(cx, ids!(header.space_info_label)) + .set_text(cx, ""); self.is_loading = true; // Restore UI state if we've viewed this space before, otherwise start fresh @@ -1393,7 +1546,9 @@ impl SpaceLobbyScreen { // TODO: move avatar setting to `draw_walk()` // Set parent avatar - let avatar_ref = self.view.avatar(cx, ids!(header.parent_space_row.parent_avatar)); + let avatar_ref = self + .view + .avatar(cx, ids!(header.parent_space_row.parent_avatar)); let first_char = utils::user_name_first_letter(&space_name); avatar_ref.show_text(cx, None, None, first_char.unwrap_or("#")); @@ -1403,13 +1558,17 @@ impl SpaceLobbyScreen { impl SpaceLobbyScreenRef { pub fn set_displayed_space(&self, cx: &mut Cx, space_name_id: &RoomNameId) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_displayed_space(cx, space_name_id); } /// Saves the current UI state. Call this when the screen is being hidden or destroyed. pub fn save_current_state(&self) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.save_current_state(); } } diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 8f613dc93..201491054 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -13,7 +13,13 @@ use matrix_sdk::{RoomDisplayName, RoomState}; use ruma::{OwnedRoomAliasId, OwnedRoomId, room::JoinRuleSummary}; use crate::{ - home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, utils::{self, RoomNameId} + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room::{ + FetchedRoomAvatar, + room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}, + }, + shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, + utils::{self, RoomNameId}, }; script_mod! { @@ -197,7 +203,7 @@ script_mod! { width: Fill, spacing: 0.0 - auto_tail: false, + auto_tail: false, max_pull_down: 0.0, scroll_bar: ScrollBar { // hide the scroll bar bar_size: 0.0, @@ -216,7 +222,7 @@ script_mod! { Desktop := View { align: Align{x: 0.5, y: 0.5} padding: 0, - width: (NAVIGATION_TAB_BAR_SIZE), + width: (NAVIGATION_TAB_BAR_SIZE), height: Fill CachedWidget { @@ -237,7 +243,6 @@ script_mod! { } } - /// Actions emitted by and handled by the SpacesBar widget (and its children). #[derive(Clone, Debug, Default)] pub enum SpacesBarAction { @@ -249,14 +254,17 @@ pub enum SpacesBarAction { None, } - #[derive(Script, ScriptHook, Widget, Animator)] pub struct SpacesBarEntry { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - - #[rust] space_name_id: Option, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + + #[rust] + space_name_id: Option, } impl Widget for SpacesBarEntry { @@ -269,13 +277,13 @@ impl Widget for SpacesBarEntry { let emit_hover_in_action = |this: &Self, cx: &mut Cx| { let is_desktop = cx.display_context.is_desktop(); cx.widget_action( - this.widget_uid(), + this.widget_uid(), TooltipAction::HoverIn { widget_rect: area.rect(cx), - text: this.space_name_id.as_ref().map_or( - String::from("Unknown Space Name"), - |sni| sni.to_string(), - ), + text: this + .space_name_id + .as_ref() + .map_or(String::from("Unknown Space Name"), |sni| sni.to_string()), options: CalloutTooltipOptions { position: if is_desktop { TooltipPosition::Right @@ -295,17 +303,14 @@ impl Widget for SpacesBarEntry { } Hit::FingerHoverOut(_) => { self.animator_play(cx, ids!(hover.off)); - cx.widget_action( - self.widget_uid(), - TooltipAction::HoverOut, - ); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); } Hit::FingerDown(fe) => { self.animator_play(cx, ids!(hover.down)); if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { if let Some(space_name_id) = self.space_name_id.clone() { cx.widget_action( - self.widget_uid(), + self.widget_uid(), SpacesBarAction::ButtonSecondaryClicked { space_name_id }, ); } @@ -316,7 +321,7 @@ impl Widget for SpacesBarEntry { emit_hover_in_action(self, cx); if let Some(space_name_id) = self.space_name_id.clone() { cx.widget_action( - self.widget_uid(), + self.widget_uid(), SpacesBarAction::ButtonSecondaryClicked { space_name_id }, ); } @@ -325,7 +330,7 @@ impl Widget for SpacesBarEntry { self.animator_play(cx, ids!(hover.on)); if let Some(space_name_id) = self.space_name_id.clone() { cx.widget_action( - self.widget_uid(), + self.widget_uid(), SpacesBarAction::ButtonClicked { space_name_id }, ); } @@ -336,7 +341,7 @@ impl Widget for SpacesBarEntry { _ => {} } } - + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { self.view.draw_walk(cx, scope, walk) } @@ -345,12 +350,20 @@ impl Widget for SpacesBarEntry { impl SpacesBarEntry { fn set_metadata(&mut self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool) { self.space_name_id = Some(space_name_id); - self.animator_toggle(cx, is_selected, Animate::No, ids!(active.on), ids!(active.off)); + self.animator_toggle( + cx, + is_selected, + Animate::No, + ids!(active.on), + ids!(active.off), + ); } } impl SpacesBarEntryRef { pub fn set_metadata(&self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_metadata(cx, space_name_id, is_selected); } } @@ -376,8 +389,6 @@ pub struct JoinedSpaceInfo { pub children_count: u64, } - - /// The possible updates that should be displayed by the single list of all spaces. /// /// These updates are enqueued by the `enqueue_spaces_list_update` function @@ -443,7 +454,6 @@ pub enum SpacesListUpdate { ScrollToSpace(OwnedRoomId), } - static PENDING_SPACE_UPDATES: SegQueue = SegQueue::new(); /// Enqueue a new room update for the list of all spaces @@ -453,37 +463,42 @@ pub fn enqueue_spaces_list_update(update: SpacesListUpdate) { SignalToUI::set_ui_signal(); } - /// The tab bar with buttons that navigate through top-level app pages. /// /// * In the "desktop" (wide) layout, this is a vertical bar on the left. /// * In the "mobile" (narrow) layout, this is a horizontal bar on the bottom. #[derive(Script, ScriptHook, Widget)] pub struct SpacesBar { - #[deref] view: AdaptiveView, + #[deref] + view: AdaptiveView, /// The set of all joined spaces, keyed by the space ID. - #[rust] all_joined_spaces: HashMap, + #[rust] + all_joined_spaces: HashMap, /// The currently-active filter function for the list of spaces. /// /// Note: for performance reasons, this does not get automatically applied /// when its value changes. Instead, you must manually invoke it on the set of `all_joined_spaces` /// in order to update the set of `displayed_spaces` accordingly. - #[rust] display_filter: RoomDisplayFilter, + #[rust] + display_filter: RoomDisplayFilter, /// The list of spaces currently displayed in the UI, in order from top to bottom. /// This is a strict subset of the rooms in `all_joined_spaces`, and should be determined /// by applying the `display_filter` to the set of `all_joined_spaces`. - #[rust] displayed_spaces: Vec, + #[rust] + displayed_spaces: Vec, /// Whether the list of `displayed_spaces` is currently filtered: /// `true` if filtered, `false` if showing everything. - #[rust] is_filtered: bool, + #[rust] + is_filtered: bool, /// The ID of the currently-selected space in this SpacesBar. /// Only one space can be selected at once. - #[rust] selected_space: Option, + #[rust] + selected_space: Option, } impl Widget for SpacesBar { @@ -504,7 +519,9 @@ impl Widget for SpacesBar { } // Update which space is currently selected. - if let SpacesBarAction::ButtonClicked { space_name_id } = action.as_widget_action().cast() { + if let SpacesBarAction::ButtonClicked { space_name_id } = + action.as_widget_action().cast() + { self.selected_space = Some(space_name_id.room_id().clone()); self.redraw(cx); cx.action(NavigationBarAction::GoToSpace { space_name_id }); @@ -534,7 +551,9 @@ impl Widget for SpacesBar { while let Some(widget_to_draw) = self.view.draw_walk(cx, scope, walk).step() { // We only care about drawing the portal list. let portal_list_ref = widget_to_draw.as_portal_list(); - let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + let Some(mut list) = portal_list_ref.borrow_mut() else { + continue; + }; // AdaptiveView + CachedWidget does not properly handle DSL-level style overrides, // so we must manually apply the different style choices here when drawing it. @@ -560,7 +579,7 @@ impl Widget for SpacesBar { "Found no\nmatching spaces." } else { "Found no\njoined spaces." - } + }, ); item } else { @@ -568,11 +587,11 @@ impl Widget for SpacesBar { }; item.draw_all(cx, scope); } - } - else { + } else { list.set_item_range(cx, 0, len + 1); while let Some(portal_list_index) = list.next_visible_item(cx) { - let item = if let Some(space) = self.displayed_spaces + let item = if let Some(space) = self + .displayed_spaces .get(portal_list_index) .and_then(|space_id| self.all_joined_spaces.get(space_id)) { @@ -586,41 +605,38 @@ impl Widget for SpacesBar { avatar_ref.show_text(cx, None, None, text); } FetchedRoomAvatar::Image(image_data) => { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| utils::load_png_or_jpg(&img_ref, cx, image_data), - ); + let res = avatar_ref.show_image(cx, None, |cx, img_ref| { + utils::load_png_or_jpg(&img_ref, cx, image_data) + }); if res.is_err() { - avatar_ref.show_text( - cx, - None, - None, - &space_name, - ); + avatar_ref.show_text(cx, None, None, &space_name); } } } item.as_spaces_bar_entry().set_metadata( cx, space.space_name_id.clone(), - self.selected_space.as_ref().is_some_and(|id| id == space.space_name_id.room_id()), + self.selected_space + .as_ref() + .is_some_and(|id| id == space.space_name_id.room_id()), ); item - } - else if portal_list_index == len { + } else if portal_list_index == len { let item = list.item(cx, portal_list_index, id!(StatusLabel)); - let descriptor = if self.is_filtered { "matching" } else { "joined" }; + let descriptor = if self.is_filtered { + "matching" + } else { + "joined" + }; let text = match len { - 0 => format!("Found no\n{descriptor} spaces."), - 1 => format!("Found 1\n{descriptor} space."), + 0 => format!("Found no\n{descriptor} spaces."), + 1 => format!("Found 1\n{descriptor} space."), 2..100 => format!("Found {len}\n{descriptor} spaces."), - 100.. => format!("Found 99+\n{descriptor} spaces."), + 100.. => format!("Found 99+\n{descriptor} spaces."), }; item.label(cx, ids!(label)).set_text(cx, &text); item - } - else { + } else { list.item(cx, portal_list_index, id!(BottomFiller)) }; item.draw_all(cx, scope); @@ -633,9 +649,8 @@ impl Widget for SpacesBar { } impl SpacesBar { - /// Handle all pending updates to the spaces list. + /// Handle all pending updates to the spaces list. fn handle_spaces_list_updates(&mut self, cx: &mut Cx, _event: &Event, _scope: &mut Scope) { - fn adjust_displayed_spaces( was_displayed: bool, should_display: bool, @@ -644,10 +659,11 @@ impl SpacesBar { ) { match (was_displayed, should_display) { // No need to update anything - (true, true) | (false, false) => { } + (true, true) | (false, false) => {} // Space was displayed but should no longer be displayed. (true, false) => { - displayed_spaces.iter() + displayed_spaces + .iter() .position(|s| s == &space_id) .map(|index| displayed_spaces.remove(index)); } @@ -658,7 +674,6 @@ impl SpacesBar { } } - let mut num_updates: usize = 0; while let Some(update) = PENDING_SPACE_UPDATES.pop() { num_updates += 1; @@ -666,26 +681,46 @@ impl SpacesBar { SpacesListUpdate::AddJoinedSpace(joined_space) => { let space_id = joined_space.space_name_id.room_id().clone(); let should_display = (self.display_filter)(&joined_space); - let replaced = self.all_joined_spaces.insert(space_id.clone(), joined_space); + let replaced = self + .all_joined_spaces + .insert(space_id.clone(), joined_space); if replaced.is_none() { - adjust_displayed_spaces(false, should_display, space_id, &mut self.displayed_spaces); + adjust_displayed_spaces( + false, + should_display, + space_id, + &mut self.displayed_spaces, + ); } else { error!("BUG: Added joined space {space_id} that already existed"); } } - SpacesListUpdate::UpdateCanonicalAlias { space_id, new_canonical_alias } => { + SpacesListUpdate::UpdateCanonicalAlias { + space_id, + new_canonical_alias, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { let was_displayed = (self.display_filter)(space); space.canonical_alias = new_canonical_alias; let should_display = (self.display_filter)(space); - adjust_displayed_spaces(was_displayed, should_display, space_id, &mut self.displayed_spaces); + adjust_displayed_spaces( + was_displayed, + should_display, + space_id, + &mut self.displayed_spaces, + ); } else { - error!("Error: couldn't find space {space_id} to update space canonical alias"); + error!( + "Error: couldn't find space {space_id} to update space canonical alias" + ); } } - SpacesListUpdate::UpdateSpaceName { space_id, new_space_name } => { + SpacesListUpdate::UpdateSpaceName { + space_id, + new_space_name, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { let was_displayed = (self.display_filter)(space); space.space_name_id = RoomNameId::new( @@ -693,7 +728,12 @@ impl SpacesBar { space_id.clone(), ); let should_display = (self.display_filter)(space); - adjust_displayed_spaces(was_displayed, should_display, space_id, &mut self.displayed_spaces); + adjust_displayed_spaces( + was_displayed, + should_display, + space_id, + &mut self.displayed_spaces, + ); } else { error!("Error: couldn't find space {space_id} to update space name"); } @@ -719,15 +759,23 @@ impl SpacesBar { } } - SpacesListUpdate::UpdateNumJoinedMembers { space_id, num_joined_members } => { + SpacesListUpdate::UpdateNumJoinedMembers { + space_id, + num_joined_members, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { space.num_joined_members = num_joined_members; } else { - error!("Error: couldn't find space {space_id} to update space num_joined_members"); + error!( + "Error: couldn't find space {space_id} to update space num_joined_members" + ); } } - SpacesListUpdate::UpdateJoinRule { space_id, join_rule } => { + SpacesListUpdate::UpdateJoinRule { + space_id, + join_rule, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { space.join_rule = join_rule; } else { @@ -735,27 +783,42 @@ impl SpacesBar { } } - SpacesListUpdate::UpdateWorldReadable { space_id, world_readable } => { + SpacesListUpdate::UpdateWorldReadable { + space_id, + world_readable, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { space.world_readable = world_readable; } else { - error!("Error: couldn't find space {space_id} to update space world_readable"); + error!( + "Error: couldn't find space {space_id} to update space world_readable" + ); } } - SpacesListUpdate::UpdateGuestCanJoin { space_id, guest_can_join } => { + SpacesListUpdate::UpdateGuestCanJoin { + space_id, + guest_can_join, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { space.guest_can_join = guest_can_join; } else { - error!("Error: couldn't find space {space_id} to update space guest_can_join"); + error!( + "Error: couldn't find space {space_id} to update space guest_can_join" + ); } } - SpacesListUpdate::UpdateChildrenCount { space_id, children_count } => { + SpacesListUpdate::UpdateChildrenCount { + space_id, + children_count, + } => { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { space.children_count = children_count; } else { - error!("Error: couldn't find space {space_id} to update space children_count"); + error!( + "Error: couldn't find space {space_id} to update space children_count" + ); } } @@ -784,7 +847,6 @@ impl SpacesBar { } } - /// Updates the lists of displayed spaces based on the current search filter. fn update_displayed_spaces(&mut self, cx: &mut Cx, keywords: &str) { let portal_list = self.view.portal_list(cx, ids!(spaces_list)); @@ -807,18 +869,22 @@ impl SpacesBar { self.display_filter = filter; self.is_filtered = true; - let filtered_spaces_iter = self.all_joined_spaces.iter() + let filtered_spaces_iter = self + .all_joined_spaces + .iter() .filter(|(_, space)| (self.display_filter)(*space)); self.displayed_spaces = if let Some(sort_fn) = sort_fn { - let mut filtered_spaces = filtered_spaces_iter - .collect::>(); + let mut filtered_spaces = filtered_spaces_iter.collect::>(); filtered_spaces.sort_by(|(_, space_a), (_, space_b)| sort_fn(*space_a, *space_b)); filtered_spaces .into_iter() - .map(|(space_id, _)| space_id.clone()).collect() + .map(|(space_id, _)| space_id.clone()) + .collect() } else { - filtered_spaces_iter.map(|(space_id, _)| space_id.clone()).collect() + filtered_spaces_iter + .map(|(space_id, _)| space_id.clone()) + .collect() }; portal_list.set_first_id_and_scroll(0, 0.0); diff --git a/src/home/tombstone_footer.rs b/src/home/tombstone_footer.rs index 2383cb180..12823a950 100644 --- a/src/home/tombstone_footer.rs +++ b/src/home/tombstone_footer.rs @@ -5,11 +5,14 @@ //! the option to join the successor room or stay in the current tombstoned room. use makepad_widgets::*; -use matrix_sdk::{ - ruma::OwnedRoomId, RoomState, SuccessorRoom -}; +use matrix_sdk::{ruma::OwnedRoomId, RoomState, SuccessorRoom}; -use crate::{app::AppStateAction, room::{BasicRoomDetails, FetchedRoomAvatar, FetchedRoomPreview}, shared::avatar::AvatarWidgetExt, utils}; +use crate::{ + app::AppStateAction, + room::{BasicRoomDetails, FetchedRoomAvatar, FetchedRoomPreview}, + shared::avatar::AvatarWidgetExt, + utils, +}; const DEFAULT_TOMBSTONE_REASON: &str = "This room has been replaced and is no longer active."; const DEFAULT_JOIN_BUTTON_TEXT: &str = "Go to the replacement room"; @@ -95,26 +98,34 @@ pub enum SuccessorRoomDetails { Full { room_preview: FetchedRoomPreview, reason: Option, - } + }, } - /// A view that shows information about a tombstoned room and its successor. #[derive(Script, ScriptHook, Widget)] pub struct TombstoneFooter { - #[deref] view: View, + #[deref] + view: View, /// The ID of the current tombstoned room. - #[rust] room_id: Option, + #[rust] + room_id: Option, /// The details of the successor room. - #[rust] successor_info: Option, + #[rust] + successor_info: Option, } impl Widget for TombstoneFooter { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { - if self.view.button(cx, ids!(join_successor_button)).clicked(actions) { + if self + .view + .button(cx, ids!(join_successor_button)) + .clicked(actions) + { let Some(destination_room) = self.successor_info.clone() else { - error!("BUG: cannot navigate to replacement room: no successor room information."); + error!( + "BUG: cannot navigate to replacement room: no successor room information." + ); return; }; cx.action(AppStateAction::NavigateToRoom { @@ -144,7 +155,9 @@ impl TombstoneFooter { let successor_room_avatar = self.view.avatar(cx, ids!(successor_room_avatar)); let successor_room_name = self.view.label(cx, ids!(successor_room_name)); - log!("Showing TombstoneFooter for room {tombstoned_room_id}, Successor: {successor_room_details:?}"); + log!( + "Showing TombstoneFooter for room {tombstoned_room_id}, Successor: {successor_room_details:?}" + ); match successor_room_details { SuccessorRoomDetails::None => { replacement_reason.set_text(cx, DEFAULT_TOMBSTONE_REASON); @@ -154,36 +167,33 @@ impl TombstoneFooter { self.successor_info = None; } SuccessorRoomDetails::Basic(sr) => { - replacement_reason.set_text( - cx, - sr.reason.as_deref().unwrap_or(DEFAULT_TOMBSTONE_REASON) - ); + replacement_reason + .set_text(cx, sr.reason.as_deref().unwrap_or(DEFAULT_TOMBSTONE_REASON)); join_successor_button.set_text(cx, DEFAULT_JOIN_BUTTON_TEXT); successor_room_avatar.show_text(cx, None, None, "#"); successor_room_name.set_text(cx, &format!("Room ID {}", sr.room_id)); self.successor_info = Some(sr.into()); - }, - SuccessorRoomDetails::Full { room_preview, reason } => { - replacement_reason.set_text( - cx, - reason.as_deref().unwrap_or(DEFAULT_TOMBSTONE_REASON) - ); + } + SuccessorRoomDetails::Full { + room_preview, + reason, + } => { + replacement_reason + .set_text(cx, reason.as_deref().unwrap_or(DEFAULT_TOMBSTONE_REASON)); join_successor_button.set_text( cx, matches!(room_preview.state, Some(RoomState::Joined)) .then_some(DEFAULT_JOIN_BUTTON_TEXT) - .unwrap_or("Join the replacement room") + .unwrap_or("Join the replacement room"), ); match &room_preview.room_avatar { FetchedRoomAvatar::Text(text) => { successor_room_avatar.show_text(cx, None, None, text); } FetchedRoomAvatar::Image(image_data) => { - let res = successor_room_avatar.show_image( - cx, - None, - |cx, img_ref| utils::load_png_or_jpg(&img_ref, cx, image_data), - ); + let res = successor_room_avatar.show_image(cx, None, |cx, img_ref| { + utils::load_png_or_jpg(&img_ref, cx, image_data) + }); if res.is_err() { successor_room_avatar.show_text( cx, @@ -196,7 +206,10 @@ impl TombstoneFooter { } match room_preview.room_name_id.name_for_avatar() { Some(n) => successor_room_name.set_text(cx, n), - _ => successor_room_name.set_text(cx, &format!("Unnamed Room, ID: {}", room_preview.room_name_id.room_id())), + _ => successor_room_name.set_text( + cx, + &format!("Unnamed Room, ID: {}", room_preview.room_name_id.room_id()), + ), } self.successor_info = Some(room_preview.clone().into()); } @@ -222,13 +235,17 @@ impl TombstoneFooterRef { tombstoned_room_id: &OwnedRoomId, successor_room_details: &SuccessorRoomDetails, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, tombstoned_room_id, successor_room_details); } /// See [`TombstoneFooter::hide()`]. pub fn hide(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.hide(cx); } } diff --git a/src/join_leave_room_modal.rs b/src/join_leave_room_modal.rs index eb8f5632c..66365fb58 100644 --- a/src/join_leave_room_modal.rs +++ b/src/join_leave_room_modal.rs @@ -8,7 +8,20 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::mpsc::UnboundedSender; -use crate::{home::invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, room::BasicRoomDetails, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_negative_button_style, apply_neutral_button_style, apply_positive_button_style, apply_primary_button_style}}, sliding_sync::{MatrixRequest, submit_async_request}, space_service_sync::{SpaceRequest, SpaceRoomListAction}, utils::{self, RoomNameId}}; +use crate::{ + home::invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, + room::BasicRoomDetails, + shared::{ + popup_list::{PopupKind, enqueue_popup_notification}, + styles::{ + apply_negative_button_style, apply_neutral_button_style, apply_positive_button_style, + apply_primary_button_style, + }, + }, + sliding_sync::{MatrixRequest, submit_async_request}, + space_service_sync::{SpaceRequest, SpaceRoomListAction}, + utils::{self, RoomNameId}, +}; script_mod! { use mod.prelude.widgets.* @@ -114,14 +127,17 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct JoinLeaveRoomModal { - #[deref] view: View, - #[rust] kind: Option, + #[deref] + view: View, + #[rust] + kind: Option, /// Whether the modal is in a final state, meaning the user can only click "Okay" to close it. /// /// * Set to `Some(true)` after a successful action (e.g., joining or leaving a room). /// * Set to `Some(false)` after a join/leave error occurs. /// * Set to `None` when the user is still able to interact with the modal. - #[rust] final_success: Option, + #[rust] + final_success: Option, } /// Kinds of content that can be shown and handled by the [`JoinLeaveRoomModal`]. @@ -151,8 +167,9 @@ pub enum JoinLeaveModalKind { impl JoinLeaveModalKind { pub fn room_id(&self) -> &OwnedRoomId { match self { - JoinLeaveModalKind::AcceptInvite(invite) - | JoinLeaveModalKind::RejectInvite(invite) => invite.room_id(), + JoinLeaveModalKind::AcceptInvite(invite) | JoinLeaveModalKind::RejectInvite(invite) => { + invite.room_id() + } JoinLeaveModalKind::JoinRoom { details, .. } | JoinLeaveModalKind::LeaveRoom(details) | JoinLeaveModalKind::LeaveSpace { details, .. } => details.room_id(), @@ -161,8 +178,9 @@ impl JoinLeaveModalKind { pub fn room_name(&self) -> &RoomNameId { match self { - JoinLeaveModalKind::AcceptInvite(invite) - | JoinLeaveModalKind::RejectInvite(invite) => invite.room_name_id(), + JoinLeaveModalKind::AcceptInvite(invite) | JoinLeaveModalKind::RejectInvite(invite) => { + invite.room_name_id() + } JoinLeaveModalKind::JoinRoom { details, .. } | JoinLeaveModalKind::LeaveRoom(details) | JoinLeaveModalKind::LeaveSpace { details, .. } => details.room_name_id(), @@ -172,8 +190,9 @@ impl JoinLeaveModalKind { #[allow(unused)] // remove when we use it in navigate_to_room pub fn basic_room_details(&self) -> &BasicRoomDetails { match self { - JoinLeaveModalKind::AcceptInvite(invite) - | JoinLeaveModalKind::RejectInvite(invite) => &invite.room_info, + JoinLeaveModalKind::AcceptInvite(invite) | JoinLeaveModalKind::RejectInvite(invite) => { + &invite.room_info + } JoinLeaveModalKind::JoinRoom { details, .. } | JoinLeaveModalKind::LeaveRoom(details) | JoinLeaveModalKind::LeaveSpace { details, .. } => details, @@ -202,7 +221,6 @@ pub enum JoinLeaveRoomModalAction { }, } - impl Widget for JoinLeaveRoomModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -220,25 +238,34 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let cancel_button = self.view.button(cx, ids!(cancel_button)); let cancel_clicked = cancel_button.clicked(actions); - if cancel_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // Inform other widgets that this modal has been closed. - cx.action(JoinLeaveRoomModalAction::Close { successful: false, was_internal: cancel_clicked }); + cx.action(JoinLeaveRoomModalAction::Close { + successful: false, + was_internal: cancel_clicked, + }); self.reset_state(); return; } - let Some(kind) = self.kind.as_ref() else { return }; + let Some(kind) = self.kind.as_ref() else { + return; + }; let mut needs_redraw = false; if accept_button.clicked(actions) { if let Some(successful) = self.final_success { - cx.action(JoinLeaveRoomModalAction::Close { successful, was_internal: true }); + cx.action(JoinLeaveRoomModalAction::Close { + successful, + was_internal: true, + }); self.reset_state(); return; - } - else { + } else { let title: Cow; let description: String; let accept_button_text: &str; @@ -268,7 +295,11 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { }); } JoinLeaveModalKind::JoinRoom { details, is_space } => { - title = format!("Joining this {}...", if *is_space { "space" } else { "room" }).into(); + title = format!( + "Joining this {}...", + if *is_space { "space" } else { "room" } + ) + .into(); description = format!( "Joining \"{}\".\n\n\ Waiting for confirmation from the homeserver...", @@ -291,7 +322,10 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { room_id: room.room_id().clone(), }); } - JoinLeaveModalKind::LeaveSpace { details, space_request_sender } => { + JoinLeaveModalKind::LeaveSpace { + details, + space_request_sender, + } => { title = "Leaving this space...".into(); description = format!( "Leaving \"{}\".\n\n\ @@ -299,9 +333,12 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { details.room_name_id(), ); accept_button_text = "Leaving..."; - if space_request_sender.send( - SpaceRequest::LeaveSpace { space_name_id: details.room_name_id().clone() } - ).is_err() { + if space_request_sender + .send(SpaceRequest::LeaveSpace { + space_name_id: details.room_name_id().clone(), + }) + .is_err() + { enqueue_popup_notification( "Failed to send leave space request.\n\nPlease restart Robrix.", PopupKind::Error, @@ -312,7 +349,9 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { } self.view.label(cx, ids!(title)).set_text(cx, &title); - self.view.label(cx, ids!(description)).set_text(cx, &description); + self.view + .label(cx, ids!(description)) + .set_text(cx, &description); self.view.view(cx, ids!(tip_view)).set_visible(cx, false); accept_button.set_text(cx, accept_button_text); accept_button.set_enabled(cx, false); @@ -329,23 +368,33 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { PopupKind::Success, Some(3.0), ); - self.view.label(cx, ids!(title)).set_text(cx, "Joined room!"); - self.view.label(cx, ids!(description)).set_text(cx, &format!( - "Successfully joined \"{}\".", - kind.room_name(), - )); + self.view + .label(cx, ids!(title)) + .set_text(cx, "Joined room!"); + self.view.label(cx, ids!(description)).set_text( + cx, + &format!("Successfully joined \"{}\".", kind.room_name(),), + ); new_final_success = Some(true); } - Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == kind.room_id() => { - self.view.label(cx, ids!(title)).set_text(cx, "Error joining room!"); - let was_invite = matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)); - let msg = utils::stringify_join_leave_error(error, kind.room_name(), true, was_invite); - self.view.label(cx, ids!(description)).set_text(cx, &msg); - enqueue_popup_notification( - msg, - PopupKind::Error, - None, + Some(JoinRoomResultAction::Failed { room_id, error }) + if room_id == kind.room_id() => + { + self.view + .label(cx, ids!(title)) + .set_text(cx, "Error joining room!"); + let was_invite = matches!( + kind, + JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_) ); + let msg = utils::stringify_join_leave_error( + error, + kind.room_name(), + true, + was_invite, + ); + self.view.label(cx, ids!(description)).set_text(cx, &msg); + enqueue_popup_notification(msg, PopupKind::Error, None); new_final_success = Some(false); } _ => {} @@ -356,49 +405,66 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let title: &str; let description: String; let popup_msg: Cow<'static, str>; - if matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)) { + if matches!( + kind, + JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_) + ) { title = "Rejected invite!"; - description = format!( - "Successfully rejected invite to \"{}\".", - kind.room_name(), - ); + description = + format!("Successfully rejected invite to \"{}\".", kind.room_name(),); popup_msg = "Successfully rejected invite.".into(); } else { title = "Left room!"; - description = format!( - "Successfully left \"{}\".", - kind.room_name(), - ); + description = format!("Successfully left \"{}\".", kind.room_name(),); popup_msg = "Successfully left room.".into(); } self.view.label(cx, ids!(title)).set_text(cx, title); - self.view.label(cx, ids!(description)).set_text(cx, &description); + self.view + .label(cx, ids!(description)) + .set_text(cx, &description); enqueue_popup_notification(popup_msg, PopupKind::Success, Some(5.0)); new_final_success = Some(true); } - Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == kind.room_id() => { + Some(LeaveRoomResultAction::Failed { room_id, error }) + if room_id == kind.room_id() => + { let title: &str; let description: String; let popup_msg: Cow<'static, str>; - if matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)) { + if matches!( + kind, + JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_) + ) { title = "Error rejecting invite!"; - description = utils::stringify_join_leave_error(error, kind.room_name(), false, true); + description = + utils::stringify_join_leave_error(error, kind.room_name(), false, true); popup_msg = "Failed to reject invite.".into(); } else { title = "Error leaving room!"; - description = utils::stringify_join_leave_error(error, kind.room_name(), false, false); + description = utils::stringify_join_leave_error( + error, + kind.room_name(), + false, + false, + ); popup_msg = "Failed to leave room.".into(); } self.view.label(cx, ids!(title)).set_text(cx, title); - self.view.label(cx, ids!(description)).set_text(cx, &description); + self.view + .label(cx, ids!(description)) + .set_text(cx, &description); enqueue_popup_notification(popup_msg, PopupKind::Error, None); new_final_success = Some(false); } _ => {} } - if let Some(SpaceRoomListAction::LeaveSpaceResult { space_name_id, result }) = action.downcast_ref() { + if let Some(SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result, + }) = action.downcast_ref() + { if space_name_id.room_id() == kind.room_id() { let title: &str; let description: String; @@ -410,12 +476,15 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { } Err(e) => { title = "Error leaving space!"; - description = format!("Failed to leave space \"{space_name_id}\".\n\nError: {e}"); + description = + format!("Failed to leave space \"{space_name_id}\".\n\nError: {e}"); new_final_success = Some(false); } } self.view.label(cx, ids!(title)).set_text(cx, title); - self.view.label(cx, ids!(description)).set_text(cx, &description); + self.view + .label(cx, ids!(description)) + .set_text(cx, &description); } } } @@ -441,14 +510,9 @@ impl JoinLeaveRoomModal { self.final_success = None; } - /// Populates this modal with the proper info based on + /// Populates this modal with the proper info based on /// the given `kind of join or leave action. - fn set_kind( - &mut self, - cx: &mut Cx, - kind: JoinLeaveModalKind, - show_tip: bool, - ) { + fn set_kind(&mut self, cx: &mut Cx, kind: JoinLeaveModalKind, show_tip: bool) { log!("Showing JoinLeaveRoomModal for {kind:?}"); let title: &str; let description: String; @@ -509,7 +573,9 @@ impl JoinLeaveRoomModal { } self.view.label(cx, ids!(title)).set_text(cx, title); - self.view.label(cx, ids!(description)).set_text(cx, &description); + self.view + .label(cx, ids!(description)) + .set_text(cx, &description); if show_tip { self.view.view(cx, ids!(tip_view)).set_visible(cx, true); self.view.label(cx, ids!(tip)).set_text(cx, &format!( @@ -523,10 +589,11 @@ impl JoinLeaveRoomModal { let mut cancel_button = self.button(cx, ids!(cancel_button)); accept_button.set_text(cx, "Yes"); - let is_negative = matches!(kind, + let is_negative = matches!( + kind, JoinLeaveModalKind::RejectInvite(_) - | JoinLeaveModalKind::LeaveRoom(_) - | JoinLeaveModalKind::LeaveSpace { .. } + | JoinLeaveModalKind::LeaveRoom(_) + | JoinLeaveModalKind::LeaveSpace { .. } ); if is_negative { @@ -554,13 +621,10 @@ impl JoinLeaveRoomModal { impl JoinLeaveRoomModalRef { /// Sets the details of this join/leave modal. - pub fn set_kind( - &self, - cx: &mut Cx, - kind: JoinLeaveModalKind, - show_tip: bool, - ) { - let Some(mut inner) = self.borrow_mut() else { return }; + pub fn set_kind(&self, cx: &mut Cx, kind: JoinLeaveModalKind, show_tip: bool) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_kind(cx, kind, show_tip); } } diff --git a/src/lib.rs b/src/lib.rs index 346c0314b..f26e0c117 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,6 @@ macro_rules! live { pub type LivePtr = makepad_widgets::ScriptValue; - pub fn widget_ref_from_live_ptr( cx: &mut makepad_widgets::Cx, ptr: Option, @@ -61,7 +60,6 @@ pub mod shared; mod event_preview; pub mod room; - /// All content related to TSP (Trust Spanning Protocol) wallets/identities. #[cfg(feature = "tsp")] pub mod tsp; @@ -69,7 +67,6 @@ pub mod tsp; #[cfg(not(feature = "tsp"))] pub mod tsp_dummy; - // Matrix stuff pub mod sliding_sync; pub mod space_service_sync; diff --git a/src/location.rs b/src/location.rs index 515d00322..446ca9008 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,6 +1,12 @@ //! Functions for querying the device's current location. -use std::{sync::{mpsc::{self, Receiver, Sender}, Mutex}, time::SystemTime}; +use std::{ + sync::{ + mpsc::{self, Receiver, Sender}, + Mutex, + }, + time::SystemTime, +}; use makepad_widgets::{Cx, error, log}; use robius_location::{Access, Accuracy, Coordinates, Location, Manager}; @@ -12,7 +18,7 @@ pub enum LocationAction { Update(LocationUpdate), /// The location handler encountered an error. Error(robius_location::Error), - None + None, } /// An updated location sample, including coordinates and a system timestamp. @@ -32,7 +38,6 @@ pub fn get_latest_location() -> Option { *(LATEST_LOCATION.lock().unwrap()) } - struct LocationHandler; impl robius_location::Handler for LocationHandler { @@ -61,12 +66,10 @@ impl robius_location::Handler for LocationHandler { } } - fn location_request_loop( request_receiver: Receiver, mut manager: ManagerWrapper, ) -> Result<(), robius_location::Error> { - manager.update_once()?; while let Ok(request) = request_receiver.recv() { @@ -87,7 +90,6 @@ fn location_request_loop( Err(robius_location::Error::Unknown) } - pub enum LocationRequest { UpdateOnce, StartUpdates, diff --git a/src/login/login_status_modal.rs b/src/login/login_status_modal.rs index ee92a87cc..da1cf0637 100644 --- a/src/login/login_status_modal.rs +++ b/src/login/login_status_modal.rs @@ -75,7 +75,8 @@ script_mod! { /// A modal dialog that displays the status of a login attempt. #[derive(Script, ScriptHook, Widget)] pub struct LoginStatusModal { - #[deref] view: View, + #[deref] + view: View, } #[derive(Clone, Debug, Default)] @@ -113,7 +114,7 @@ impl WidgetMatchEvent for LoginStatusModal { // a `LoginStatusModalAction::Close` action, as that would cause // an infinite action feedback loop. if !modal_dismissed { - cx.widget_action(widget_uid, LoginStatusModalAction::Close); + cx.widget_action(widget_uid, LoginStatusModalAction::Close); } } } diff --git a/src/logout/logout_confirm_modal.rs b/src/logout/logout_confirm_modal.rs index 506162acf..5be332933 100644 --- a/src/logout/logout_confirm_modal.rs +++ b/src/logout/logout_confirm_modal.rs @@ -85,13 +85,15 @@ script_mod! { /// A modal dialog that displays logout confirmation. #[derive(Script, ScriptHook, Widget)] pub struct LogoutConfirmModal { - #[deref] view: View, + #[deref] + view: View, /// Whether the modal is in a final state, meaning the user can only click "Okay" to close it. /// /// * Set to `Some(true)` after a successful logout Action /// * Set to `Some(false)` after a logout error occurs. /// * Set to `None` when the user is still able to interact with the modal. - #[rust] final_success: Option, + #[rust] + final_success: Option, } /// Actions handled by the parent widget of the [`LogoutConfirmModal`]. @@ -111,16 +113,14 @@ pub enum LogoutConfirmModalAction { None, } -/// Actions related to logout process +/// Actions related to logout process pub enum LogoutAction { /// A positive response to a logout request from the Matrix homeserver. LogoutSuccess, /// A negative response to a logout request from the Matrix homeserver. LogoutFailure(String), /// A request from the background task to the main UI thread to clear all app state. - ClearAppState { - on_clear_appstate: Arc, - }, + ClearAppState { on_clear_appstate: Arc }, /// Signal that the application is in an invalid state and needs to be restarted. /// This happens when critical components have been cleaned up during a previous /// logout attempt that reached the point of no return, but the app wasn't restarted. @@ -129,10 +129,7 @@ pub enum LogoutAction { cleared_component: ClearedComponentType, }, /// Progress update from the logout state machine - ProgressUpdate { - message: String, - percentage: u8, - }, + ProgressUpdate { message: String, percentage: u8 }, /// Indicates logout is in progress or not InProgress(bool), } @@ -146,7 +143,10 @@ impl std::fmt::Debug for LogoutAction { LogoutAction::ApplicationRequiresRestart { cleared_component } => { write!(f, "ApplicationRequiresRestart({:?})", cleared_component) } - LogoutAction::ProgressUpdate { message, percentage } => { + LogoutAction::ProgressUpdate { + message, + percentage, + } => { write!(f, "ProgressUpdate({}, {}%)", message, percentage) } LogoutAction::InProgress(value) => write!(f, "InProgress({})", value), @@ -182,11 +182,16 @@ impl WidgetMatchEvent for LogoutConfirmModal { let cancel_button = self.button(cx, ids!(cancel_button)); let mut confirm_button = self.button(cx, ids!(confirm_button)); - let modal_dismissed = actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))); + let modal_dismissed = actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))); let cancel_clicked = cancel_button.clicked(actions); if cancel_clicked || modal_dismissed { - cx.action(LogoutConfirmModalAction::Close { successful: false, was_internal: cancel_clicked }); + cx.action(LogoutConfirmModalAction::Close { + successful: false, + was_internal: cancel_clicked, + }); self.reset_state(cx); return; } @@ -199,7 +204,10 @@ impl WidgetMatchEvent for LogoutConfirmModal { cx.quit(); } - cx.action(LogoutConfirmModalAction::Close { successful, was_internal: true }); + cx.action(LogoutConfirmModalAction::Close { + successful, + was_internal: true, + }); self.reset_state(cx); return; } else { @@ -210,7 +218,9 @@ impl WidgetMatchEvent for LogoutConfirmModal { cancel_button.set_text(cx, "Abort"); cancel_button.set_enabled(cx, true); - submit_async_request(MatrixRequest::Logout { is_desktop: cx.display_context.is_desktop() }); + submit_async_request(MatrixRequest::Logout { + is_desktop: cx.display_context.is_desktop(), + }); needs_redraw = true; } } @@ -230,7 +240,8 @@ impl WidgetMatchEvent for LogoutConfirmModal { Some(LogoutAction::LogoutFailure(error)) => { if is_logout_past_point_of_no_return() { - self.label(cx, ids!(title)).set_text(cx, "Logout error, please restart Robrix."); + self.label(cx, ids!(title)) + .set_text(cx, "Logout error, please restart Robrix."); self.set_message(cx, "The logout process encountered an error when communicating with the homeserver. Since your login session has been partially invalidated, Robrix must restart in order to continue to properly function."); confirm_button.set_text(cx, "Restart now"); @@ -242,7 +253,6 @@ impl WidgetMatchEvent for LogoutConfirmModal { confirm_button.set_enabled(cx, true); cancel_button.set_visible(cx, false); - } else { self.set_message(cx, &format!("Logout failed: {}", error)); confirm_button.set_text(cx, "Okay"); @@ -255,7 +265,8 @@ impl WidgetMatchEvent for LogoutConfirmModal { } Some(LogoutAction::ApplicationRequiresRestart { .. }) => { - self.label(cx, ids!(title)).set_text(cx, "Logout error, please restart Robrix."); + self.label(cx, ids!(title)) + .set_text(cx, "Logout error, please restart Robrix."); self.set_message(cx, "Application is in an inconsistent state and needs to be restarted to continue."); confirm_button.set_text(cx, "Restart now"); @@ -271,7 +282,10 @@ impl WidgetMatchEvent for LogoutConfirmModal { needs_redraw = true; } - Some(LogoutAction::ProgressUpdate { message, percentage }) => { + Some(LogoutAction::ProgressUpdate { + message, + percentage, + }) => { // Just update the message text to show progress self.set_message(cx, &format!("{} ({}%)", message, percentage)); // Disable confirm button during logout, but keep cancel/abort enabled @@ -288,7 +302,6 @@ impl WidgetMatchEvent for LogoutConfirmModal { if needs_redraw { self.redraw(cx); } - } } @@ -312,7 +325,6 @@ impl LogoutConfirmModal { confirm_button.reset_hover(cx); self.redraw(cx); } - } impl LogoutConfirmModalRef { @@ -323,10 +335,9 @@ impl LogoutConfirmModalRef { } } - pub fn reset_state(&self,cx: &mut Cx) { + pub fn reset_state(&self, cx: &mut Cx) { if let Some(mut inner) = self.borrow_mut() { inner.reset_state(cx); } } - } diff --git a/src/logout/logout_errors.rs b/src/logout/logout_errors.rs index c09719d01..973e069f4 100644 --- a/src/logout/logout_errors.rs +++ b/src/logout/logout_errors.rs @@ -42,4 +42,4 @@ impl fmt::Display for LogoutError { } } -impl std::error::Error for LogoutError {} \ No newline at end of file +impl std::error::Error for LogoutError {} diff --git a/src/logout/logout_state_machine.rs b/src/logout/logout_state_machine.rs index 3ccb922ca..d26b38a6a 100644 --- a/src/logout/logout_state_machine.rs +++ b/src/logout/logout_state_machine.rs @@ -147,7 +147,7 @@ impl LogoutProgress { step_started_at: now, } } - + fn update(&mut self, state: LogoutState, message: String, percentage: u8) { self.state = state; self.message = message; @@ -194,12 +194,9 @@ pub struct LogoutStateMachine { impl LogoutStateMachine { pub fn new(config: LogoutConfig) -> Self { - let initial_progress = LogoutProgress::new( - LogoutState::Idle, - "Ready to logout".to_string(), - 0 - ); - + let initial_progress = + LogoutProgress::new(LogoutState::Idle, "Ready to logout".to_string(), 0); + Self { current_state: Arc::new(Mutex::new(LogoutState::Idle)), progress: Arc::new(Mutex::new(initial_progress)), @@ -208,113 +205,136 @@ impl LogoutStateMachine { cancellation_requested: Arc::new(AtomicBool::new(false)), } } - + /// Get current state pub async fn current_state(&self) -> LogoutState { self.current_state.lock().await.clone() } - + /// Get current progress pub async fn progress(&self) -> LogoutProgress { self.progress.lock().await.clone() } - + /// Request cancellation (only works before point of no return) pub fn request_cancellation(&self) { if !self.point_of_no_return.load(Ordering::Acquire) { self.cancellation_requested.store(true, Ordering::Release); } } - + /// Check if cancellation was requested fn is_cancelled(&self) -> bool { self.cancellation_requested.load(Ordering::Acquire) } - + /// Transition to a new state - async fn transition_to(&self, new_state: LogoutState, message: String, percentage: u8) -> Result<()> { + async fn transition_to( + &self, + new_state: LogoutState, + message: String, + percentage: u8, + ) -> Result<()> { // Check for cancellation before transitioning - if self.is_cancelled() && !matches!(new_state, LogoutState::PointOfNoReturn | LogoutState::Failed(_)) { + if self.is_cancelled() + && !matches!( + new_state, + LogoutState::PointOfNoReturn | LogoutState::Failed(_) + ) + { let mut state = self.current_state.lock().await; *state = LogoutState::Failed(LogoutError::Recoverable(RecoverableError::Cancelled)); return Err(anyhow!("Logout cancelled by user")); } - - log!("Logout state transition: {:?} -> {:?}", self.current_state.lock().await.clone(), new_state); - + + log!( + "Logout state transition: {:?} -> {:?}", + self.current_state.lock().await.clone(), + new_state + ); + // Update state and progress, then extract values for UI update let mut state = self.current_state.lock().await; *state = new_state.clone(); drop(state); - + let mut progress = self.progress.lock().await; progress.update(new_state, message.clone(), percentage); let progress_message = progress.message.clone(); let progress_percentage = progress.percentage; drop(progress); - + // Send progress update to UI - log!("Sending progress update: {} ({}%)", progress_message, progress_percentage); - Cx::post_action(LogoutAction::ProgressUpdate { + log!( + "Sending progress update: {} ({}%)", + progress_message, + progress_percentage + ); + Cx::post_action(LogoutAction::ProgressUpdate { message: progress_message, - percentage: progress_percentage + percentage: progress_percentage, }); - + Ok(()) } - + /// Execute the logout process pub async fn execute(&self) -> Result<()> { log!("LogoutStateMachine::execute() started"); - + // Set logout in progress flag set_logout_in_progress(true); - + // Reset global point of no return flag set_logout_point_of_no_return(false); - + // Start from Idle state self.transition_to( LogoutState::PreChecking, "Checking prerequisites...".to_string(), - 10 - ).await?; - + 10, + ) + .await?; + // Pre-checks if let Err(e) = self.perform_prechecks().await { self.transition_to( LogoutState::Failed(e.clone()), format!("Precheck failed: {}", e), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&e).await; return Err(anyhow!(e)); } - + // Stop sync service self.transition_to( LogoutState::StoppingSyncService, "Stopping sync service...".to_string(), - 20 - ).await?; - + 20, + ) + .await?; + if let Err(e) = self.stop_sync_service().await { self.transition_to( LogoutState::Failed(e.clone()), format!("Failed to stop sync service: {}", e), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&e).await; return Err(anyhow!(e)); } - + // Server logout self.transition_to( LogoutState::LoggingOutFromServer, "Logging out from server...".to_string(), - 30 - ).await?; - + 30, + ) + .await?; + match self.perform_server_logout().await { Ok(_) => { self.point_of_no_return.store(true, Ordering::Release); @@ -322,9 +342,10 @@ impl LogoutStateMachine { self.transition_to( LogoutState::PointOfNoReturn, "Point of no return reached".to_string(), - 50 - ).await?; - + 50, + ) + .await?; + // We delete latest_user_id after reaching LOGOUT_POINT_OF_NO_RETURN: // 1. To prevent auto-login with invalid session on next start // 2. While keeping session file intact for potential future login @@ -334,16 +355,18 @@ impl LogoutStateMachine { } Err(e) => { // Check if it's an M_UNKNOWN_TOKEN error - if matches!(&e, LogoutError::Recoverable(RecoverableError::ServerLogoutFailed(msg)) if msg.contains("M_UNKNOWN_TOKEN")) { + if matches!(&e, LogoutError::Recoverable(RecoverableError::ServerLogoutFailed(msg)) if msg.contains("M_UNKNOWN_TOKEN")) + { log!("Token already invalidated, continuing with logout"); self.point_of_no_return.store(true, Ordering::Release); set_logout_point_of_no_return(true); self.transition_to( LogoutState::PointOfNoReturn, "Token already invalidated".to_string(), - 50 - ).await?; - + 50, + ) + .await?; + // Same delete operation as in the success case above if let Err(e) = delete_latest_user_id().await { log!("Warning: Failed to delete latest user ID: {}", e); @@ -353,94 +376,107 @@ impl LogoutStateMachine { if let Some(sync_service) = get_sync_service() { sync_service.start().await; } - + self.transition_to( LogoutState::Failed(e.clone()), format!("Server logout failed: {}", e), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&e).await; return Err(anyhow!(e)); } } } - + // From here on, all failures are unrecoverable - + // Close tabs (desktop only) if self.config.is_desktop { self.transition_to( LogoutState::ClosingTabs, "Closing all tabs...".to_string(), - 60 - ).await?; - + 60, + ) + .await?; + if let Err(e) = self.close_all_tabs().await { - let error = LogoutError::Unrecoverable(UnrecoverableError::PostPointOfNoReturnFailure(e.to_string())); + let error = LogoutError::Unrecoverable( + UnrecoverableError::PostPointOfNoReturnFailure(e.to_string()), + ); self.transition_to( LogoutState::Failed(error.clone()), "Failed to close tabs".to_string(), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&error).await; return Err(anyhow!(error)); } } - + // Clean app state self.transition_to( LogoutState::CleaningAppState, "Cleaning up application state...".to_string(), - 70 - ).await?; - + 70, + ) + .await?; + // All static resources (CLIENT, SYNC_SERVICE, etc.) are defined in the sliding_sync module, // so the state machine delegates the cleanup operation to sliding_sync's clear_app_state function // rather than accessing these static variables directly from outside the module. if let Err(e) = clear_app_state(&self.config).await { - let error = LogoutError::Unrecoverable(UnrecoverableError::PostPointOfNoReturnFailure(e.to_string())); + let error = LogoutError::Unrecoverable(UnrecoverableError::PostPointOfNoReturnFailure( + e.to_string(), + )); self.transition_to( LogoutState::Failed(error.clone()), "Failed to clean app state".to_string(), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&error).await; return Err(anyhow!(error)); } - + // Shutdown tasks self.transition_to( LogoutState::ShuttingDownTasks, "Shutting down background tasks...".to_string(), - 80 - ).await?; - + 80, + ) + .await?; + self.shutdown_background_tasks(); - + // Restart runtime self.transition_to( LogoutState::RestartingRuntime, "Restarting Matrix runtime...".to_string(), - 90 - ).await?; - - if let Err(e) = self.restart_runtime(){ + 90, + ) + .await?; + + if let Err(e) = self.restart_runtime() { let error = LogoutError::Unrecoverable(UnrecoverableError::RuntimeRestartFailed); self.transition_to( LogoutState::Failed(error.clone()), format!("Failed to restart runtime: {}", e), - 0 - ).await?; + 0, + ) + .await?; self.handle_error(&error).await; return Err(anyhow!(error)); } - + // Success! self.transition_to( LogoutState::Completed, "Logout completed successfully".to_string(), - 100 - ).await?; + 100, + ) + .await?; // Close the settings screen after logout, since its content // is specific to the currently-logged-in user's account. @@ -451,24 +487,28 @@ impl LogoutStateMachine { Cx::post_action(LogoutAction::LogoutSuccess); Ok(()) } - + // Individual step implementations async fn perform_prechecks(&self) -> Result<(), LogoutError> { log!("perform_prechecks started"); - + // Check client existence if get_client().is_none() { log!("perform_prechecks: client cleared"); - return Err(LogoutError::Unrecoverable(UnrecoverableError::ComponentsCleared)); + return Err(LogoutError::Unrecoverable( + UnrecoverableError::ComponentsCleared, + )); } - + // Check sync service if get_sync_service().is_none() { log!("perform_prechecks: sync service cleared"); - return Err(LogoutError::Unrecoverable(UnrecoverableError::ComponentsCleared)); + return Err(LogoutError::Unrecoverable( + UnrecoverableError::ComponentsCleared, + )); } log!("perform_prechecks: sync service exists"); - + // Check access token if let Some(client) = get_client() { if client.access_token().is_none() { @@ -477,39 +517,51 @@ impl LogoutStateMachine { } log!("perform_prechecks: access token exists"); } - + log!("perform_prechecks completed successfully"); Ok(()) } - + async fn stop_sync_service(&self) -> Result<(), LogoutError> { if let Some(sync_service) = get_sync_service() { sync_service.stop().await; Ok(()) } else { - Err(LogoutError::Unrecoverable(UnrecoverableError::ComponentsCleared)) + Err(LogoutError::Unrecoverable( + UnrecoverableError::ComponentsCleared, + )) } } - + async fn perform_server_logout(&self) -> Result<(), LogoutError> { let Some(client) = get_client() else { - return Err(LogoutError::Unrecoverable(UnrecoverableError::ComponentsCleared)); + return Err(LogoutError::Unrecoverable( + UnrecoverableError::ComponentsCleared, + )); }; - + match tokio::time::timeout( self.config.server_logout_timeout, - client.matrix_auth().logout() - ).await { + client.matrix_auth().logout(), + ) + .await + { Ok(Ok(_)) => Ok(()), - Ok(Err(e)) => Err(LogoutError::Recoverable(RecoverableError::ServerLogoutFailed(e.to_string()))), - Err(_) => Err(LogoutError::Recoverable(RecoverableError::Timeout("Server logout timed out".to_string()))), + Ok(Err(e)) => Err(LogoutError::Recoverable( + RecoverableError::ServerLogoutFailed(e.to_string()), + )), + Err(_) => Err(LogoutError::Recoverable(RecoverableError::Timeout( + "Server logout timed out".to_string(), + ))), } } - + async fn close_all_tabs(&self) -> Result<()> { let on_close_all = Arc::new(Notify::new()); - Cx::post_action(MainDesktopUiAction::CloseAllTabs { on_close_all: on_close_all.clone() }); - + Cx::post_action(MainDesktopUiAction::CloseAllTabs { + on_close_all: on_close_all.clone(), + }); + match tokio::time::timeout(self.config.tab_close_timeout, on_close_all.notified()).await { Ok(_) => { log!("Received signal that all tabs were closed successfully"); @@ -518,28 +570,28 @@ impl LogoutStateMachine { Err(_) => Err(anyhow!("Timed out waiting for tabs to close")), } } - + fn shutdown_background_tasks(&self) { shutdown_background_tasks(); } - + fn restart_runtime(&self) -> Result<()> { start_matrix_tokio() .map(|_| ()) .map_err(|e| anyhow!("Failed to restart runtime: {}", e)) } - + /// Handle errors by posting appropriate actions async fn handle_error(&self, error: &LogoutError) { // Reset logout in progress flag on error (unless we've reached point of no return) if !is_logout_past_point_of_no_return() { set_logout_in_progress(false); } - + match error { LogoutError::Unrecoverable(UnrecoverableError::ComponentsCleared) => { - Cx::post_action(LogoutAction::ApplicationRequiresRestart { - cleared_component: ClearedComponentType::Client + Cx::post_action(LogoutAction::ApplicationRequiresRestart { + cleared_component: ClearedComponentType::Client, }); } LogoutError::Recoverable(RecoverableError::Cancelled) => { @@ -582,16 +634,22 @@ fn set_logout_in_progress(value: bool) { /// Execute logout using the state machine pub async fn logout_with_state_machine(is_desktop: bool) -> Result<()> { - log!("logout_with_state_machine called with is_desktop: {}", is_desktop); - + log!( + "logout_with_state_machine called with is_desktop: {}", + is_desktop + ); + let config = LogoutConfig { is_desktop, ..Default::default() }; - + let state_machine = LogoutStateMachine::new(config); let result = state_machine.execute().await; - - log!("logout_with_state_machine finished with result: {:?}", result.is_ok()); + + log!( + "logout_with_state_machine finished with result: {:?}", + result.is_ok() + ); result } diff --git a/src/main.rs b/src/main.rs index 3de0885e8..dc8875f93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,10 @@ // This cfg option hides the command prompt console window on Windows. // TODO: move this into Makepad itself as an addition to the `MAKEPAD` env var. -#![cfg_attr(all(feature = "hide_windows_console", target_os = "windows"), windows_subsystem = "windows")] +#![cfg_attr( + all(feature = "hide_windows_console", target_os = "windows"), + windows_subsystem = "windows" +)] fn main() { robrix::app::app_main() diff --git a/src/media_cache.rs b/src/media_cache.rs index f87ae36da..547f21d82 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -1,9 +1,20 @@ -use std::{ops::{Deref, DerefMut}, sync::{Arc, Mutex}, time::SystemTime}; +use std::{ + ops::{Deref, DerefMut}, + sync::{Arc, Mutex}, + time::SystemTime, +}; use hashbrown::{hash_map::RawEntryMut, HashMap}; use makepad_widgets::{error, log, SignalToUI}; -use matrix_sdk::{media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, ruma::{events::room::MediaSource, OwnedMxcUri}, Error, HttpError}; +use matrix_sdk::{ + media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, + ruma::{events::room::MediaSource, OwnedMxcUri}, + Error, HttpError, +}; use reqwest::StatusCode; -use crate::{home::room_screen::TimelineUpdate, sliding_sync::{self, MatrixRequest}}; +use crate::{ + home::room_screen::TimelineUpdate, + sliding_sync::{self, MatrixRequest}, +}; /// The value type in the media cache, one per Matrix URI. #[derive(Debug, Clone)] @@ -26,7 +37,6 @@ pub enum MediaCacheEntry { /// A reference to a media cache entry and its associated format. pub type MediaCacheEntryRef = Arc>; - /// A cache of fetched media, indexed by Matrix URI. /// /// A single Matrix URI may have multiple media formats associated with it, @@ -57,9 +67,7 @@ impl MediaCache { /// /// It will also optionally send updates to the given timeline update sender /// when a media request has completed. - pub fn new( - timeline_update_sender: Option>, - ) -> Self { + pub fn new(timeline_update_sender: Option>) -> Self { Self { cache: HashMap::new(), timeline_update_sender, @@ -104,11 +112,11 @@ impl MediaCache { value.thumbnail = Some((Arc::clone(&entry_ref), requested_mts.clone())); // If a full-size image is already loaded, return it. if let Some(existing_file) = value.full_file.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap().deref() { - post_request_retval = ( - MediaCacheEntry::Loaded(Arc::clone(d)), - MediaFormat::File, - ); + if let MediaCacheEntry::Loaded(d) = + existing_file.lock().unwrap().deref() + { + post_request_retval = + (MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::File); } } entry_ref_to_fetch = entry_ref; @@ -116,17 +124,18 @@ impl MediaCache { } MediaFormat::File => { if let Some(entry_ref) = value.full_file.as_ref() { - return ( - entry_ref.lock().unwrap().deref().clone(), - MediaFormat::File, - ); + return (entry_ref.lock().unwrap().deref().clone(), MediaFormat::File); } else { // Here, a full-size image was requested but not found, so fetch it. let entry_ref = Arc::new(Mutex::new(MediaCacheEntry::Requested)); value.full_file = Some(entry_ref.clone()); // If a thumbnail is already loaded, return it. - if let Some((existing_thumbnail, existing_mts)) = value.thumbnail.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap().deref() { + if let Some((existing_thumbnail, existing_mts)) = + value.thumbnail.as_ref() + { + if let MediaCacheEntry::Loaded(d) = + existing_thumbnail.lock().unwrap().deref() + { post_request_retval = ( MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::Thumbnail(existing_mts.clone()), @@ -170,7 +179,11 @@ impl MediaCache { /// Removes a specific media format from the cache for the given MXC URI. /// If `format` is None, removes the entire cache entry for the URI. /// Returns the removed cache entry if found, None otherwise. - pub fn remove_cache_entry(&mut self, mxc_uri: &OwnedMxcUri, format: Option) -> Option { + pub fn remove_cache_entry( + &mut self, + mxc_uri: &OwnedMxcUri, + format: Option, + ) -> Option { match format { Some(MediaFormat::Thumbnail(_)) => { if let Some(cache_value) = self.cache.get_mut(mxc_uri) { @@ -200,7 +213,8 @@ impl MediaCache { // Remove the entire entry for this MXC URI self.cache.remove(mxc_uri).map(|cache_value| { // Return the full_file entry if it exists, otherwise the thumbnail entry - cache_value.full_file + cache_value + .full_file .or_else(|| cache_value.thumbnail.map(|(entry, _)| entry)) .unwrap_or_else(|| Arc::new(Mutex::new(MediaCacheEntry::Requested))) }) @@ -214,7 +228,10 @@ fn error_to_media_cache_entry(error: Error, request: &MediaRequestParameters) -> match error { Error::Http(http_error) => { if let Some(client_error) = http_error.as_client_api_error() { - error!("Client error for media cache: {client_error} for request: {:?}", request); + error!( + "Client error for media cache: {client_error} for request: {:?}", + request + ); MediaCacheEntry::Failed(client_error.status_code) } else { match *http_error { @@ -223,9 +240,11 @@ fn error_to_media_cache_entry(error: Error, request: &MediaRequestParameters) -> if !reqwest_error.is_connect() { MediaCacheEntry::Failed(StatusCode::INTERNAL_SERVER_ERROR) } else if reqwest_error.is_status() { - MediaCacheEntry::Failed(reqwest_error - .status() - .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)) + MediaCacheEntry::Failed( + reqwest_error + .status() + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), + ) } else { MediaCacheEntry::Failed(StatusCode::INTERNAL_SERVER_ERROR) } @@ -236,7 +255,7 @@ fn error_to_media_cache_entry(error: Error, request: &MediaRequestParameters) -> } Error::InsufficientData => MediaCacheEntry::Failed(StatusCode::PARTIAL_CONTENT), Error::AuthenticationRequired => MediaCacheEntry::Failed(StatusCode::UNAUTHORIZED), - _ => MediaCacheEntry::Failed(StatusCode::INTERNAL_SERVER_ERROR) + _ => MediaCacheEntry::Failed(StatusCode::INTERNAL_SERVER_ERROR), } } @@ -256,20 +275,24 @@ fn insert_into_cache>>( if let MediaSource::Plain(mxc_uri) = &request.source { log!("Fetched media for {mxc_uri}"); let mut path = crate::temp_storage::get_temp_dir_path().clone(); - let filename = format!("{}_{}_{}", - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis(), - mxc_uri.server_name().unwrap(), mxc_uri.media_id().unwrap(), + let filename = format!( + "{}_{}_{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_millis(), + mxc_uri.server_name().unwrap(), + mxc_uri.media_id().unwrap(), ); path.push(filename); path.set_extension("png"); log!("Writing user media image to disk: {:?}", path); - std::fs::write(path, &data) - .expect("Failed to write user media image to disk"); + std::fs::write(path, &data).expect("Failed to write user media image to disk"); } } MediaCacheEntry::Loaded(data) } - Err(e) => error_to_media_cache_entry(e, &request) + Err(e) => error_to_media_cache_entry(e, &request), }; *value_ref.lock().unwrap() = new_value; diff --git a/src/persistence/app_state.rs b/src/persistence/app_state.rs index 6bc88714f..811ad9895 100644 --- a/src/persistence/app_state.rs +++ b/src/persistence/app_state.rs @@ -5,12 +5,10 @@ use serde::{self, Deserialize, Serialize}; use matrix_sdk::ruma::{OwnedUserId, UserId}; use crate::{app::AppState, app_data_dir, persistence::persistent_state_dir}; - const LATEST_APP_STATE_FILE_NAME: &str = "latest_app_state.json"; const WINDOW_GEOM_STATE_FILE_NAME: &str = "window_geom_state.json"; - /// Persistable state of the window's size, position, and fullscreen status. #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WindowGeomState { @@ -22,15 +20,10 @@ pub struct WindowGeomState { pub is_fullscreen: bool, } - /// Save the current app state to persistent storage. -pub fn save_app_state( - app_state: AppState, - user_id: OwnedUserId, -) -> anyhow::Result<()> { - let file = std::fs::File::create( - persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME) - )?; +pub fn save_app_state(app_state: AppState, user_id: OwnedUserId) -> anyhow::Result<()> { + let file = + std::fs::File::create(persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME))?; let mut writer = std::io::BufWriter::new(file); serde_json::to_writer(&mut writer, &app_state)?; writer.flush()?; @@ -67,7 +60,7 @@ pub async fn load_app_state(user_id: &UserId) -> anyhow::Result { log!("No saved app state found, using default."); return Ok(AppState::default()); } - Err(e) => return Err(e.into()) + Err(e) => return Err(e.into()), }; match serde_json::from_slice(&file_bytes) { Ok(app_state) => { @@ -75,7 +68,9 @@ pub async fn load_app_state(user_id: &UserId) -> anyhow::Result { Ok(app_state) } Err(e) => { - error!("Failed to deserialize app state: {e}. This may be due to an incompatible format from a previous version."); + error!( + "Failed to deserialize app state: {e}. This may be due to an incompatible format from a previous version." + ); // Backup the old file to preserve user's data let backup_path = state_path.with_extension("json.bak"); diff --git a/src/persistence/tsp_state.rs b/src/persistence/tsp_state.rs index 8f50d8e5a..59ec864d4 100644 --- a/src/persistence/tsp_state.rs +++ b/src/persistence/tsp_state.rs @@ -17,7 +17,6 @@ pub fn tsp_wallets_dir() -> std::path::PathBuf { app_data_dir().join(WALLETS_DIR_NAME) } - /// The TSP state that is saved to persistent storage. /// /// It contains metadata about all wallets that have been created or imported. @@ -39,29 +38,22 @@ pub struct SavedTspState { impl SavedTspState { /// Returns true if this TSP state has any content. pub fn has_content(&self) -> bool { - !self.wallets.is_empty() - || self.default_wallet.is_some() - || self.default_vid.is_some() + !self.wallets.is_empty() || self.default_wallet.is_some() || self.default_vid.is_some() } pub fn num_wallets(&self) -> usize { - self.default_wallet.is_some() as usize - + self.wallets.len() + self.default_wallet.is_some() as usize + self.wallets.len() } } - /// Loads the TSP state from persistent storage. pub async fn load_tsp_state() -> anyhow::Result { - let content = match tokio::fs::read_to_string( - app_data_dir().join(TSP_STATE_FILE_NAME) - ).await { + let content = match tokio::fs::read_to_string(app_data_dir().join(TSP_STATE_FILE_NAME)).await { Ok(file) => file, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(SavedTspState::default()), - Err(e) => return Err(e.into()) + Err(e) => return Err(e.into()), }; - serde_json::from_str(&content) - .map_err(anyhow::Error::msg) + serde_json::from_str(&content).map_err(anyhow::Error::msg) } /// Asynchronously save the current TSP state to persistent storage. diff --git a/src/profile/user_profile.rs b/src/profile/user_profile.rs index cedbbeba3..4bdeca0d2 100644 --- a/src/profile/user_profile.rs +++ b/src/profile/user_profile.rs @@ -1,14 +1,25 @@ //! Widgets and types related to displaying info about a user profile. -use std::{borrow::Cow, ops::{Deref, DerefMut}}; +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, +}; use makepad_widgets::*; -use matrix_sdk::{room::{RoomMember, RoomMemberRole}, ruma::{events::room::member::MembershipState, OwnedRoomId, OwnedUserId}}; +use matrix_sdk::{ + room::{RoomMember, RoomMemberRole}, + ruma::{events::room::member::MembershipState, OwnedRoomId, OwnedUserId}, +}; use crate::{ - avatar_cache, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, current_user_id, is_user_ignored, submit_async_request}, utils + avatar_cache, + shared::{ + avatar::{AvatarState, AvatarWidgetExt}, + popup_list::{PopupKind, enqueue_popup_notification}, + }, + sliding_sync::{MatrixRequest, current_user_id, is_user_ignored, submit_async_request}, + utils, }; use super::user_profile_cache; - /// Information retrieved about a user: their displayable name, ID, and known avatar state. #[derive(Clone, Debug)] pub struct UserProfile { @@ -34,14 +45,14 @@ impl UserProfile { /// skipping any leading "@" characters. #[allow(unused)] pub fn first_letter(&self) -> &str { - self.username.as_deref() + self.username + .as_deref() .and_then(|un| utils::user_name_first_letter(un)) .or_else(|| utils::user_name_first_letter(self.user_id.as_str())) .unwrap_or_default() } } - /// Basic info needed to populate the contents of an avatar widget. #[derive(Clone, Debug)] pub struct UserProfileAndRoomId { @@ -121,7 +132,7 @@ script_mod! { } LineH { padding: 15 } - + membership := View { width: Fill, height: Fit, @@ -285,7 +296,6 @@ script_mod! { } } - #[derive(Clone, Default, Debug)] pub enum ShowUserProfileAction { ShowUserProfile(UserProfileAndRoomId), @@ -321,48 +331,56 @@ impl UserProfilePaneInfo { } fn membership_status(&self) -> &str { - self.room_member.as_ref().map_or( - "Not a Member", - |member| match member.membership() { + self.room_member + .as_ref() + .map_or("Not a Member", |member| match member.membership() { MembershipState::Join => "Status: Joined", MembershipState::Leave => "Status: Left", MembershipState::Ban => "Status: Banned", MembershipState::Invite => "Status: Invited", MembershipState::Knock => "Status: Knocking", _ => "Status: Unknown", - } - ) + }) } fn role_in_room(&self) -> Cow<'_, str> { - self.room_member.as_ref().map_or( - "Role: Unknown".into(), - |member| match member.suggested_role_for_power_level() { - RoomMemberRole::Creator => "Role: Creator".into(), - RoomMemberRole::Administrator => "Role: Admin".into(), - RoomMemberRole::Moderator => "Role: Moderator".into(), - RoomMemberRole::User => "Role: Standard User".into(), - } - ) + self.room_member + .as_ref() + .map_or("Role: Unknown".into(), |member| { + match member.suggested_role_for_power_level() { + RoomMemberRole::Creator => "Role: Creator".into(), + RoomMemberRole::Administrator => "Role: Admin".into(), + RoomMemberRole::Moderator => "Role: Moderator".into(), + RoomMemberRole::User => "Role: Standard User".into(), + } + }) } } #[derive(Script, ScriptHook, Widget, Animator)] pub struct UserProfileSlidingPane { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - #[live] slide: f32, - - #[rust] info: Option, - #[rust] is_animating_out: bool, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + #[live] + slide: f32, + + #[rust] + info: Option, + #[rust] + is_animating_out: bool, } impl Widget for UserProfileSlidingPane { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - if !self.visible { return; } + if !self.visible { + return; + } let animator_action = self.animator_handle_event(cx, event); if animator_action.must_redraw() { @@ -393,20 +411,23 @@ impl Widget for UserProfileSlidingPane { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) - ) - || event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(_fde) => { - cx.set_key_focus(area); - false - } - Hit::FingerUp(fue) if fue.is_over => { - fue.mouse_button().is_some_and(|b| b.is_back()) - || !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + ) || event.back_pressed() + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + Hit::FingerUp(fue) if fue.is_over => { + fue.mouse_button().is_some_and(|b| b.is_back()) + || !self + .view(cx, ids!(main_content)) + .area() + .rect(cx) + .contains(fue.abs) + } + _ => false, } - _ => false, - } }; if close_pane { self.is_animating_out = true; @@ -428,14 +449,16 @@ impl Widget for UserProfileSlidingPane { our_info.user_id.clone(), Some(&our_info.room_id), false, - |profile, rooms| (profile.clone(), rooms.get(&our_info.room_id).cloned()) + |profile, rooms| (profile.clone(), rooms.get(&our_info.room_id).cloned()), ) { let prev_avatar_state = our_info.avatar_state.clone(); our_info.user_profile = new_profile; our_info.room_member = room_member; // Use the avatar URI from the `room_member`, as it will be the most up-to-date // and specific to the room that this user profile sliding pane is currently being shown for. - if let Some(avatar_uri) = our_info.room_member.as_ref() + if let Some(avatar_uri) = our_info + .room_member + .as_ref() .and_then(|rm| rm.avatar_url().map(|u| u.to_owned())) { our_info.avatar_state = AvatarState::Known(Some(avatar_uri)); @@ -446,11 +469,11 @@ impl Widget for UserProfileSlidingPane { // If the new avatar state is fully `Loaded`, keep it as is. // If the new avatar state is *not* fully `Loaded`, but the previous one was, keep the previous one. match (prev_avatar_state, &mut our_info.avatar_state) { - (_, AvatarState::Loaded(_)) => { } - (prev @ AvatarState::Loaded(_), existing_avatar_state ) => { + (_, AvatarState::Loaded(_)) => {} + (prev @ AvatarState::Loaded(_), existing_avatar_state) => { *existing_avatar_state = prev; } - _ => { } + _ => {} } redraw_this_pane = true; } @@ -460,10 +483,15 @@ impl Widget for UserProfileSlidingPane { } } - let Some(info) = self.info.as_ref() else { return }; + let Some(info) = self.info.as_ref() else { + return; + }; if let Event::Actions(actions) = event { - if self.button(cx, ids!(direct_message_button)).clicked(actions) { + if self + .button(cx, ids!(direct_message_button)) + .clicked(actions) + { submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { user_profile: info.user_profile.clone(), // Don't just create a new DM room; we want to first get confirmation from the user. @@ -471,7 +499,10 @@ impl Widget for UserProfileSlidingPane { }); } - if self.button(cx, ids!(copy_link_to_user_button)).clicked(actions) { + if self + .button(cx, ids!(copy_link_to_user_button)) + .clicked(actions) + { let matrix_to_uri = info.user_id.matrix_to_uri().to_string(); cx.copy_to_clipboard(&matrix_to_uri); enqueue_popup_notification( @@ -493,7 +524,8 @@ impl Widget for UserProfileSlidingPane { room_id: info.room_id.clone(), room_member: room_member.clone(), }); - log!("Submitting request to {}ignore user {}.", + log!( + "Submitting request to {}ignore user {}.", if room_member.is_ignored() { "un" } else { "" }, info.user_id, ); @@ -502,7 +534,6 @@ impl Widget for UserProfileSlidingPane { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { let Some(info) = self.info.as_ref() else { self.visible = false; @@ -528,20 +559,29 @@ impl Widget for UserProfileSlidingPane { }); // Set the user name, using the user ID as a fallback. - self.label(cx, ids!(user_name)).set_text(cx, info.displayable_name()); - self.label(cx, ids!(user_id)).set_text(cx, info.user_id.as_str()); + self.label(cx, ids!(user_name)) + .set_text(cx, info.displayable_name()); + self.label(cx, ids!(user_id)) + .set_text(cx, info.user_id.as_str()); // Set the avatar image, using the user name as a fallback. let avatar_ref = self.avatar(cx, ids!(avatar)); info.avatar_state .data() - .and_then(|data| avatar_ref.show_image(cx, None, |cx, img| utils::load_png_or_jpg(&img, cx, data)).ok()) + .and_then(|data| { + avatar_ref + .show_image(cx, None, |cx, img| utils::load_png_or_jpg(&img, cx, data)) + .ok() + }) .unwrap_or_else(|| avatar_ref.show_text(cx, None, None, info.displayable_name())); // Set the membership status and role in the room. - self.label(cx, ids!(membership_title_label)).set_text(cx, &info.membership_title()); - self.label(cx, ids!(membership_status_label)).set_text(cx, info.membership_status()); - self.label(cx, ids!(role_info_label)).set_text(cx, info.role_in_room().as_ref()); + self.label(cx, ids!(membership_title_label)) + .set_text(cx, &info.membership_title()); + self.label(cx, ids!(membership_status_label)) + .set_text(cx, info.membership_status()); + self.label(cx, ids!(role_info_label)) + .set_text(cx, info.role_in_room().as_ref()); // Draw and show/hide the buttons according to user and room membership info: // * `direct_message_button` is hidden if the user is the same as the account user, @@ -551,28 +591,39 @@ impl Widget for UserProfileSlidingPane { // * `ignore_user_button` is hidden if the user is not a member of the room, // or if the user is the same as the account user, since you cannot ignore yourself. // * The button text changes to "Unignore" if the user is already ignored. - let is_pane_showing_current_account = info.room_member.as_ref() + let is_pane_showing_current_account = info + .room_member + .as_ref() .map(|rm| rm.is_account_user()) .unwrap_or_else(|| current_user_id().is_some_and(|uid| uid == info.user_id)); - self.button(cx, ids!(direct_message_button)).set_visible(cx, !is_pane_showing_current_account); + self.button(cx, ids!(direct_message_button)) + .set_visible(cx, !is_pane_showing_current_account); let ignore_user_button = self.button(cx, ids!(ignore_user_button)); - ignore_user_button.set_visible(cx, !is_pane_showing_current_account && info.room_member.is_some()); + ignore_user_button.set_visible( + cx, + !is_pane_showing_current_account && info.room_member.is_some(), + ); // Unfortunately the Matrix SDK's RoomMember type does not properly track // the `ignored` state of a user, so we have to maintain it separately. - let is_ignored = info.room_member.as_ref() + let is_ignored = info + .room_member + .as_ref() .is_some_and(|rm| is_user_ignored(rm.user_id())); ignore_user_button.set_text( cx, - if is_ignored { "Unignore (Unblock) User" } else { "Ignore (Block) User" } + if is_ignored { + "Unignore (Unblock) User" + } else { + "Ignore (Block) User" + }, ); self.view.draw_walk(cx, scope, walk) } } - impl UserProfileSlidingPane { /// Returns `true` if this pane is currently being shown. pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { @@ -592,14 +643,13 @@ impl UserProfileSlidingPane { info.user_id.clone(), Some(&info.room_id), true, - |profile, rooms| (profile.clone(), rooms.get(&info.room_id).cloned()) + |profile, rooms| (profile.clone(), rooms.get(&info.room_id).cloned()), ) { log!("Found user {} room member info in cache", info.user_id); // Update avatar state, preferring that of the room member info. if let Some(uri) = room_member.avatar_url() { info.avatar_state = AvatarState::Known(Some(uri.to_owned())); - } - else { + } else { match new_profile.avatar_state { s @ AvatarState::Known(Some(_)) | s @ AvatarState::Loaded(_) => { info.avatar_state = s.clone(); @@ -609,7 +659,8 @@ impl UserProfileSlidingPane { } // Update displayable username. if info.username.is_none() { - info.username = room_member.display_name() + info.username = room_member + .display_name() .map(|dn| dn.to_owned()) .or_else(|| new_profile.username.clone()); } @@ -619,9 +670,11 @@ impl UserProfileSlidingPane { info.avatar_state.update_from_cache(cx); // If TSP is enabled, populate the TSP verification info for this user. - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { use crate::tsp::verify_user::TspVerifyUserWidgetExt; - self.view.tsp_verify_user(cx, ids!(tsp_verify_user)) + self.view + .tsp_verify_user(cx, ids!(tsp_verify_user)) .show(cx, info.user_id.clone()); } @@ -636,10 +689,18 @@ impl UserProfileSlidingPane { self.view(cx, ids!(bg_view)).set_visible(cx, true); self.view.button(cx, ids!(close_button)).reset_hover(cx); - self.view.button(cx, ids!(direct_message_button)).reset_hover(cx); - self.view.button(cx, ids!(copy_link_to_user_button)).reset_hover(cx); - self.view.button(cx, ids!(jump_to_read_receipt_button)).reset_hover(cx); - self.view.button(cx, ids!(ignore_user_button)).reset_hover(cx); + self.view + .button(cx, ids!(direct_message_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(copy_link_to_user_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(jump_to_read_receipt_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(ignore_user_button)) + .reset_hover(cx); self.redraw(cx); } } @@ -647,19 +708,25 @@ impl UserProfileSlidingPane { impl UserProfileSlidingPaneRef { /// See [`UserProfileSlidingPane::is_currently_shown()`] pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { return false }; + let Some(inner) = self.borrow() else { + return false; + }; inner.is_currently_shown(cx) } /// See [`UserProfileSlidingPane::set_info()`] pub fn set_info(&self, cx: &mut Cx, info: UserProfilePaneInfo) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_info(cx, info); } /// See [`UserProfileSlidingPane::show()`] pub fn show(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx); } } diff --git a/src/profile/user_profile_cache.rs b/src/profile/user_profile_cache.rs index a669929e8..c57871276 100644 --- a/src/profile/user_profile_cache.rs +++ b/src/profile/user_profile_cache.rs @@ -4,10 +4,19 @@ use crossbeam_queue::SegQueue; use makepad_widgets::{warning, Cx, SignalToUI}; -use matrix_sdk::{room::RoomMember, ruma::{OwnedRoomId, OwnedUserId, UserId}}; -use std::{cell::RefCell, collections::{btree_map::Entry, BTreeMap}}; +use matrix_sdk::{ + room::RoomMember, + ruma::{OwnedRoomId, OwnedUserId, UserId}, +}; +use std::{ + cell::RefCell, + collections::{btree_map::Entry, BTreeMap}, +}; -use crate::{shared::avatar::AvatarState, sliding_sync::{submit_async_request, MatrixRequest}}; +use crate::{ + shared::avatar::AvatarState, + sliding_sync::{submit_async_request, MatrixRequest}, +}; use super::user_profile::UserProfile; @@ -67,47 +76,60 @@ impl UserProfileUpdate { /// Applies this update to the given user profile info cache. fn apply_to_cache(self, cache: &mut BTreeMap) { match self { - UserProfileUpdate::Full { new_profile, room_id, room_member } => { - match cache.entry(new_profile.user_id.clone()) { - Entry::Occupied(mut entry) => match entry.get_mut() { - e @ UserProfileCacheEntry::Requested => { - *e = UserProfileCacheEntry::Loaded { - user_profile: new_profile, - rooms: { - let mut room_members_map = BTreeMap::new(); - room_members_map.insert(room_id, room_member); - room_members_map - }, - }; - } - UserProfileCacheEntry::Loaded { user_profile, rooms } => { - *user_profile = new_profile; - rooms.insert(room_id, room_member); - } - } - Entry::Vacant(entry) => { - entry.insert(UserProfileCacheEntry::Loaded { + UserProfileUpdate::Full { + new_profile, + room_id, + room_member, + } => match cache.entry(new_profile.user_id.clone()) { + Entry::Occupied(mut entry) => match entry.get_mut() { + e @ UserProfileCacheEntry::Requested => { + *e = UserProfileCacheEntry::Loaded { user_profile: new_profile, rooms: { let mut room_members_map = BTreeMap::new(); room_members_map.insert(room_id, room_member); room_members_map }, - }); + }; + } + UserProfileCacheEntry::Loaded { + user_profile, + rooms, + } => { + *user_profile = new_profile; + rooms.insert(room_id, room_member); } + }, + Entry::Vacant(entry) => { + entry.insert(UserProfileCacheEntry::Loaded { + user_profile: new_profile, + rooms: { + let mut room_members_map = BTreeMap::new(); + room_members_map.insert(room_id, room_member); + room_members_map + }, + }); } - } - UserProfileUpdate::RoomMemberOnly { room_id, room_member } => { + }, + UserProfileUpdate::RoomMemberOnly { + room_id, + room_member, + } => { match cache.entry(room_member.user_id().to_owned()) { Entry::Occupied(mut entry) => match entry.get_mut() { e @ UserProfileCacheEntry::Requested => { // This shouldn't happen, but we can still technically handle it correctly. - warning!("BUG: User profile cache entry was `Requested` for user {} when handling RoomMemberOnly update", room_member.user_id()); + warning!( + "BUG: User profile cache entry was `Requested` for user {} when handling RoomMemberOnly update", + room_member.user_id() + ); *e = UserProfileCacheEntry::Loaded { user_profile: UserProfile { user_id: room_member.user_id().to_owned(), username: None, - avatar_state: AvatarState::Known(room_member.avatar_url().map(|url| url.to_owned())), + avatar_state: AvatarState::Known( + room_member.avatar_url().map(|url| url.to_owned()), + ), }, rooms: { let mut room_members_map = BTreeMap::new(); @@ -119,15 +141,20 @@ impl UserProfileUpdate { UserProfileCacheEntry::Loaded { rooms, .. } => { rooms.insert(room_id, room_member); } - } + }, Entry::Vacant(entry) => { // This shouldn't happen, but we can still technically handle it correctly. - warning!("BUG: User profile cache entry not found for user {} when handling RoomMemberOnly update", room_member.user_id()); + warning!( + "BUG: User profile cache entry not found for user {} when handling RoomMemberOnly update", + room_member.user_id() + ); entry.insert(UserProfileCacheEntry::Loaded { user_profile: UserProfile { user_id: room_member.user_id().to_owned(), username: None, - avatar_state: AvatarState::Known(room_member.avatar_url().map(|url| url.to_owned())), + avatar_state: AvatarState::Known( + room_member.avatar_url().map(|url| url.to_owned()), + ), }, rooms: { let mut room_members_map = BTreeMap::new(); @@ -150,7 +177,7 @@ impl UserProfileUpdate { UserProfileCacheEntry::Loaded { user_profile, .. } => { *user_profile = new_profile; } - } + }, Entry::Vacant(entry) => { entry.insert(UserProfileCacheEntry::Loaded { user_profile: new_profile, @@ -193,42 +220,42 @@ pub fn with_user_profile( where F: FnOnce(&UserProfile, &BTreeMap) -> R, { - USER_PROFILE_CACHE.with_borrow_mut(|cache| - match cache.entry(user_id) { - Entry::Occupied(entry) => match entry.get() { - UserProfileCacheEntry::Loaded { user_profile, rooms } => { - if room_id.is_some_and(|id| !rooms.contains_key(id)) { - submit_async_request(MatrixRequest::GetUserProfile { - user_id: entry.key().clone(), - room_id: room_id.cloned(), - local_only: false, - }); - } - Some(f(user_profile, rooms)) - } - UserProfileCacheEntry::Requested => { - // log!("User {} profile request is already in flight....", entry.key()); - None - } - } - Entry::Vacant(entry) => { - if fetch_if_missing { - // log!("Did not find User {} in cache, fetching from server.", entry.key()); - // TODO: use the extra `via` parameters from `matrix_to_uri.via()`. + USER_PROFILE_CACHE.with_borrow_mut(|cache| match cache.entry(user_id) { + Entry::Occupied(entry) => match entry.get() { + UserProfileCacheEntry::Loaded { + user_profile, + rooms, + } => { + if room_id.is_some_and(|id| !rooms.contains_key(id)) { submit_async_request(MatrixRequest::GetUserProfile { user_id: entry.key().clone(), room_id: room_id.cloned(), local_only: false, }); - entry.insert(UserProfileCacheEntry::Requested); } + Some(f(user_profile, rooms)) + } + UserProfileCacheEntry::Requested => { + // log!("User {} profile request is already in flight....", entry.key()); None } + }, + Entry::Vacant(entry) => { + if fetch_if_missing { + // log!("Did not find User {} in cache, fetching from server.", entry.key()); + // TODO: use the extra `via` parameters from `matrix_to_uri.via()`. + submit_async_request(MatrixRequest::GetUserProfile { + user_id: entry.key().clone(), + room_id: room_id.cloned(), + local_only: false, + }); + entry.insert(UserProfileCacheEntry::Requested); + } + None } - ) + }) } - /// Returns the given user's displayable name (optionally in the given room), /// using the user's account-wide displayable name as a fallback. /// @@ -276,8 +303,7 @@ impl CachedName { pub fn as_deref(&self) -> Option<&str> { match self { - CachedName::FoundInRoom(name) - | CachedName::FoundInProfile(name) => name.as_deref(), + CachedName::FoundInRoom(name) | CachedName::FoundInProfile(name) => name.as_deref(), CachedName::NotFound => None, } } @@ -294,7 +320,7 @@ impl From for Option { /// Clears cached user profile. /// This function requires passing in a reference to `Cx`, -/// which acts as a guarantee that these thread-local caches are cleared on the main UI thread, +/// which acts as a guarantee that these thread-local caches are cleared on the main UI thread, pub fn clear_user_profile_cache(_cx: &mut Cx) { // Clear user profile cache USER_PROFILE_CACHE.with_borrow_mut(|cache| { diff --git a/src/room/mod.rs b/src/room/mod.rs index 68b20bae9..e09e9407c 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use makepad_widgets::ScriptVm; use matrix_sdk::{RoomDisplayName, RoomHero, RoomState, SuccessorRoom, room_preview::RoomPreview}; -use ruma::{OwnedRoomAliasId, OwnedRoomId, room::{JoinRuleSummary, RoomType}}; +use ruma::{ + OwnedRoomAliasId, OwnedRoomId, + room::{JoinRuleSummary, RoomType}, +}; use crate::utils::RoomNameId; @@ -50,7 +53,7 @@ impl From<&SuccessorRoom> for BasicRoomDetails { } impl From for BasicRoomDetails { fn from(frp: FetchedRoomPreview) -> Self { - BasicRoomDetails::FetchedRoomPreview(frp) + BasicRoomDetails::FetchedRoomPreview(frp) } } impl BasicRoomDetails { @@ -58,7 +61,7 @@ impl BasicRoomDetails { match self { Self::RoomId(room_name_id) | Self::Name(room_name_id) - | Self::NameAndAvatar { room_name_id, ..} => room_name_id.room_id(), + | Self::NameAndAvatar { room_name_id, .. } => room_name_id.room_id(), Self::FetchedRoomPreview(frp) => frp.room_name_id.room_id(), } } @@ -80,15 +83,13 @@ impl BasicRoomDetails { /// If this is the `RoomId` or `Name` variants, the avatar will be empty. pub fn room_avatar(&self) -> &FetchedRoomAvatar { match self { - Self::RoomId(_) - | Self::Name(_) => &EMPTY_AVATAR, - Self::NameAndAvatar { room_avatar, ..} => room_avatar, + Self::RoomId(_) | Self::Name(_) => &EMPTY_AVATAR, + Self::NameAndAvatar { room_avatar, .. } => room_avatar, Self::FetchedRoomPreview(frp) => &frp.room_avatar, } } } - /// Actions related to room previews being fetched. #[derive(Debug)] pub enum RoomPreviewAction { @@ -104,7 +105,6 @@ pub struct FetchedRoomPreview { pub room_avatar: FetchedRoomAvatar, // Below: copied from the `RoomPreview` struct. - /// The canonical alias for the room. pub canonical_alias: Option, /// The room's topic, if set. @@ -131,10 +131,9 @@ pub struct FetchedRoomPreview { } impl FetchedRoomPreview { pub fn from(room_preview: RoomPreview, room_avatar: FetchedRoomAvatar) -> Self { - let display_name = room_preview.name.map_or( - RoomDisplayName::Empty, - RoomDisplayName::Named, - ); + let display_name = room_preview + .name + .map_or(RoomDisplayName::Empty, RoomDisplayName::Named); Self { room_name_id: RoomNameId::new(display_name, room_preview.room_id), room_avatar, @@ -152,7 +151,6 @@ impl FetchedRoomPreview { } } - static EMPTY_AVATAR: FetchedRoomAvatar = FetchedRoomAvatar::Text(String::new()); /// A fully-fetched room avatar ready to be displayed. diff --git a/src/room/room_display_filter.rs b/src/room/room_display_filter.rs index acbc17edb..5c38e6d49 100644 --- a/src/room/room_display_filter.rs +++ b/src/room/room_display_filter.rs @@ -1,12 +1,22 @@ use std::{ - borrow::Cow, cmp::Ordering, collections::{BTreeMap, HashSet}, ops::Deref + borrow::Cow, + cmp::Ordering, + collections::{BTreeMap, HashSet}, + ops::Deref, }; use bitflags::bitflags; -use matrix_sdk::{RoomDisplayName, ruma::{ - OwnedRoomAliasId, RoomAliasId, RoomId, events::tag::{TagName, Tags} -}}; +use matrix_sdk::{ + RoomDisplayName, + ruma::{ + OwnedRoomAliasId, RoomAliasId, RoomId, + events::tag::{TagName, Tags}, + }, +}; -use crate::{home::rooms_list::{InvitedRoomInfo, JoinedRoomInfo}, home::spaces_bar::JoinedSpaceInfo}; +use crate::{ + home::rooms_list::{InvitedRoomInfo, JoinedRoomInfo}, + home::spaces_bar::JoinedSpaceInfo, +}; static EMPTY_TAGS: Tags = BTreeMap::new(); @@ -142,7 +152,6 @@ impl FilterableRoom for JoinedSpaceInfo { } } - pub type RoomFilterFn = dyn Fn(&dyn FilterableRoom) -> bool; pub type SortFn = dyn Fn(&dyn FilterableRoom, &dyn FilterableRoom) -> Ordering; @@ -245,18 +254,16 @@ impl RoomDisplayFilterBuilder { } fn matches_room_name(room: &dyn FilterableRoom, keywords: &str) -> bool { - room.room_name() - .to_lowercase() - .contains(keywords) + room.room_name().to_lowercase().contains(keywords) } fn matches_room_alias(room: &dyn FilterableRoom, keywords: &str) -> bool { room.canonical_alias() .is_some_and(|alias| alias.as_str().eq_ignore_ascii_case(keywords)) - || - room.alt_aliases() - .iter() - .any(|alias| alias.as_str().eq_ignore_ascii_case(keywords)) + || room + .alt_aliases() + .iter() + .any(|alias| alias.as_str().eq_ignore_ascii_case(keywords)) } fn matches_room_tags(room: &dyn FilterableRoom, search_tags: &HashSet) -> bool { @@ -267,10 +274,13 @@ impl RoomDisplayFilterBuilder { ["low_priority", "low-priority", "lowpriority", "lowPriority"] .contains(&search_tag) } - TagName::ServerNotice => { - ["server_notice", "server-notice", "servernotice", "serverNotice"] - .contains(&search_tag) - } + TagName::ServerNotice => [ + "server_notice", + "server-notice", + "servernotice", + "serverNotice", + ] + .contains(&search_tag), TagName::User(user_tag) => user_tag.as_ref().eq_ignore_ascii_case(search_tag), _ => false, } @@ -316,10 +326,14 @@ impl RoomDisplayFilterBuilder { RoomFilterCriteria::RoomId if criteria.contains(RoomFilterCriteria::RoomId) => { Self::matches_room_id(room, &keywords) } - RoomFilterCriteria::RoomAlias if criteria.contains(RoomFilterCriteria::RoomAlias) => { + RoomFilterCriteria::RoomAlias + if criteria.contains(RoomFilterCriteria::RoomAlias) => + { Self::matches_room_alias(room, &keywords) } - RoomFilterCriteria::RoomTags if criteria.contains(RoomFilterCriteria::RoomTags) => { + RoomFilterCriteria::RoomTags + if criteria.contains(RoomFilterCriteria::RoomTags) => + { Self::matches_room_tags(room, &search_tags) } _ => false, diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 93b8d4a9d..614017021 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -15,12 +15,33 @@ //! * A "cannot-send-message" notice, which is shown if the user cannot send messages to the room. //! - 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 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, +}; script_mod! { use mod.prelude.widgets.* @@ -161,14 +182,18 @@ script_mod! { /// Main component for message input with @mention support #[derive(Script, ScriptHook, Widget)] pub struct RoomInputBar { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, /// Whether the `ReplyingPreview` was visible when the `EditingPane` was shown. /// If true, when the `EditingPane` gets hidden, we need to re-show the `ReplyingPreview`. - #[rust] was_replying_preview_visible: bool, + #[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)>, + #[rust] + replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, } impl Widget for RoomInputBar { @@ -178,14 +203,21 @@ impl Widget for RoomInputBar { .get::() .expect("BUG: RoomScreenProps should be available in Scope::props for RoomInputBar"); - match event.hits(cx, self.view.view(cx, ids!(replying_preview.reply_preview_content)).area()) { + match event.hits( + cx, + self.view + .view(cx, ids!(replying_preview.reply_preview_content)) + .area(), + ) { // If the hit occurred on the replying message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { - if let Some(event_id) = self.replying_to.as_ref() + if let Some(event_id) = self + .replying_to + .as_ref() .and_then(|(event_tl_item, _)| event_tl_item.event_id().map(ToOwned::to_owned)) { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::JumpToEvent(event_id), ); } else { @@ -241,40 +273,56 @@ impl RoomInputBar { None, ); } - self.view.location_preview(cx, ids!(location_preview)).show(); + self.view + .location_preview(cx, ids!(location_preview)) + .show(); self.redraw(cx); } // Handle the send location button being clicked. - if self.button(cx, ids!(location_preview.send_location_button)).clicked(actions) { + if self + .button(cx, ids!(location_preview.send_location_button)) + .clicked(actions) + { let location_preview = self.location_preview(cx, ids!(location_preview)); if let Some((coords, _system_time_opt)) = location_preview.get_current_data() { - let geo_uri = format!("{}{},{}", utils::GEO_URI_SCHEME, coords.latitude, coords.longitude); - let message = RoomMessageEventContent::new( - MessageType::Location( - LocationMessageEventContent::new(geo_uri.clone(), geo_uri) - ) + let geo_uri = format!( + "{}{},{}", + utils::GEO_URI_SCHEME, + coords.latitude, + coords.longitude ); - let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } + let message = RoomMessageEventContent::new(MessageType::Location( + LocationMessageEventContent::new(geo_uri.clone(), geo_uri), + )); + let replied_to = self + .replying_to + .take() + .and_then(|(event_tl_item, _emb)| { + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } + }) }) - ).or_else(|| - room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| - Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - } - ) - ); + .or_else(|| { + room_screen_props.timeline_kind.thread_root_event_id().map( + |thread_root_event_id| Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + }, + ) + }); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -291,31 +339,41 @@ impl RoomInputBar { // Handle the send message button being clicked or Cmd/Ctrl + Return being pressed. if self.button(cx, ids!(send_message_button)).clicked(actions) - || text_input.returned(actions).is_some_and(|(_, m)| m.is_primary()) + || text_input + .returned(actions) + .is_some_and(|(_, m)| m.is_primary()) { let entered_text = mentionable_text_input.text().trim().to_string(); if !entered_text.is_empty() { let message = mentionable_text_input.create_message_with_mentions(&entered_text); - let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } + let replied_to = self + .replying_to + .take() + .and_then(|(event_tl_item, _emb)| { + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } + }) }) - ).or_else(|| - room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| - Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - } - ) - ); + .or_else(|| { + room_screen_props.timeline_kind.thread_root_event_id().map( + |thread_root_event_id| Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + }, + ) + }); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -349,18 +407,29 @@ impl RoomInputBar { if is_text_input_empty { if let Some(KeyEvent { key_code: KeyCode::ArrowUp, - modifiers: KeyModifiers { shift: false, control: false, alt: false, logo: false }, + modifiers: + KeyModifiers { + shift: false, + control: false, + alt: false, + logo: false, + }, .. - }) = text_input.key_down_unhandled(actions) { + }) = text_input.key_down_unhandled(actions) + { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::EditLatest, ); } } // If the EditingPane has been hidden, handle that. - if self.view.editing_pane(cx, ids!(editing_pane)).was_hidden(actions) { + if self + .view + .editing_pane(cx, ids!(editing_pane)) + .was_hidden(actions) + { self.on_editing_pane_hidden(cx); } } @@ -408,13 +477,15 @@ impl RoomInputBar { // 2. Hide other views that are irrelevant to a reply, e.g., // the `EditingPane` would improperly cover up the ReplyPreview. - self.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); + self.editing_pane(cx, ids!(editing_pane)) + .force_reset_hide(cx); self.on_editing_pane_hidden(cx); // 3. Automatically focus the keyboard on the message input box // so that the user can immediately start typing their reply // without having to manually click on the message input box. if grab_key_focus { - self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)).set_key_focus(cx); + self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + .set_key_focus(cx); } self.button(cx, ids!(cancel_reply_button)).reset_hover(cx); self.redraw(cx); @@ -444,7 +515,9 @@ impl RoomInputBar { let replying_preview = self.view.view(cx, ids!(replying_preview)); self.was_replying_preview_visible = replying_preview.visible(); replying_preview.set_visible(cx, false); - self.view.location_preview(cx, ids!(location_preview)).clear(); + self.view + .location_preview(cx, ids!(location_preview)) + .clear(); let editing_pane = self.view.editing_pane(cx, ids!(editing_pane)); match behavior { @@ -466,12 +539,14 @@ impl RoomInputBar { // Same goes for the replying_preview, if it was previously shown. self.view.view(cx, ids!(input_bar)).set_visible(cx, true); if self.was_replying_preview_visible && self.replying_to.is_some() { - self.view.view(cx, ids!(replying_preview)).set_visible(cx, true); + self.view + .view(cx, ids!(replying_preview)) + .set_visible(cx, true); } self.redraw(cx); // We don't need to do anything with the editing pane itself here, // because it has already been hidden by the time this function gets called. - } + } /// Updates (populates and shows or hides) this room's tombstone footer /// based on the given successor room details. @@ -489,7 +564,10 @@ impl RoomInputBar { input_bar.set_visible(cx, false); } else { tombstone_footer.hide(cx); - if !self.editing_pane(cx, ids!(editing_pane)).is_currently_shown(cx) { + if !self + .editing_pane(cx, ids!(editing_pane)) + .is_currently_shown(cx) + { input_bar.set_visible(cx, true); } } @@ -515,14 +593,14 @@ impl RoomInputBar { /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - fn update_user_power_levels( - &mut self, - cx: &mut Cx, - user_power_levels: UserPowerLevels, - ) { + fn update_user_power_levels(&mut self, cx: &mut Cx, user_power_levels: UserPowerLevels) { let can_send = user_power_levels.can_send_message(); - self.view.view(cx, ids!(input_bar)).set_visible(cx, can_send); - self.view.view(cx, ids!(can_not_send_message_notice)).set_visible(cx, !can_send); + self.view + .view(cx, ids!(input_bar)) + .set_visible(cx, can_send); + self.view + .view(cx, ids!(can_not_send_message_notice)) + .set_visible(cx, !can_send); } /// Returns true if the TSP signing checkbox is checked, false otherwise. @@ -543,7 +621,9 @@ impl RoomInputBarRef { replying_to: (EventTimelineItem, EmbeddedEvent), timeline_kind: &TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_replying_to(cx, replying_to, timeline_kind, true); } @@ -554,7 +634,9 @@ impl RoomInputBarRef { event_tl_item: EventTimelineItem, timeline_kind: TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_editing_pane( cx, ShowEditingPaneBehavior::ShowNew { event_tl_item }, @@ -565,12 +647,10 @@ impl RoomInputBarRef { /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - pub fn update_user_power_levels( - &self, - cx: &mut Cx, - user_power_levels: UserPowerLevels, - ) { - let Some(mut inner) = self.borrow_mut() else { return }; + pub fn update_user_power_levels(&self, cx: &mut Cx, user_power_levels: UserPowerLevels) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.update_user_power_levels(cx, user_power_levels); } @@ -581,7 +661,9 @@ impl RoomInputBarRef { tombstoned_room_id: &OwnedRoomId, successor_room_details: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.update_tombstone_footer(cx, tombstoned_room_id, successor_room_details); } @@ -593,22 +675,36 @@ impl RoomInputBarRef { timeline_event_item_id: TimelineEventItemId, edit_result: Result<(), matrix_sdk_ui::timeline::Error>, ) { - let Some(inner) = self.borrow_mut() else { return }; - inner.editing_pane(cx, ids!(editing_pane)) + let Some(inner) = self.borrow_mut() else { + return; + }; + inner + .editing_pane(cx, ids!(editing_pane)) .handle_edit_result(cx, timeline_event_item_id, edit_result); } /// Save a snapshot of the UI state of this `RoomInputBar`. pub fn save_state(&self) -> RoomInputBarState { - let Some(inner) = self.borrow() else { return Default::default() }; + let Some(inner) = self.borrow() else { + return Default::default(); + }; // Clear the location preview. We don't save this state because the // current location might change by the next time the user opens this same room. - inner.child_by_path(ids!(location_preview)).as_location_preview().clear(); + inner + .child_by_path(ids!(location_preview)) + .as_location_preview() + .clear(); RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), - editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), - text_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input.text_input)).as_text_input().save_state(), + editing_pane_state: inner + .child_by_path(ids!(editing_pane)) + .as_editing_pane() + .save_state(), + text_input_state: inner + .child_by_path(ids!(input_bar.mentionable_text_input.text_input)) + .as_text_input() + .save_state(), } } @@ -621,7 +717,9 @@ impl RoomInputBarRef { user_power_levels: UserPowerLevels, tombstone_info: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; let RoomInputBarState { was_replying_preview_visible, text_input_state, @@ -637,7 +735,8 @@ impl RoomInputBarRef { inner.update_user_power_levels(cx, user_power_levels); // 1. Restore the state of the TextInput within the MentionableTextInput. - inner.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + inner + .text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) .restore_state(cx, text_input_state); // 2. Restore the state of the replying-to preview. @@ -656,7 +755,9 @@ impl RoomInputBarRef { timeline_kind.clone(), ); } else { - inner.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); + inner + .editing_pane(cx, ids!(editing_pane)) + .force_reset_hide(cx); inner.on_editing_pane_hidden(cx); } @@ -682,9 +783,7 @@ pub struct RoomInputBarState { /// Defines what to do when showing the `EditingPane` from the `RoomInputBar`. enum ShowEditingPaneBehavior { /// Show a new edit session, e.g., when first clicking "edit" on a message. - ShowNew { - event_tl_item: EventTimelineItem, - }, + ShowNew { event_tl_item: EventTimelineItem }, /// Restore the state of an `EditingPane` that already existed, e.g., when /// reopening a room that had an `EditingPane` open when it was closed. RestoreExisting { diff --git a/src/room/typing_notice.rs b/src/room/typing_notice.rs index 55fad31bd..437a25b70 100644 --- a/src/room/typing_notice.rs +++ b/src/room/typing_notice.rs @@ -62,9 +62,12 @@ script_mod! { /// A notice that slides into view when someone is typing. #[derive(Script, ScriptHook, Widget, Animator)] pub struct TypingNotice { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for TypingNotice { @@ -87,7 +90,9 @@ impl TypingNotice { [] => { // Animate out the typing notice view (sliding it out towards the bottom). self.animator_play(cx, ids!(typing_notice_animator.hide)); - self.view.bouncing_dots(cx, ids!(bouncing_dots)).stop_animation(cx); + self.view + .bouncing_dots(cx, ids!(bouncing_dots)) + .stop_animation(cx); return; } [user] => format!("{user} is typing "), @@ -96,20 +101,21 @@ impl TypingNotice { if others.len() > 1 { format!("{user1}, {user2}, and {} are typing ", &others[0]) } else { - format!( - "{user1}, {user2}, and {} others are typing ", - others.len() - ) + format!("{user1}, {user2}, and {} others are typing ", others.len()) } } }; // Set the typing notice text and make its view visible. - self.view.label(cx, ids!(typing_label)).set_text(cx, &typing_notice_text); + self.view + .label(cx, ids!(typing_label)) + .set_text(cx, &typing_notice_text); self.view.set_visible(cx, true); // Animate in the typing notice view (sliding it up from the bottom). self.animator_play(cx, ids!(typing_notice_animator.show)); // Start the typing notice text animation of bouncing dots. - self.view.bouncing_dots(cx, ids!(bouncing_dots)).start_animation(cx); + self.view + .bouncing_dots(cx, ids!(bouncing_dots)) + .start_animation(cx); } } diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 877f66bfc..b0d82cd9b 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -2,7 +2,20 @@ use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; -use crate::{app::ConfirmDeleteAction, avatar_cache::{self}, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, utils}; +use crate::{ + app::ConfirmDeleteAction, + avatar_cache::{self}, + logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, + profile::user_profile::UserProfile, + shared::{ + avatar::{AvatarState, AvatarWidgetExt}, + confirmation_modal::ConfirmationModalContent, + popup_list::{PopupKind, enqueue_popup_notification}, + styles::*, + }, + sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, + utils, +}; script_mod! { use mod.prelude.widgets.* @@ -207,9 +220,11 @@ script_mod! { /// The view containing all user account-related settings. #[derive(Script, ScriptHook, Widget)] pub struct AccountSettings { - #[deref] view: View, + #[deref] + view: View, - #[rust] own_profile: Option, + #[rust] + own_profile: Option, } impl Widget for AccountSettings { @@ -221,7 +236,7 @@ impl Widget for AccountSettings { match event.hits(cx, copy_user_id_button_area) { Hit::FingerHoverIn(_) | Hit::FingerLongPress(_) => { cx.widget_action( - copy_user_id_button.widget_uid(), + copy_user_id_button.widget_uid(), TooltipAction::HoverIn { text: "Copy User ID".to_string(), widget_rect: copy_user_id_button_area.rect(cx), @@ -233,10 +248,7 @@ impl Widget for AccountSettings { ); } Hit::FingerHoverOut(_) => { - cx.widget_action( - copy_user_id_button.widget_uid(), - TooltipAction::HoverOut, - ); + cx.widget_action(copy_user_id_button.widget_uid(), TooltipAction::HoverOut); } _ => {} } @@ -272,7 +284,14 @@ impl MatchEvent for AccountSettings { // Handle LogoutAction::InProgress to update button state if let Some(LogoutAction::InProgress(is_in_progress)) = action.downcast_ref() { let logout_button = self.view.button(cx, ids!(logout_button)); - logout_button.set_text(cx, if *is_in_progress { "Logging out..." } else { "Log out" }); + logout_button.set_text( + cx, + if *is_in_progress { + "Logging out..." + } else { + "Log out" + }, + ); logout_button.set_enabled(cx, !*is_in_progress); logout_button.reset_hover(cx); continue; @@ -283,15 +302,26 @@ impl MatchEvent for AccountSettings { // so here, we only need to update this widget's local profile info. match action.downcast_ref() { Some(AccountDataAction::AvatarChanged(new_avatar_url)) => { - self.view.widget(cx, ids!(upload_avatar_spinner)).set_visible(cx, false); - self.view.widget(cx, ids!(delete_avatar_spinner)).set_visible(cx, false); + self.view + .widget(cx, ids!(upload_avatar_spinner)) + .set_visible(cx, false); + self.view + .widget(cx, ids!(delete_avatar_spinner)) + .set_visible(cx, false); // Update our cached profile with the new avatar URL if let Some(profile) = self.own_profile.as_mut() { profile.avatar_state = AvatarState::Known(new_avatar_url.clone()); profile.avatar_state.update_from_cache(cx); self.populate_avatar_views(cx); enqueue_popup_notification( - format!("Successfully {} avatar.", if new_avatar_url.is_some() { "updated" } else { "deleted" }), + format!( + "Successfully {} avatar.", + if new_avatar_url.is_some() { + "updated" + } else { + "deleted" + } + ), PopupKind::Success, Some(4.0), ); @@ -299,53 +329,82 @@ impl MatchEvent for AccountSettings { continue; } Some(AccountDataAction::AvatarChangeFailed(err_msg)) => { - self.view.widget(cx, ids!(upload_avatar_spinner)).set_visible(cx, false); - self.view.widget(cx, ids!(delete_avatar_spinner)).set_visible(cx, false); + self.view + .widget(cx, ids!(upload_avatar_spinner)) + .set_visible(cx, false); + self.view + .widget(cx, ids!(delete_avatar_spinner)) + .set_visible(cx, false); // Re-enable the avatar buttons so user can try again Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); Self::enable_delete_avatar_button( cx, - self.own_profile.as_ref().is_some_and(|p| p.avatar_state.has_avatar()), - &delete_avatar_button - ); - enqueue_popup_notification( - err_msg.clone(), - PopupKind::Error, - Some(4.0), + self.own_profile + .as_ref() + .is_some_and(|p| p.avatar_state.has_avatar()), + &delete_avatar_button, ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, Some(4.0)); continue; } Some(AccountDataAction::DisplayNameChanged(new_name)) => { - self.view.widget(cx, ids!(save_name_spinner)).set_visible(cx, false); + self.view + .widget(cx, ids!(save_name_spinner)) + .set_visible(cx, false); // Update our cached profile with the new display name if let Some(profile) = self.own_profile.as_mut() { profile.username = new_name.clone(); } // Update the display name text input and disable buttons - let (text, len) = new_name.as_deref().map(|s| (s, s.len())).unwrap_or_default(); + let (text, len) = new_name + .as_deref() + .map(|s| (s, s.len())) + .unwrap_or_default(); display_name_input.set_text(cx, text); - display_name_input.set_cursor(cx, Cursor { index: len, prefer_next_row: false }, false); + display_name_input.set_cursor( + cx, + Cursor { + index: len, + prefer_next_row: false, + }, + false, + ); display_name_input.set_is_read_only(cx, false); display_name_input.set_disabled(cx, false); - Self::enable_display_name_buttons(cx, false, &accept_display_name_button, &cancel_display_name_button); + Self::enable_display_name_buttons( + cx, + false, + &accept_display_name_button, + &cancel_display_name_button, + ); enqueue_popup_notification( - format!("Successfully {} display name.", if new_name.is_some() { "updated" } else { "removed" }), + format!( + "Successfully {} display name.", + if new_name.is_some() { + "updated" + } else { + "removed" + } + ), PopupKind::Success, Some(4.0), ); continue; } Some(AccountDataAction::DisplayNameChangeFailed(err_msg)) => { - self.view.widget(cx, ids!(save_name_spinner)).set_visible(cx, false); + self.view + .widget(cx, ids!(save_name_spinner)) + .set_visible(cx, false); // Re-enable the buttons and text input so that the user can try again display_name_input.set_is_read_only(cx, false); display_name_input.set_disabled(cx, false); - Self::enable_display_name_buttons(cx, true, &accept_display_name_button, &cancel_display_name_button); - enqueue_popup_notification( - err_msg.clone(), - PopupKind::Error, - Some(4.0), + Self::enable_display_name_buttons( + cx, + true, + &accept_display_name_button, + &cancel_display_name_button, ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, Some(4.0)); continue; } _ => {} @@ -353,13 +412,17 @@ impl MatchEvent for AccountSettings { match action.downcast_ref() { Some(AccountSettingsAction::AvatarDeleteStarted) => { - self.view.widget(cx, ids!(delete_avatar_spinner)).set_visible(cx, true); + self.view + .widget(cx, ids!(delete_avatar_spinner)) + .set_visible(cx, true); Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); continue; } Some(AccountSettingsAction::AvatarUploadStarted) => { - self.view.widget(cx, ids!(upload_avatar_spinner)).set_visible(cx, true); + self.view + .widget(cx, ids!(upload_avatar_spinner)) + .set_visible(cx, true); Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); continue; @@ -368,7 +431,9 @@ impl MatchEvent for AccountSettings { } } - let Some(own_profile) = &self.own_profile else { return }; + let Some(own_profile) = &self.own_profile else { + return; + }; if upload_avatar_button.clicked(actions) { // TODO: uncomment the below once avatar uploading is implemented @@ -408,15 +473,32 @@ impl MatchEvent for AccountSettings { let trimmed = new_name.trim(); let current_name = own_profile.username.as_deref().unwrap_or(""); let enable = trimmed != current_name; - Self::enable_display_name_buttons(cx, enable, &accept_display_name_button, &cancel_display_name_button); + Self::enable_display_name_buttons( + cx, + enable, + &accept_display_name_button, + &cancel_display_name_button, + ); } if cancel_display_name_button.clicked(actions) { // Reset the display name input and disable the name change buttons. let new_text = own_profile.username.as_deref().unwrap_or(""); display_name_input.set_text(cx, new_text); - display_name_input.set_cursor(cx, Cursor { index: new_text.len(), prefer_next_row: false }, false); - Self::enable_display_name_buttons(cx, false, &accept_display_name_button, &cancel_display_name_button); + display_name_input.set_cursor( + cx, + Cursor { + index: new_text.len(), + prefer_next_row: false, + }, + false, + ); + Self::enable_display_name_buttons( + cx, + false, + &accept_display_name_button, + &cancel_display_name_button, + ); } if accept_display_name_button.clicked(actions) { @@ -426,18 +508,25 @@ impl MatchEvent for AccountSettings { }; // While the request is in flight, show the loading spinner and disable the buttons & text input submit_async_request(MatrixRequest::SetDisplayName { new_display_name }); - self.view.widget(cx, ids!(save_name_spinner)).set_visible(cx, true); + self.view + .widget(cx, ids!(save_name_spinner)) + .set_visible(cx, true); display_name_input.set_disabled(cx, true); display_name_input.set_is_read_only(cx, true); - Self::enable_display_name_buttons(cx, false, &accept_display_name_button, &cancel_display_name_button); - enqueue_popup_notification( - "Uploading new display name...", - PopupKind::Info, - Some(5.0), + Self::enable_display_name_buttons( + cx, + false, + &accept_display_name_button, + &cancel_display_name_button, ); + enqueue_popup_notification("Uploading new display name...", PopupKind::Info, Some(5.0)); } - if self.view.button(cx, ids!(copy_user_id_button)).clicked(actions) { + if self + .view + .button(cx, ids!(copy_user_id_button)) + .clicked(actions) + { cx.copy_to_clipboard(own_profile.user_id.as_str()); enqueue_popup_notification( "Copied your User ID to the clipboard.", @@ -446,7 +535,11 @@ impl MatchEvent for AccountSettings { ); } - if self.view.button(cx, ids!(manage_account_button)).clicked(actions) { + if self + .view + .button(cx, ids!(manage_account_button)) + .clicked(actions) + { // TODO: support opening the user's account management page in a browser, // or perhaps in an in-app pane if that's what is needed for regular UN+PW login. enqueue_popup_notification( @@ -475,11 +568,13 @@ impl AccountSettings { let our_own_avatar = self.view.avatar(cx, ids!(our_own_avatar)); let mut drew_avatar = false; if let Some(avatar_img_data) = own_profile.avatar_state.data() { - drew_avatar = our_own_avatar.show_image( - cx, - None, // don't make this avatar clickable; we handle clicks on this ProfileIcon widget directly. - |cx, img| utils::load_png_or_jpg(&img, cx, avatar_img_data), - ).is_ok(); + drew_avatar = our_own_avatar + .show_image( + cx, + None, // don't make this avatar clickable; we handle clicks on this ProfileIcon widget directly. + |cx, img| utils::load_png_or_jpg(&img, cx, avatar_img_data), + ) + .is_ok(); } if !drew_avatar { our_own_avatar.show_text( @@ -493,20 +588,22 @@ impl AccountSettings { Self::enable_upload_avatar_button( cx, true, - &self.view.button(cx, ids!(upload_avatar_button)) + &self.view.button(cx, ids!(upload_avatar_button)), ); Self::enable_delete_avatar_button( cx, own_profile.avatar_state.has_avatar(), - &self.view.button(cx, ids!(delete_avatar_button)) + &self.view.button(cx, ids!(delete_avatar_button)), ); } /// Show and initializes the account settings within the SettingsScreen. pub fn populate(&mut self, cx: &mut Cx, own_profile: UserProfile) { - self.view.label(cx, ids!(user_id)) + self.view + .label(cx, ids!(user_id)) .set_text(cx, own_profile.user_id.as_str()); - self.view.text_input(cx, ids!(display_name_input)) + self.view + .text_input(cx, ids!(display_name_input)) .set_text(cx, own_profile.username.as_deref().unwrap_or_default()); Self::enable_display_name_buttons( cx, @@ -518,22 +615,30 @@ impl AccountSettings { self.own_profile = Some(own_profile); self.populate_avatar_views(cx); - self.view.button(cx, ids!(upload_avatar_button)).reset_hover(cx); - self.view.button(cx, ids!(delete_avatar_button)).reset_hover(cx); - self.view.button(cx, ids!(accept_display_name_button)).reset_hover(cx); - self.view.button(cx, ids!(cancel_display_name_button)).reset_hover(cx); - self.view.button(cx, ids!(copy_user_id_button)).reset_hover(cx); - self.view.button(cx, ids!(manage_account_button)).reset_hover(cx); + self.view + .button(cx, ids!(upload_avatar_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(delete_avatar_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(accept_display_name_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(cancel_display_name_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(copy_user_id_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(manage_account_button)) + .reset_hover(cx); self.view.button(cx, ids!(logout_button)).reset_hover(cx); self.view.redraw(cx); } /// Enable or disable the delete avatar button. - fn enable_delete_avatar_button( - cx: &mut Cx, - enable: bool, - delete_avatar_button: &ButtonRef, - ) { + fn enable_delete_avatar_button(cx: &mut Cx, enable: bool, delete_avatar_button: &ButtonRef) { let (delete_button_fg_color, delete_button_bg_color) = if enable { (COLOR_FG_DANGER_RED, COLOR_BG_DANGER_RED) } else { @@ -556,11 +661,7 @@ impl AccountSettings { } /// Enable or disable the upload avatar button. - fn enable_upload_avatar_button( - cx: &mut Cx, - enable: bool, - upload_avatar_button: &ButtonRef, - ) { + fn enable_upload_avatar_button(cx: &mut Cx, enable: bool, upload_avatar_button: &ButtonRef) { let (upload_button_fg_color, upload_button_bg_color) = if enable { (COLOR_PRIMARY, COLOR_ACTIVE_PRIMARY) } else { @@ -634,7 +735,9 @@ impl AccountSettings { impl AccountSettingsRef { /// See [`AccountSettings::show()`]. pub fn populate(&self, cx: &mut Cx, own_profile: UserProfile) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.populate(cx, own_profile); } } diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 24baf849d..201ae14cc 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,7 +1,10 @@ - use makepad_widgets::*; -use crate::{home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, profile::user_profile::UserProfile, settings::account_settings::AccountSettingsWidgetExt}; +use crate::{ + home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, + profile::user_profile::UserProfile, + settings::account_settings::AccountSettingsWidgetExt, +}; script_mod! { use mod.prelude.widgets.* @@ -84,11 +87,11 @@ script_mod! { } } - /// The top-level widget showing all app and user settings/preferences. #[derive(Script, ScriptHook, Widget)] pub struct SettingsScreen { - #[deref] view: View, + #[deref] + view: View, } impl Widget for SettingsScreen { @@ -105,16 +108,15 @@ impl Widget for SettingsScreen { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) - ) - || event.back_pressed() - || match event.hits(cx, area) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(_fde) => { - cx.set_key_focus(area); - false + ) || event.back_pressed() + || match event.hits(cx, area) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + _ => false, } - _ => false, - } }; if close_pane { cx.action(NavigationBarAction::CloseSettings); @@ -132,26 +134,30 @@ impl Widget for SettingsScreen { match action.downcast_ref() { Some(CreateWalletModalAction::Open) => { use crate::tsp::create_wallet_modal::CreateWalletModalWidgetExt; - self.view.create_wallet_modal(cx, ids!(create_wallet_modal_inner)).show(cx); + self.view + .create_wallet_modal(cx, ids!(create_wallet_modal_inner)) + .show(cx); self.view.modal(cx, ids!(create_wallet_modal)).open(cx); } Some(CreateWalletModalAction::Close) => { self.view.modal(cx, ids!(create_wallet_modal)).close(cx); } - None => { } + None => {} } // Handle the create DID modal being opened or closed. match action.downcast_ref() { Some(CreateDidModalAction::Open) => { use crate::tsp::create_did_modal::CreateDidModalWidgetExt; - self.view.create_did_modal(cx, ids!(create_did_modal_inner)).show(cx); + self.view + .create_did_modal(cx, ids!(create_did_modal_inner)) + .show(cx); self.view.modal(cx, ids!(create_did_modal)).open(cx); } Some(CreateDidModalAction::Close) => { self.view.modal(cx, ids!(create_did_modal)).close(cx); } - None => { } + None => {} } } } @@ -169,7 +175,9 @@ impl SettingsScreen { error!("Failed to get own profile for settings screen."); return; }; - self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); + self.view + .account_settings(cx, ids!(account_settings)) + .populate(cx, profile); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); @@ -179,7 +187,9 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. pub fn populate(&self, cx: &mut Cx, own_profile: Option) { - let Some(mut inner) = self.borrow_mut() else { return; }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.populate(cx, own_profile); } } diff --git a/src/shared/avatar.rs b/src/shared/avatar.rs index 3e9a73842..370dfa6df 100644 --- a/src/shared/avatar.rs +++ b/src/shared/avatar.rs @@ -9,13 +9,18 @@ use std::sync::Arc; use makepad_widgets::*; -use matrix_sdk::{ruma::{EventId, OwnedRoomId, OwnedUserId, UserId}}; +use matrix_sdk::{ + ruma::{EventId, OwnedRoomId, OwnedUserId, UserId}, +}; use matrix_sdk_ui::timeline::{Profile, TimelineDetails}; use ruma::OwnedMxcUri; use crate::{ avatar_cache::{self, AvatarCacheEntry}, - profile::{user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, user_profile_cache}, + profile::{ + user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId}, + user_profile_cache, + }, sliding_sync::{submit_async_request, MatrixRequest, TimelineKind}, utils, }; @@ -81,35 +86,38 @@ script_mod! { } } - #[derive(ScriptHook, Script, Widget)] pub struct Avatar { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, /// Information about the user profile being shown in this Avatar. /// If `Some`, this Avatar will respond to clicks/taps. - #[rust] info: Option, + #[rust] + info: Option, } impl Widget for Avatar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - let Some(info) = self.info.clone() else { return }; + let Some(info) = self.info.clone() else { + return; + }; let area = self.view.area(); let widget_uid = self.widget_uid(); match event.hits(cx, area) { Hit::FingerDown(_fde) => { cx.set_key_focus(area); } - Hit::FingerUp(fue) => if fue.is_over && fue.is_primary_hit() && fue.was_tap() { - cx.widget_action( - widget_uid, - ShowUserProfileAction::ShowUserProfile(info), - ); + Hit::FingerUp(fue) => { + if fue.is_over && fue.is_primary_hit() && fue.was_tap() { + cx.widget_action(widget_uid, ShowUserProfileAction::ShowUserProfile(info)); + } } - _ =>() + _ => (), } } @@ -119,7 +127,8 @@ impl Widget for Avatar { fn set_text(&mut self, cx: &mut Cx, v: &str) { let f = utils::user_name_first_letter(v) - .unwrap_or("?").to_uppercase(); + .unwrap_or("?") + .to_uppercase(); self.label(cx, ids!(text_view.text)).set_text(cx, &f); self.view(cx, ids!(img_view)).set_visible(cx, false); self.view(cx, ids!(text_view)).set_visible(cx, true); @@ -144,7 +153,12 @@ impl Avatar { info: Option, username: T, ) { - if let Some(AvatarTextInfo { user_id, username, room_id }) = info { + if let Some(AvatarTextInfo { + user_id, + username, + room_id, + }) = info + { self.info = Some(UserProfileAndRoomId { user_profile: UserProfile { user_id, @@ -187,7 +201,8 @@ impl Avatar { info: Option, image_set_function: F, ) -> Result<(), E> - where F: FnOnce(&mut Cx, ImageRef) -> Result<(), E> + where + F: FnOnce(&mut Cx, ImageRef) -> Result<(), E>, { let img_ref = self.image(cx, ids!(img_view.img)); let res = image_set_function(cx, img_ref); @@ -195,7 +210,13 @@ impl Avatar { self.view(cx, ids!(img_view)).set_visible(cx, true); self.view(cx, ids!(text_view)).set_visible(cx, false); - if let Some(AvatarImageInfo { user_id, username, room_id, img_data }) = info { + if let Some(AvatarImageInfo { + user_id, + username, + room_id, + img_data, + }) = info + { self.info = Some(UserProfileAndRoomId { user_profile: UserProfile { user_id, @@ -268,14 +289,16 @@ impl Avatar { Some(timeline_kind.room_id()), true, |profile, rooms| { - rooms.get(timeline_kind.room_id()).map(|rm| { - ( - rm.display_name().map(|n| n.to_owned()), - AvatarState::Known(rm.avatar_url().map(|u| u.to_owned())), - ) - }) - .unwrap_or_else(|| (profile.username.clone(), profile.avatar_state.clone())) - } + rooms + .get(timeline_kind.room_id()) + .map(|rm| { + ( + rm.display_name().map(|n| n.to_owned()), + AvatarState::Known(rm.avatar_url().map(|u| u.to_owned())), + ) + }) + .unwrap_or_else(|| (profile.username.clone(), profile.avatar_state.clone())) + }, ) }; @@ -322,12 +345,14 @@ impl Avatar { .and_then(|data| { self.show_image( cx, - is_clickable.then(|| AvatarImageInfo::from(( - avatar_user_id.to_owned(), - username_opt.clone(), - timeline_kind.room_id().to_owned(), - data.clone() - ))), + is_clickable.then(|| { + AvatarImageInfo::from(( + avatar_user_id.to_owned(), + username_opt.clone(), + timeline_kind.room_id().to_owned(), + data.clone(), + )) + }), |cx, img| utils::load_png_or_jpg(&img, cx, &data), ) .ok() @@ -336,11 +361,13 @@ impl Avatar { self.show_text( cx, None, - is_clickable.then(|| AvatarTextInfo::from(( - avatar_user_id.to_owned(), - username_opt, - timeline_kind.room_id().to_owned(), - ))), + is_clickable.then(|| { + AvatarTextInfo::from(( + avatar_user_id.to_owned(), + username_opt, + timeline_kind.room_id().to_owned(), + )) + }), &username, ) }); @@ -369,7 +396,8 @@ impl AvatarRef { info: Option, image_set_function: F, ) -> Result<(), E> - where F: FnOnce(&mut Cx, ImageRef) -> Result<(), E> + where + F: FnOnce(&mut Cx, ImageRef) -> Result<(), E>, { if let Some(mut inner) = self.borrow_mut() { inner.show_image(cx, info, image_set_function) @@ -428,7 +456,11 @@ pub struct AvatarTextInfo { } impl From<(OwnedUserId, Option, OwnedRoomId)> for AvatarTextInfo { fn from((user_id, username, room_id): (OwnedUserId, Option, OwnedRoomId)) -> Self { - Self { user_id, username, room_id } + Self { + user_id, + username, + room_id, + } } } @@ -440,17 +472,29 @@ pub struct AvatarImageInfo { pub img_data: Arc<[u8]>, } impl From<(OwnedUserId, Option, OwnedRoomId, Arc<[u8]>)> for AvatarImageInfo { - fn from((user_id, username, room_id, img_data): (OwnedUserId, Option, OwnedRoomId, Arc<[u8]>)) -> Self { - Self { user_id, username, room_id, img_data } + fn from( + (user_id, username, room_id, img_data): ( + OwnedUserId, + Option, + OwnedRoomId, + Arc<[u8]>, + ), + ) -> Self { + Self { + user_id, + username, + room_id, + img_data, + } } } - /// The currently-known state of an avatar for a user, room, or space. #[derive(Clone, Default)] pub enum AvatarState { /// It isn't yet known if this user/room/space has an avatar. - #[default] Unknown, + #[default] + Unknown, /// It is known that this user/room/space does or does not have an avatar. Known(Option), /// The avatar is known to exist and has been fetched successfully. @@ -461,11 +505,11 @@ pub enum AvatarState { impl std::fmt::Debug for AvatarState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - AvatarState::Unknown => write!(f, "Unknown"), + AvatarState::Unknown => write!(f, "Unknown"), AvatarState::Known(Some(_)) => write!(f, "Known(Some)"), - AvatarState::Known(None) => write!(f, "Known(None)"), - AvatarState::Loaded(data) => write!(f, "Loaded({} bytes)", data.len()), - AvatarState::Failed => write!(f, "Failed"), + AvatarState::Known(None) => write!(f, "Known(None)"), + AvatarState::Loaded(data) => write!(f, "Loaded({} bytes)", data.len()), + AvatarState::Failed => write!(f, "Failed"), } } } diff --git a/src/shared/bouncing_dots.rs b/src/shared/bouncing_dots.rs index 5b8e79024..b99fc2fe1 100644 --- a/src/shared/bouncing_dots.rs +++ b/src/shared/bouncing_dots.rs @@ -22,20 +22,20 @@ script_mod! { let center_y = self.rect_size.y * 0.5; // Create three circle SDFs sdf.circle( - self.rect_size.x * 0.25, - amplitude * sin(self.anim_time * 2.0 * PI * self.freq) + center_y, + self.rect_size.x * 0.25, + amplitude * sin(self.anim_time * 2.0 * PI * self.freq) + center_y, self.dot_radius ); sdf.fill(self.color); sdf.circle( - self.rect_size.x * 0.5, - amplitude * sin(self.anim_time * 2.0 * PI * self.freq + self.phase_offset) + center_y, + self.rect_size.x * 0.5, + amplitude * sin(self.anim_time * 2.0 * PI * self.freq + self.phase_offset) + center_y, self.dot_radius ); sdf.fill(self.color); sdf.circle( - self.rect_size.x * 0.75, - amplitude * sin(self.anim_time * 2.0 * PI * self.freq + self.phase_offset * 2) + center_y, + self.rect_size.x * 0.75, + amplitude * sin(self.anim_time * 2.0 * PI * self.freq + self.phase_offset * 2) + center_y, self.dot_radius ); sdf.fill(self.color); @@ -62,15 +62,18 @@ script_mod! { } } } - + } } #[derive(Script, ScriptHook, Widget, Animator)] pub struct BouncingDots { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for BouncingDots { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { @@ -85,7 +88,6 @@ impl Widget for BouncingDots { } } - impl BouncingDotsRef { /// Starts animation of the bouncing dots. pub fn start_animation(&self, cx: &mut Cx) { diff --git a/src/shared/collapsible_header.rs b/src/shared/collapsible_header.rs index e9ad7e337..e412b770a 100644 --- a/src/shared/collapsible_header.rs +++ b/src/shared/collapsible_header.rs @@ -98,19 +98,21 @@ impl HeaderCategory { #[derive(Clone, Debug, Default)] pub enum CollapsibleHeaderAction { /// The header was clicked to toggled its expanded/collapsed state. - Toggled { - category: HeaderCategory, - }, + Toggled { category: HeaderCategory }, #[default] None, } #[derive(Script, ScriptHook, Widget)] pub struct CollapsibleHeader { - #[deref] view: View, - #[rust(true)] is_expanded: bool, - #[rust] category: HeaderCategory, - #[rust] num_unread_mentions: u64, + #[deref] + view: View, + #[rust(true)] + is_expanded: bool, + #[rust] + category: HeaderCategory, + #[rust] + num_unread_mentions: u64, } impl Widget for CollapsibleHeader { @@ -122,22 +124,33 @@ impl Widget for CollapsibleHeader { cx.set_key_focus(self.view.area()); } Hit::FingerUp(fe) => { - if !rooms_list_props.was_scrolling && fe.is_over && fe.is_primary_hit() && fe.was_tap() { + if !rooms_list_props.was_scrolling + && fe.is_over + && fe.is_primary_hit() + && fe.was_tap() + { self.toggle_collapse(cx, scope); } } - _ => { } + _ => {} } self.view.handle_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // Set arrow and label state during draw to ensure child widgets are available. - if let Some(mut arrow) = self.view.child_by_path(ids!(collapse_icon)).borrow_mut::() { + if let Some(mut arrow) = self + .view + .child_by_path(ids!(collapse_icon)) + .borrow_mut::() + { arrow.set_is_open_no_animate(self.is_expanded); } - self.view.child_by_path(ids!(label)).set_text(cx, self.category.as_str()); - self.view.child_by_path(ids!(unread_badge)) + self.view + .child_by_path(ids!(label)) + .set_text(cx, self.category.as_str()); + self.view + .child_by_path(ids!(unread_badge)) .as_unread_badge() .update_counts(false, self.num_unread_mentions, 0); self.view.draw_walk(cx, scope, walk) @@ -147,12 +160,16 @@ impl Widget for CollapsibleHeader { impl CollapsibleHeader { fn toggle_collapse(&mut self, cx: &mut Cx, _scope: &mut Scope) { self.is_expanded = !self.is_expanded; - if let Some(mut arrow) = self.view.child_by_path(ids!(collapse_icon)).borrow_mut::() { + if let Some(mut arrow) = self + .view + .child_by_path(ids!(collapse_icon)) + .borrow_mut::() + { arrow.set_is_open(cx, self.is_expanded, Animate::Yes); } self.redraw(cx); cx.widget_action( - self.widget_uid(), + self.widget_uid(), CollapsibleHeaderAction::Toggled { category: self.category, }, diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index bf8a4d091..33adbc2a7 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -609,8 +609,12 @@ impl CommandTextInput { if let (Some(t_idx), Some(h_idx)) = (trigger_grapheme_idx, head_grapheme_idx) { // Additional range check to prevent index errors if t_idx >= text_graphemes.len() || h_idx > text_graphemes.len() { - log!("Error: Grapheme indices out of range: t_idx={}, h_idx={}, graphemes_len={}", - t_idx, h_idx, text_graphemes.len()); + log!( + "Error: Grapheme indices out of range: t_idx={}, h_idx={}, graphemes_len={}", + t_idx, + h_idx, + text_graphemes.len() + ); return String::new(); } @@ -641,14 +645,26 @@ impl CommandTextInput { return String::new(); } else { // Abnormal case: trigger character is after the cursor - log!("Warning: Trigger character is after cursor: trigger_idx={}, head_idx={}, trigger_pos={}, head={}", - t_idx, h_idx, trigger_pos, head); + log!( + "Warning: Trigger character is after cursor: trigger_idx={}, head_idx={}, trigger_pos={}, head={}", + t_idx, + h_idx, + trigger_pos, + head + ); return String::new(); } } else { // Comprehensive diagnostic information - log!("Warning: Unable to find valid grapheme indices: trigger_idx={:?}, head_idx={:?}, trigger_pos={}, head={}, text_len={}, graphemes_len={}", - trigger_grapheme_idx, head_grapheme_idx, trigger_pos, head, text.len(), text_graphemes.len()); + log!( + "Warning: Unable to find valid grapheme indices: trigger_idx={:?}, head_idx={:?}, trigger_pos={}, head={}, text_len={}, graphemes_len={}", + trigger_grapheme_idx, + head_grapheme_idx, + trigger_pos, + head, + text.len(), + text_graphemes.len() + ); return String::new(); } } diff --git a/src/shared/confirmation_modal.rs b/src/shared/confirmation_modal.rs index 998b76eb4..83b9b6dc6 100644 --- a/src/shared/confirmation_modal.rs +++ b/src/shared/confirmation_modal.rs @@ -4,7 +4,6 @@ use std::borrow::Cow; use makepad_widgets::*; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -135,7 +134,7 @@ pub enum ConfirmationModalAction { /// accept button (true) or cancel button (false). Close(bool), #[default] - None + None, } impl ActionDefaultRef for ConfirmationModalAction { @@ -187,11 +186,12 @@ impl std::fmt::Debug for ConfirmationModalContent { } } - #[derive(Script, ScriptHook, Widget)] pub struct ConfirmationModal { - #[deref] view: View, - #[rust] content: ConfirmationModalContent, + #[deref] + view: View, + #[rust] + content: ConfirmationModalContent, } impl Widget for ConfirmationModal { @@ -212,17 +212,16 @@ impl WidgetMatchEvent for ConfirmationModal { // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); - if cancel_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // If the modal was dismissed by clicking outside of it, we MUST NOT emit // a `ConfirmationModalAction::Close` action, as that would cause // an infinite action feedback loop. if cancel_clicked { - cx.widget_action( - self.widget_uid(), - ConfirmationModalAction::Close(false), - ); + cx.widget_action(self.widget_uid(), ConfirmationModalAction::Close(false)); } if let Some(on_cancel_clicked) = self.content.on_cancel_clicked.take() { on_cancel_clicked(cx); @@ -235,10 +234,7 @@ impl WidgetMatchEvent for ConfirmationModal { if let Some(on_accept_clicked) = self.content.on_accept_clicked.take() { on_accept_clicked(cx); } - cx.widget_action( - self.widget_uid(), - ConfirmationModalAction::Close(true), - ); + cx.widget_action(self.widget_uid(), ConfirmationModalAction::Close(true)); } } } @@ -250,21 +246,35 @@ impl ConfirmationModal { } fn apply_content(&mut self, cx: &mut Cx) { - self.view.label(cx, ids!(title)).set_text(cx, &self.content.title_text); - self.view.label(cx, ids!(body)).set_text(cx, &self.content.body_text); + self.view + .label(cx, ids!(title)) + .set_text(cx, &self.content.title_text); + self.view + .label(cx, ids!(body)) + .set_text(cx, &self.content.body_text); self.view.button(cx, ids!(accept_button)).set_text( cx, - self.content.accept_button_text.as_deref().unwrap_or("Confirm"), + self.content + .accept_button_text + .as_deref() + .unwrap_or("Confirm"), ); self.view.button(cx, ids!(cancel_button)).set_text( cx, - self.content.cancel_button_text.as_deref().unwrap_or("Cancel"), + self.content + .cancel_button_text + .as_deref() + .unwrap_or("Cancel"), ); self.view.button(cx, ids!(cancel_button)).reset_hover(cx); self.view.button(cx, ids!(accept_button)).reset_hover(cx); - self.view.button(cx, ids!(accept_button)).set_enabled(cx, true); - self.view.button(cx, ids!(cancel_button)).set_enabled(cx, true); + self.view + .button(cx, ids!(accept_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(cancel_button)) + .set_enabled(cx, true); self.view.redraw(cx); } } @@ -272,7 +282,9 @@ impl ConfirmationModal { impl ConfirmationModalRef { /// Shows the confirmation modal with the given content. pub fn show(&self, cx: &mut Cx, content: ConfirmationModalContent) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, content); } @@ -281,7 +293,9 @@ impl ConfirmationModalRef { /// If `true`, the user clicked the accept button; if `false`, the user clicked the cancel button. /// See [`ConfirmationModalAction::Close`] for more. pub fn closed(&self, actions: &Actions) -> Option { - if let ConfirmationModalAction::Close(accepted) = actions.find_widget_action(self.widget_uid()).cast_ref() { + if let ConfirmationModalAction::Close(accepted) = + actions.find_widget_action(self.widget_uid()).cast_ref() + { Some(*accepted) } else { None diff --git a/src/shared/expand_arrow.rs b/src/shared/expand_arrow.rs index 4528568d7..125b9282b 100644 --- a/src/shared/expand_arrow.rs +++ b/src/shared/expand_arrow.rs @@ -60,21 +60,34 @@ script_mod! { /// Animated expand/collapse triangle arrow. #[derive(Script, ScriptHook, Widget, Animator)] pub struct ExpandArrow { - #[uid] uid: WidgetUid, - #[source] source: ScriptObjectRef, - #[apply_default] animator: Animator, - #[redraw] #[live] draw_bg: DrawQuad, - #[walk] walk: Walk, + #[uid] + uid: WidgetUid, + #[source] + source: ScriptObjectRef, + #[apply_default] + animator: Animator, + #[redraw] + #[live] + draw_bg: DrawQuad, + #[walk] + walk: Walk, /// Tracks the desired opened state set from outside. /// Applied to draw_bg.opened during draw_walk. - #[rust] opened_value: f32, + #[rust] + opened_value: f32, } impl ExpandArrow { /// Animate open/close (use in event handlers only, not during draw). pub fn set_is_open(&mut self, cx: &mut Cx, is_open: bool, animate: Animate) { self.opened_value = if is_open { 1.0 } else { 0.0 }; - self.animator_toggle(cx, is_open, animate, ids!(expand.expanded), ids!(expand.collapsed)) + self.animator_toggle( + cx, + is_open, + animate, + ids!(expand.expanded), + ids!(expand.collapsed), + ) } /// Set open/close state without animation (safe to call anytime). @@ -92,7 +105,8 @@ impl Widget for ExpandArrow { fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { if !self.animator.is_track_animating(id!(expand)) { - self.draw_bg.set_dyn_instance(cx, id!(opened), &[self.opened_value]); + self.draw_bg + .set_dyn_instance(cx, id!(opened), &[self.opened_value]); } self.draw_bg.draw_walk(cx, walk); DrawStep::done() diff --git a/src/shared/html_or_plaintext.rs b/src/shared/html_or_plaintext.rs index c86eac05d..a83976374 100644 --- a/src/shared/html_or_plaintext.rs +++ b/src/shared/html_or_plaintext.rs @@ -1,9 +1,17 @@ //! A `HtmlOrPlaintext` view can display either plaintext or rich HTML content. use makepad_widgets::*; -use matrix_sdk::{ruma::{matrix_uri::MatrixId, OwnedMxcUri}, OwnedServerName}; - -use crate::{avatar_cache::{self, AvatarCacheEntry}, profile::user_profile_cache, sliding_sync::{current_user_id, submit_async_request, MatrixRequest}, utils}; +use matrix_sdk::{ + ruma::{matrix_uri::MatrixId, OwnedMxcUri}, + OwnedServerName, +}; + +use crate::{ + avatar_cache::{self, AvatarCacheEntry}, + profile::user_profile_cache, + sliding_sync::{current_user_id, submit_async_request, MatrixRequest}, + utils, +}; use super::avatar::AvatarWidgetExt; @@ -190,7 +198,7 @@ script_mod! { } #[derive(Debug, Clone, Default)] -pub enum RobrixHtmlLinkAction{ +pub enum RobrixHtmlLinkAction { ClickedMatrixLink { /// The URL of the link, which is only temporarily needed here /// because we don't fully handle MatrixId links directly in-app yet. @@ -208,15 +216,18 @@ pub enum RobrixHtmlLinkAction{ /// Matrix links are displayed using the [`MatrixLinkPill`] widget. #[derive(Script, Widget)] struct RobrixHtmlLink { - #[deref] view: View, + #[deref] + view: View, /// The displayable text of the link. /// This should be set automatically by the Html widget /// when it parses and draws an Html `` tag. - #[live] pub text: ArcStringMut, + #[live] + pub text: ArcStringMut, /// The URL of the link. /// This is set by the `on_after_new_scoped()` hook below. - #[live] pub url: String, + #[live] + pub url: String, } impl ScriptHook for RobrixHtmlLink { @@ -229,7 +240,7 @@ impl ScriptHook for RobrixHtmlLink { self.url = attr.into(); break; } - _ => { } + _ => {} } } } @@ -305,19 +316,26 @@ pub enum MatrixLinkPillState { /// This can be a link to a user, a room, or a message in a room. #[derive(Script, ScriptHook, Widget)] struct MatrixLinkPill { - #[deref] view: View, - - #[rust] matrix_id: Option, - #[rust] via: Vec, - #[rust] state: MatrixLinkPillState, - #[rust] url: String, + #[deref] + view: View, + + #[rust] + matrix_id: Option, + #[rust] + via: Vec, + #[rust] + state: MatrixLinkPillState, + #[rust] + url: String, } impl Widget for MatrixLinkPill { fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { if let Event::Actions(actions) = event { for action in actions { - if let Some(loaded @ MatrixLinkPillState::Loaded { matrix_id, .. }) = action.downcast_ref() { + if let Some(loaded @ MatrixLinkPillState::Loaded { matrix_id, .. }) = + action.downcast_ref() + { if self.matrix_id.as_ref() == Some(matrix_id) { self.state = loaded.clone(); self.redraw(cx); @@ -335,13 +353,13 @@ impl Widget for MatrixLinkPill { if fe.is_over && fe.is_primary_hit() && fe.was_tap() { if let Some(matrix_id) = self.matrix_id.clone() { cx.widget_action( - self.widget_uid(), + self.widget_uid(), RobrixHtmlLinkAction::ClickedMatrixLink { matrix_id, via: self.via.clone(), key_modifiers: fe.modifiers, url: self.url.clone(), - } + }, ); } } @@ -366,7 +384,13 @@ impl Widget for MatrixLinkPill { impl MatrixLinkPill { /// Populates this pill's info based on the given Matrix ID and via servers. - fn populate_pill(&mut self, cx: &mut Cx, url: String, matrix_id: &MatrixId, via: &[OwnedServerName]) { + fn populate_pill( + &mut self, + cx: &mut Cx, + url: String, + matrix_id: &MatrixId, + via: &[OwnedServerName], + ) { self.url = url; self.matrix_id = Some(matrix_id.clone()); self.via = via.to_vec(); @@ -385,7 +409,12 @@ impl MatrixLinkPill { user_id.clone(), None, true, - |profile, _| { (profile.displayable_name().to_owned(), profile.avatar_state.clone()) } + |profile, _| { + ( + profile.displayable_name().to_owned(), + profile.avatar_state.clone(), + ) + }, ) { Some((name, avatar)) => { self.set_text(cx, &name); @@ -401,7 +430,9 @@ impl MatrixLinkPill { // Handle room ID or alias match &self.state { - MatrixLinkPillState::Loaded { name, avatar_url, .. } => { + MatrixLinkPillState::Loaded { + name, avatar_url, .. + } => { self.label(cx, ids!(title)).set_text(cx, name); self.populate_avatar(cx, avatar_url.as_ref()); return; @@ -413,14 +444,16 @@ impl MatrixLinkPill { }); self.state = MatrixLinkPillState::Requested; } - MatrixLinkPillState::Requested => { } + MatrixLinkPillState::Requested => {} } // While waiting for the async request to complete, show the matrix room ID/alias. match matrix_id { MatrixId::Room(room_id) => self.set_text(cx, room_id.as_str()), MatrixId::RoomAlias(alias) => self.set_text(cx, alias.as_str()), - MatrixId::Event(room_or_alias, _) => self.set_text(cx, &format!("Message in {}", room_or_alias.as_str())), - _ => { } + MatrixId::Event(room_or_alias, _) => { + self.set_text(cx, &format!("Message in {}", room_or_alias.as_str())) + } + _ => {} } self.populate_avatar(cx, None); } @@ -428,7 +461,9 @@ impl MatrixLinkPill { fn populate_avatar(&self, cx: &mut Cx, avatar_url: Option<&OwnedMxcUri>) { let avatar_ref = self.avatar(cx, ids!(avatar)); if let Some(avatar_url) = avatar_url { - if let AvatarCacheEntry::Loaded(data) = avatar_cache::get_or_fetch_avatar(cx, avatar_url) { + if let AvatarCacheEntry::Loaded(data) = + avatar_cache::get_or_fetch_avatar(cx, avatar_url) + { let res = avatar_ref.show_image( cx, None, // Don't make this avatar clickable @@ -442,7 +477,6 @@ impl MatrixLinkPill { // Show a text avatar if we couldn't load an image into the avatar. avatar_ref.show_text(cx, None, None, self.text()); } - } impl MatrixLinkPillRef { @@ -451,35 +485,48 @@ impl MatrixLinkPillRef { } pub fn get_via(&self) -> Vec { - self.borrow().map(|inner| inner.via.clone()).unwrap_or_default() + self.borrow() + .map(|inner| inner.via.clone()) + .unwrap_or_default() } } /// A widget used to display a single HTML `` tag or a `` tag. #[derive(Script, Widget)] struct MatrixHtmlSpan { - #[uid] uid: WidgetUid, + #[uid] + uid: WidgetUid, // TODO: this is unused; just here to invalidly satisfy the area provider. // I'm not sure how to implement `fn area()` given that it has multiple area rects. - #[redraw] #[area] area: Area, + #[redraw] + #[area] + area: Area, // TODO: remove these if they're unneeded - #[walk] walk: Walk, - #[layout] layout: Layout, + #[walk] + walk: Walk, + #[layout] + layout: Layout, - #[rust] drawn_areas: SmallVec<[Area; 2]>, + #[rust] + drawn_areas: SmallVec<[Area; 2]>, /// Whether to grab key focus when pressed. - #[live(true)] grab_key_focus: bool, + #[live(true)] + grab_key_focus: bool, /// The text content within the `` tag. - #[live] text: ArcStringMut, + #[live] + text: ArcStringMut, /// The current display state of the spoiler. - #[rust] spoiler: SpoilerDisplay, + #[rust] + spoiler: SpoilerDisplay, /// Foreground (text) color: the `data-mx-color` or `color` attributes. - #[rust] fg_color: Option, + #[rust] + fg_color: Option, /// Background color: the `data-mx-bg-color` attribute. - #[rust] bg_color: Option, + #[rust] + bg_color: Option, } impl ScriptHook for MatrixHtmlSpan { @@ -494,20 +541,22 @@ impl ScriptHook for MatrixHtmlSpan { while let Some((lc, attr)) = walker.while_attr_lc() { let attr = attr.trim_matches(['"', '\'']); match lc { - id!(color) - | id!(data-mx-color) => self.fg_color = utils::vec4_from_hex_str(attr), - id!(data-mx-bg-color) => self.bg_color = utils::vec4_from_hex_str(attr), - id!(data-mx-spoiler) => self.spoiler = SpoilerDisplay::Hidden { reason: attr.into() }, - _ => () + id!(color) | id!(data - mx - color) => { + self.fg_color = utils::vec4_from_hex_str(attr) + } + id!(data - mx - bg - color) => self.bg_color = utils::vec4_from_hex_str(attr), + id!(data - mx - spoiler) => { + self.spoiler = SpoilerDisplay::Hidden { + reason: attr.into(), + } + } + _ => (), } } } } } - - - /// The possible states that a spoiler can be in: hidden or revealed. /// /// The enclosed `reason` string is an optional reason given for why @@ -534,7 +583,7 @@ impl SpoilerDisplay { let s = std::mem::take(reason); *self = SpoilerDisplay::Hidden { reason: s }; } - SpoilerDisplay::None => { } + SpoilerDisplay::None => {} } } @@ -595,8 +644,7 @@ impl Widget for MatrixHtmlSpan { } match &self.spoiler { - SpoilerDisplay::Hidden { reason } - | SpoilerDisplay::Revealed { reason } => { + SpoilerDisplay::Hidden { reason } | SpoilerDisplay::Revealed { reason } => { // Draw the spoiler reason text in an italic gray font. tf.font_colors.push(COLOR_SPOILER_REASON); tf.italic.push(); @@ -611,11 +659,12 @@ impl Widget for MatrixHtmlSpan { tf.font_colors.pop(); // Now, draw the spoiler context text itself, either hidden or revealed. - if matches!(self.spoiler, SpoilerDisplay::Hidden {..}) { + if matches!(self.spoiler, SpoilerDisplay::Hidden { .. }) { // Use a background color that is the same as the foreground color, // which is a hacky way to make the spoiled text non-readable. // In the future, we should use a proper blur effect. - let spoiler_bg_color = self.fg_color + let spoiler_bg_color = self + .fg_color .or_else(|| tf.font_colors.last().copied()) .unwrap_or(tf.font_color); @@ -627,7 +676,6 @@ impl Widget for MatrixHtmlSpan { tf.draw_block.code_color = old_bg_color; tf.inline_code.pop(); - } else { tf.draw_text(cx, self.text.as_ref()); } @@ -648,9 +696,7 @@ impl Widget for MatrixHtmlSpan { } let (start, end) = tf.areas_tracker.pop_tracker(); - self.drawn_areas = SmallVec::from( - &tf.areas_tracker.areas[start..end] - ); + self.drawn_areas = SmallVec::from(&tf.areas_tracker.areas[start..end]); DrawStep::done() } @@ -665,11 +711,12 @@ impl Widget for MatrixHtmlSpan { } } - #[derive(ScriptHook, Script, Widget)] pub struct HtmlOrPlaintext { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, } impl Widget for HtmlOrPlaintext { @@ -687,12 +734,14 @@ impl HtmlOrPlaintext { pub fn show_plaintext>(&mut self, cx: &mut Cx, text: T) { self.view(cx, ids!(html_view)).set_visible(cx, false); self.view(cx, ids!(plaintext_view)).set_visible(cx, true); - self.label(cx, ids!(plaintext_view.pt_label)).set_text(cx, text.as_ref()); + self.label(cx, ids!(plaintext_view.pt_label)) + .set_text(cx, text.as_ref()); } /// Sets the HTML content, making the HTML visible and the plaintext invisible. pub fn show_html>(&mut self, cx: &mut Cx, html_body: T) { - self.html(cx, ids!(html_view.html)).set_text(cx, html_body.as_ref()); + self.html(cx, ids!(html_view.html)) + .set_text(cx, html_body.as_ref()); self.view(cx, ids!(html_view)).set_visible(cx, true); self.view(cx, ids!(plaintext_view)).set_visible(cx, false); } @@ -730,13 +779,17 @@ impl HtmlOrPlaintext { /// See [`HtmlOrPlaintextRef::set_link_color()`]. pub fn set_link_color(&mut self, cx: &mut Cx, color: Option) { let html_ref = self.html(cx, ids!(html_view.html)); - let Some(mut html) = html_ref.borrow_mut() else { return }; + let Some(mut html) = html_ref.borrow_mut() else { + return; + }; // Iterate over cached TextFlow items (auto-generated IDs start at 1) // until we hit a non-existent item. let mut i = 1u64; loop { let item = html.existing_item(LiveId(i)); - if item.is_empty() { break; } + if item.is_empty() { + break; + } // Check if this item is a RobrixHtmlLink and modify its inner HtmlLink. if let Some(link) = item.borrow_mut::() { let mut html_link = link.html_link(cx, ids!(html_link)); diff --git a/src/shared/image_viewer.rs b/src/shared/image_viewer.rs index 93eaeec82..7e2487f72 100644 --- a/src/shared/image_viewer.rs +++ b/src/shared/image_viewer.rs @@ -25,18 +25,10 @@ const SHOW_UI_DURATION: f64 = 3.0; /// Returns an error if either load fails or if the image format is unknown. pub fn get_png_or_jpg_image_buffer(data: Vec) -> Result { match imghdr::from_bytes(&data) { - Some(imghdr::Type::Png) => { - ImageBuffer::from_png(&data) - }, - Some(imghdr::Type::Jpeg) => { - ImageBuffer::from_jpg(&data) - }, - Some(_unsupported) => { - Err(ImageError::UnsupportedFormat) - } - None => { - Err(ImageError::UnsupportedFormat) - } + Some(imghdr::Type::Png) => ImageBuffer::from_png(&data), + Some(imghdr::Type::Jpeg) => ImageBuffer::from_jpg(&data), + Some(_unsupported) => Err(ImageError::UnsupportedFormat), + None => Err(ImageError::UnsupportedFormat), } } @@ -217,7 +209,7 @@ script_mod! { flow: Right, spacing: 13, align: Align{ y: 0.5 } - + avatar := Avatar { width: 45, height: 45, text_view +: { @@ -445,40 +437,58 @@ pub enum ImageViewerAction { #[derive(Script, ScriptHook, Widget, Animator)] struct ImageViewer { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[rust] drag_state: DragState, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[rust] + drag_state: DragState, /// The current rotation angle of the image. Max of 4, each step represents 90 degrees - #[rust] rotation_step: i8, + #[rust] + rotation_step: i8, /// A lock to prevent multiple rotation animations from running at the same time - #[rust] is_animating_rotation: bool, - #[apply_default] animator: Animator, + #[rust] + is_animating_rotation: bool, + #[apply_default] + animator: Animator, /// Zoom constraints for the image viewer - #[rust] config: ImageViewerZoomConfig, + #[rust] + config: ImageViewerZoomConfig, /// Indicates if the mouse cursor is currently hovering over the image. /// If true, allows wheel scroll to zoom the image. - #[rust] mouse_cursor_hover_over_image: bool, + #[rust] + mouse_cursor_hover_over_image: bool, /// Distance between two touch points for pinch-to-zoom functionality - #[rust] previous_pinch_distance: Option, + #[rust] + previous_pinch_distance: Option, /// The ID of the background task that is currently running - #[rust] background_task_id: u32, + #[rust] + background_task_id: u32, /// The mpsc::Receiver used to receive the result of the background task - #[rust] receiver: Option<(u32, Receiver>)>, + #[rust] + receiver: Option<(u32, Receiver>)>, /// Whether the full image file has been loaded - #[rust] is_loaded: bool, + #[rust] + is_loaded: bool, /// The size of the image container. /// /// Used to compute the necessary width and height for the full screen image. - #[rust] image_container_size: DVec2, + #[rust] + image_container_size: DVec2, /// The texture containing the loaded image - #[rust] texture: Option, + #[rust] + texture: Option, /// The event to trigger displaying with the loaded image after peek_walk_turtle of the widget. - #[rust] next_frame: NextFrame, + #[rust] + next_frame: NextFrame, /// Whether to display the UI overlay, including buttons and metadata. - #[rust] ui_visible_toggle: bool, + #[rust] + ui_visible_toggle: bool, /// Timer used to animate-out (hide) the UI view after the latest user input. - #[rust] hide_ui_timer: Timer, - #[rust] capped_dimension: DVec2, + #[rust] + hide_ui_timer: Timer, + #[rust] + capped_dimension: DVec2, } impl Widget for ImageViewer { @@ -608,9 +618,7 @@ impl Widget for ImageViewer { cx.set_cursor(MouseCursor::Default); } Hit::FingerHoverOver(_) => { - if !self.ui_visible_toggle - && !self.animator.in_state(cx, ids!(ui_animator.show)) - { + if !self.ui_visible_toggle && !self.animator.in_state(cx, ids!(ui_animator.show)) { self.animator_cut(cx, ids!(ui_animator.hide)); self.animator_play(cx, ids!(ui_animator.show)); cx.stop_timer(self.hide_ui_timer); @@ -651,7 +659,8 @@ impl Widget for ImageViewer { self.handle_pinch_to_zoom(cx, touch_event); } - if let (Event::Signal, Some((_background_task_id, receiver))) = (event, &mut self.receiver) { + if let (Event::Signal, Some((_background_task_id, receiver))) = (event, &mut self.receiver) + { let mut remove_receiver = false; match receiver.try_recv() { Ok(Ok(image_buffer)) => { @@ -685,8 +694,7 @@ impl Widget for ImageViewer { let animator_action = self.animator_handle_event(cx, event); if self.next_frame.is_event(event).is_some() { self.display_using_texture(cx); - } - else if let Event::NextFrame(_) = event { + } else if let Event::NextFrame(_) = event { let animation_id = match self.rotation_step { 0 => ids!(mode.upright), // 0° 1 => ids!(mode.degree_90), // 90° @@ -695,12 +703,19 @@ impl Widget for ImageViewer { _ => ids!(mode.upright), }; if self.animator.in_state(cx, animation_id) { - self.is_animating_rotation = matches!(animator_action, AnimatorAction::Animating { .. }); + self.is_animating_rotation = + matches!(animator_action, AnimatorAction::Animating { .. }); } } if event.back_pressed() - || matches!(event, Event::KeyDown(KeyEvent { key_code: KeyCode::Escape, .. })) + || matches!( + event, + Event::KeyDown(KeyEvent { + key_code: KeyCode::Escape, + .. + }) + ) { self.reset(cx); cx.action(ImageViewerAction::Hide); @@ -730,19 +745,11 @@ impl MatchEvent for ImageViewer { if self.view.button(cx, ids!(reset_button)).clicked(actions) { self.reset(cx); } - if self - .view - .button(cx, ids!(zoom_out_button)) - .clicked(actions) - { + if self.view.button(cx, ids!(zoom_out_button)).clicked(actions) { self.adjust_zoom(cx, 1.0 / self.config.zoom_scale_factor); } - if self - .view - .button(cx, ids!(zoom_in_button)) - .clicked(actions) - { + if self.view.button(cx, ids!(zoom_in_button)).clicked(actions) { self.adjust_zoom(cx, self.config.zoom_scale_factor); } @@ -794,7 +801,7 @@ impl MatchEvent for ImageViewer { LoadState::FinishedBackgroundDecoding => { self.is_loaded = true; self.hide_footer(cx); - }, + } LoadState::Error(error) => { self.show_error(cx, error); } @@ -892,7 +899,7 @@ impl ImageViewer { } /// Displays an image in the image viewer widget using the provided texture. - /// + /// /// `Texture` is an optional `Texture` that can be set to display an image. If `None`, the image is cleared. pub fn display_using_texture(&mut self, cx: &mut Cx) { if self.image_container_size.length() == 0.0 { @@ -904,21 +911,21 @@ impl ImageViewer { .as_ref() .and_then(|texture| texture.get_format(cx).vec_width_height()) .unwrap_or_default(); - + // Calculate scaling factors for both dimensions let scale_x = self.image_container_size.x / texture_width as f64; let scale_y = self.image_container_size.y / texture_height as f64; - + // Use the smaller scale factor to ensure image fits within container let scale = scale_x.min(scale_y); - + let capped_width = (texture_width as f64 * scale).floor(); let capped_height = (texture_height as f64 * scale).floor(); - self.capped_dimension = DVec2{ + self.capped_dimension = DVec2 { x: capped_width, - y: capped_height + y: capped_height, }; - + rotated_image.set_texture(cx, texture); script_apply_eval!(cx, rotated_image, { width: #(capped_width), @@ -933,7 +940,10 @@ impl ImageViewer { let capped_dimension = self.capped_dimension; let target_zoom = self.drag_state.zoom_level * zoom_factor; let (width, height) = if target_zoom < self.config.min_zoom { - (capped_dimension.x * self.config.min_zoom, capped_dimension.y * self.config.min_zoom) + ( + capped_dimension.x * self.config.min_zoom, + capped_dimension.y * self.config.min_zoom, + ) } else { let actual_zoom_factor = target_zoom / self.drag_state.zoom_level; self.drag_state.zoom_level = target_zoom; @@ -986,11 +996,14 @@ impl ImageViewer { /// status label is set to "Loading...". pub fn show_loading(&mut self, cx: &mut Cx) { let footer = self.view.view(cx, ids!(image_layer.footer)); - footer.view(cx, ids!(image_viewer_loading_spinner_view)) + footer + .view(cx, ids!(image_viewer_loading_spinner_view)) .set_visible(cx, true); - footer.label(cx, ids!(image_viewer_status_label)) + footer + .label(cx, ids!(image_viewer_status_label)) .set_text(cx, "Loading..."); - footer.view(cx, ids!(image_viewer_forbidden_view)) + footer + .view(cx, ids!(image_viewer_forbidden_view)) .set_visible(cx, false); footer.set_visible(cx, true); self.ui_visible_toggle = true; @@ -1007,11 +1020,14 @@ impl ImageViewer { return; } let footer = self.view.view(cx, ids!(image_layer.footer)); - footer.view(cx, ids!(image_viewer_loading_spinner_view)) + footer + .view(cx, ids!(image_viewer_loading_spinner_view)) .set_visible(cx, false); - footer.view(cx, ids!(image_viewer_forbidden_view)) + footer + .view(cx, ids!(image_viewer_forbidden_view)) .set_visible(cx, true); - footer.label(cx, ids!(image_viewer_status_label)) + footer + .label(cx, ids!(image_viewer_status_label)) .set_text(cx, &error.to_string()); footer.set_visible(cx, true); } @@ -1046,14 +1062,17 @@ impl ImageViewer { } if let Some((timeline_kind, event_timeline_item)) = &metadata.avatar_parameter { - let (sender, _) = self.view.avatar(cx, ids!(user_profile_view.avatar)).set_avatar_and_get_username( - cx, - timeline_kind, - event_timeline_item.sender(), - Some(event_timeline_item.sender_profile()), - event_timeline_item.event_id(), - false, - ); + let (sender, _) = self + .view + .avatar(cx, ids!(user_profile_view.avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + event_timeline_item.sender(), + Some(event_timeline_item.sender_profile()), + event_timeline_item.event_id(), + false, + ); if sender.len() > MAX_USERNAME_LENGTH { meta_view .label(cx, ids!(user_profile_view.content.username)) @@ -1070,13 +1089,17 @@ impl ImageViewer { impl ImageViewerRef { /// Configure zoom and pan settings for the image viewer pub fn configure_zoom(&mut self, config: ImageViewerZoomConfig) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.config = config; } /// See [`ImageViewer::show_loaded()`]. pub fn show_loaded(&mut self, cx: &mut Cx, image_bytes: &[u8]) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_loaded(cx, image_bytes) } @@ -1087,7 +1110,9 @@ impl ImageViewerRef { texture: Option, metadata: &Option, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.texture = texture.clone(); inner.next_frame = cx.new_next_frame(); if let Some(metadata) = metadata { @@ -1098,19 +1123,25 @@ impl ImageViewerRef { /// See [`ImageViewer::show_error()`]. pub fn show_error(&mut self, cx: &mut Cx, error: &ImageViewerError) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_error(cx, error); } /// See [`ImageViewer::hide_footer()`]. pub fn hide_footer(&mut self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.hide_footer(cx); } /// See [`ImageViewer::reset()`]. pub fn reset(&mut self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.reset(cx); } } diff --git a/src/shared/jump_to_bottom_button.rs b/src/shared/jump_to_bottom_button.rs index 9fb9a840f..d15c9b3c6 100644 --- a/src/shared/jump_to_bottom_button.rs +++ b/src/shared/jump_to_bottom_button.rs @@ -68,7 +68,7 @@ script_mod! { draw_bg +: { color: instance(COLOR_UNREAD_BADGE_MESSAGES) border_radius: uniform(4.0) - // Adjust this border_size to larger value to make oval smaller + // Adjust this border_size to larger value to make oval smaller border_size: uniform(2.0) pixel: fn() { @@ -98,15 +98,16 @@ script_mod! { } } } - + } } - #[derive(ScriptHook, Script, Widget)] pub struct JumpToBottomButton { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, } impl Widget for JumpToBottomButton { @@ -115,7 +116,7 @@ impl Widget for JumpToBottomButton { match event.hits(cx, button_area) { Hit::FingerHoverIn(_) | Hit::FingerLongPress(_) => { cx.widget_action( - self.widget_uid(), + self.widget_uid(), TooltipAction::HoverIn { text: "Jump to bottom".to_string(), widget_rect: button_area.rect(cx), @@ -127,10 +128,7 @@ impl Widget for JumpToBottomButton { ); } Hit::FingerHoverOut(_) => { - cx.widget_action( - self.widget_uid(), - TooltipAction::HoverOut, - ); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); } _ => {} } @@ -155,7 +153,8 @@ impl JumpToBottomButton { pub fn update_visibility(&mut self, cx: &mut Cx, is_at_bottom: bool) { if is_at_bottom { self.visible = false; - self.view(cx, ids!(unread_message_badge)).set_visible(cx, false); + self.view(cx, ids!(unread_message_badge)) + .set_visible(cx, false); } else { self.visible = true; } @@ -169,17 +168,20 @@ impl JumpToBottomButton { match count { UnreadMessageCount::Unknown => { self.visible = true; - self.view(cx, ids!(unread_message_badge)).set_visible(cx, true); + self.view(cx, ids!(unread_message_badge)) + .set_visible(cx, true); self.label(cx, ids!(unread_messages_count)).set_text(cx, ""); } UnreadMessageCount::Known(0) => { self.visible = false; - self.view(cx, ids!(unread_message_badge)).set_visible(cx, false); + self.view(cx, ids!(unread_message_badge)) + .set_visible(cx, false); self.label(cx, ids!(unread_messages_count)).set_text(cx, ""); } UnreadMessageCount::Known(unread_message_count) => { self.visible = true; - self.view(cx, ids!(unread_message_badge)).set_visible(cx, true); + self.view(cx, ids!(unread_message_badge)) + .set_visible(cx, true); let (border_size, plus_sign) = if unread_message_count > 99 { (0.0, "+") } else if unread_message_count > 9 { @@ -189,7 +191,7 @@ impl JumpToBottomButton { }; self.label(cx, ids!(unread_messages_count)).set_text( cx, - &format!("{}{plus_sign}", std::cmp::min(unread_message_count, 99)) + &format!("{}{plus_sign}", std::cmp::min(unread_message_count, 99)), ); let mut badge_view = self.view(cx, ids!(unread_message_badge.green_rounded_label)); script_apply_eval!(cx, badge_view, { @@ -218,11 +220,7 @@ impl JumpToBottomButton { // query the portallist's `at_end` state and set the visibility accordingly. if self.button(cx, ids!(inner_button)).clicked(actions) { - portal_list.smooth_scroll_to_end( - cx, - SCROLL_TO_BOTTOM_SPEED, - None, - ); + portal_list.smooth_scroll_to_end(cx, SCROLL_TO_BOTTOM_SPEED, None); self.update_visibility(cx, false); } else { self.update_visibility(cx, portal_list.is_at_end()); @@ -232,7 +230,6 @@ impl JumpToBottomButton { self.redraw(cx); } } - } impl JumpToBottomButtonRef { @@ -251,12 +248,7 @@ impl JumpToBottomButtonRef { } /// See [`JumpToBottomButton::update_from_actions()`]. - pub fn update_from_actions( - &self, - cx: &mut Cx, - portal_list: &PortalListRef, - actions: &Actions, - ) { + pub fn update_from_actions(&self, cx: &mut Cx, portal_list: &PortalListRef, actions: &Actions) { if let Some(mut inner) = self.borrow_mut() { inner.update_from_actions(cx, portal_list, actions); } @@ -269,5 +261,5 @@ pub enum UnreadMessageCount { /// There are unread messages, but we do not know how many. Unknown, /// There are unread messages, and we know exactly how many. - Known(u64) + Known(u64), } diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 31c422935..b074e0337 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -5,10 +5,7 @@ //! can be slotted back in later without changing the code that depends on it. use makepad_widgets::*; -use matrix_sdk::ruma::{ - events::room::message::RoomMessageEventContent, - OwnedRoomId, -}; +use matrix_sdk::ruma::{events::room::message::RoomMessageEventContent, OwnedRoomId}; script_mod! { use mod.prelude.widgets.* @@ -43,18 +40,21 @@ pub enum MentionableTextInputAction { PowerLevelsUpdated { room_id: OwnedRoomId, can_notify_room: bool, - } + }, } /// Temporary mock widget that wraps a simple TextInput (RobrixTextInput) /// while preserving the same external API as the real MentionableTextInput. #[derive(Script, ScriptHook, Widget)] pub struct MentionableTextInput { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, /// Whether the current user can notify everyone in the room (@room mention). /// Stored but not used in this mock; kept for API compatibility. - #[rust] can_notify_room: bool, + #[rust] + can_notify_room: bool, } impl Widget for MentionableTextInput { @@ -65,7 +65,8 @@ impl Widget for MentionableTextInput { if let Event::Actions(actions) = event { for action in actions { if let Some(MentionableTextInputAction::PowerLevelsUpdated { - can_notify_room, .. + can_notify_room, + .. }) = action.downcast_ref() { self.can_notify_room = *can_notify_room; @@ -83,17 +84,18 @@ impl Widget for MentionableTextInput { } fn set_text(&mut self, cx: &mut Cx, text: &str) { - self.text_input(cx, ids!(persistent.center.text_input)).set_text(cx, text); + self.text_input(cx, ids!(persistent.center.text_input)) + .set_text(cx, text); self.redraw(cx); } fn set_key_focus(&self, cx: &mut Cx) { - self.text_input(cx, ids!(persistent.center.text_input)).set_key_focus(cx); + self.text_input(cx, ids!(persistent.center.text_input)) + .set_key_focus(cx); } } impl MentionableTextInput { - /// Sets whether the current user can notify the entire room (@room mention). pub fn set_can_notify_room(&mut self, can_notify: bool) { self.can_notify_room = can_notify; @@ -108,7 +110,8 @@ impl MentionableTextInput { impl MentionableTextInputRef { /// Returns a reference to the inner `TextInput` widget. pub fn text_input_ref(&self) -> TextInputRef { - self.child_by_path(ids!(persistent.center.text_input)).as_text_input() + self.child_by_path(ids!(persistent.center.text_input)) + .as_text_input() } /// Sets whether the current user can notify the entire room (@room mention). diff --git a/src/shared/mod.rs b/src/shared/mod.rs index a92a81fd9..7c5de0224 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -21,7 +21,6 @@ pub mod verification_badge; pub mod restore_status_view; pub mod image_viewer; - pub fn script_mod(vm: &mut ScriptVm) { // Order matters here, as some widget definitions depend on others. styles::script_mod(vm); diff --git a/src/shared/popup_list.rs b/src/shared/popup_list.rs index e0838aaf0..195644272 100644 --- a/src/shared/popup_list.rs +++ b/src/shared/popup_list.rs @@ -271,7 +271,7 @@ script_mod! { main_content := mod.widgets.MainContent {} } progress_bar := mod.widgets.ProgressBar {} - // Add a small gap between the progress bar and the end of the popup + // Add a small gap between the progress bar and the end of the popup // to ensure the progress bar is within the popup. View { height: 0.2 @@ -355,16 +355,25 @@ struct PopupEntry { /// A widget that displays a vertical list of popups. #[derive(Script, Widget)] pub struct RobrixPopupNotification { - #[uid] uid: WidgetUid, - #[source] source: ScriptObjectRef, - #[live] pub content: Option, - - #[rust] draw_list: Option, - #[redraw] #[live] draw_bg: DrawQuad, - #[layout] layout: Layout, - #[walk] walk: Walk, + #[uid] + uid: WidgetUid, + #[source] + source: ScriptObjectRef, + #[live] + pub content: Option, + + #[rust] + draw_list: Option, + #[redraw] + #[live] + draw_bg: DrawQuad, + #[layout] + layout: Layout, + #[walk] + walk: Walk, // A list of tuples containing individual widgets, its content and the close timer in the order they were added. - #[rust] popups: Vec, + #[rust] + popups: Vec, } impl ScriptHook for RobrixPopupNotification { @@ -566,10 +575,7 @@ impl RobrixPopupNotification { progress_bar.animator_cut(cx, ids!(progress.off)); Timer::empty() }; - self.popups.push(PopupEntry { - view, - close_timer, - }); + self.popups.push(PopupEntry { view, close_timer }); self.redraw_overlay(cx); } @@ -616,10 +622,7 @@ impl RobrixPopupNotification { popup_item.auto_dismissal_duration = popup_item .auto_dismissal_duration .map(|duration| duration.min(3. * 60.)); - self.popups.push(PopupEntry { - view, - close_timer, - }); + self.popups.push(PopupEntry { view, close_timer }); } /// Returns a clone of the template for each popup in the list. diff --git a/src/shared/restore_status_view.rs b/src/shared/restore_status_view.rs index 5e1e89b29..e0beee6a0 100644 --- a/src/shared/restore_status_view.rs +++ b/src/shared/restore_status_view.rs @@ -45,8 +45,10 @@ script_mod! { /// A view that displays a spinner and a label to indicate that a restore operation is in progress for a room. #[derive(Script, ScriptHook, Widget)] pub struct RestoreStatusView { - #[deref] view: View, - #[live(true)] visible: bool, + #[deref] + view: View, + #[live(true)] + visible: bool, } impl Widget for RestoreStatusView { @@ -55,7 +57,7 @@ impl Widget for RestoreStatusView { self.view.handle_event(cx, event, scope); } } - + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { if self.visible { self.view.draw_walk(cx, scope, walk) @@ -74,8 +76,7 @@ impl RestoreStatusViewRef { if let Some(mut inner) = self.borrow_mut() { inner.visible = visible; if !visible { - inner.label(cx, ids!(restore_status_label)) - .set_text(cx, ""); + inner.label(cx, ids!(restore_status_label)).set_text(cx, ""); } } } @@ -91,12 +92,7 @@ impl RestoreStatusViewRef { /// /// The `room_name` parameter is used to fill in the room name in the error message. /// Its `Display` implementation automatically handles Empty names by falling back to the room ID. - pub fn set_content( - &self, - cx: &mut Cx, - all_rooms_loaded: bool, - room_name: &RoomNameId, - ) { + pub fn set_content(&self, cx: &mut Cx, all_rooms_loaded: bool, room_name: &RoomNameId) { let Some(inner) = self.borrow() else { return }; let restore_status_spinner = inner.view.view(cx, ids!(restore_status_spinner)); let restore_status_label = inner.view.label(cx, ids!(restore_status_label)); @@ -111,10 +107,8 @@ impl RestoreStatusViewRef { ); } else { restore_status_spinner.set_visible(cx, true); - restore_status_label.set_text( - cx, - "Waiting for this room to be loaded from the homeserver", - ); + restore_status_label + .set_text(cx, "Waiting for this room to be loaded from the homeserver"); } } } diff --git a/src/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index ccbee0601..63e87e3c4 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -43,9 +43,9 @@ script_mod! { height: Fit, flow: Right, // do not wrap padding: 5 - + empty_text: "Filter rooms & spaces..." - + draw_bg.border_size: 0.0 draw_text +: { text_style: theme.font_regular { font_size: 10 }, @@ -68,7 +68,8 @@ script_mod! { /// See the module-level docs for more detail. #[derive(Script, ScriptHook, Widget)] pub struct RoomFilterInputBar { - #[deref] view: View, + #[deref] + view: View, } /// Actions emitted by the `RoomFilterInputBar` based on user interaction with it. @@ -114,20 +115,14 @@ impl WidgetMatchEvent for RoomFilterInputBar { }; clear_button.set_visible(cx, !keywords.is_empty()); clear_button.reset_hover(cx); - cx.widget_action( - self.widget_uid(), - RoomFilterAction::Changed(keywords) - ); + cx.widget_action(self.widget_uid(), RoomFilterAction::Changed(keywords)); } if clear_button.clicked(actions) { input.set_text(cx, ""); clear_button.set_visible(cx, false); input.set_key_focus(cx); - cx.widget_action( - self.widget_uid(), - RoomFilterAction::Changed(String::new()) - ); + cx.widget_action(self.widget_uid(), RoomFilterAction::Changed(String::new())); } } } diff --git a/src/shared/styles.rs b/src/shared/styles.rs index a80fa55e5..8e5026260 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -246,45 +246,44 @@ script_mod! { } } - /// #FFFFFF -pub const COLOR_PRIMARY: Vec4 = vec4(1.0, 1.0, 1.0, 1.0); +pub const COLOR_PRIMARY: Vec4 = vec4(1.0, 1.0, 1.0, 1.0); /// #0F88FE -pub const COLOR_ACTIVE_PRIMARY: Vec4 = vec4(0.059, 0.533, 0.996, 1.0); +pub const COLOR_ACTIVE_PRIMARY: Vec4 = vec4(0.059, 0.533, 0.996, 1.0); /// #106FCC pub const COLOR_ACTIVE_PRIMARY_DARKER: Vec4 = vec4(0.063, 0.435, 0.682, 1.0); /// #138808 -pub const COLOR_FG_ACCEPT_GREEN: Vec4 = vec4(0.074, 0.533, 0.031, 1.0); +pub const COLOR_FG_ACCEPT_GREEN: Vec4 = vec4(0.074, 0.533, 0.031, 1.0); /// #F0FFF0 -pub const COLOR_BG_ACCEPT_GREEN: Vec4 = vec4(0.941, 1.0, 0.941, 1.0); +pub const COLOR_BG_ACCEPT_GREEN: Vec4 = vec4(0.941, 1.0, 0.941, 1.0); /// #B3B3B3 -pub const COLOR_FG_DISABLED: Vec4 = vec4(0.7, 0.7, 0.7, 1.0); +pub const COLOR_FG_DISABLED: Vec4 = vec4(0.7, 0.7, 0.7, 1.0); /// #E0E0E0 -pub const COLOR_BG_DISABLED: Vec4 = vec4(0.878, 0.878, 0.878, 1.0); +pub const COLOR_BG_DISABLED: Vec4 = vec4(0.878, 0.878, 0.878, 1.0); /// #DC0005 -pub const COLOR_FG_DANGER_RED: Vec4 = vec4(0.863, 0.0, 0.02, 1.0); +pub const COLOR_FG_DANGER_RED: Vec4 = vec4(0.863, 0.0, 0.02, 1.0); /// #FFF0F0 -pub const COLOR_BG_DANGER_RED: Vec4 = vec4(1.0, 0.941, 0.941, 1.0); +pub const COLOR_BG_DANGER_RED: Vec4 = vec4(1.0, 0.941, 0.941, 1.0); /// #572DCC -pub const COLOR_ROBRIX_PURPLE: Vec4 = vec4(0.341, 0.176, 0.8, 1.0); +pub const COLOR_ROBRIX_PURPLE: Vec4 = vec4(0.341, 0.176, 0.8, 1.0); /// #05CDC7 -pub const COLOR_ROBRIX_CYAN: Vec4 = vec4(0.031, 0.804, 0.78, 1.0); +pub const COLOR_ROBRIX_CYAN: Vec4 = vec4(0.031, 0.804, 0.78, 1.0); /// #FF0000 pub const COLOR_UNREAD_BADGE_MENTIONS: Vec4 = vec4(1.0, 0.0, 0.0, 1.0); /// #572DCC -pub const COLOR_UNREAD_BADGE_MARKED: Vec4 = COLOR_ROBRIX_CYAN; +pub const COLOR_UNREAD_BADGE_MARKED: Vec4 = COLOR_ROBRIX_CYAN; /// #AAAAAA pub const COLOR_UNREAD_BADGE_MESSAGES: Vec4 = vec4(0.667, 0.667, 0.667, 1.0); /// #FF6e00 -pub const COLOR_UNKNOWN_ROOM_AVATAR: Vec4 = vec4(1.0, 0.431, 0.0, 1.0); +pub const COLOR_UNKNOWN_ROOM_AVATAR: Vec4 = vec4(1.0, 0.431, 0.0, 1.0); /// #888888 -pub const COLOR_MESSAGE_NOTICE_TEXT: Vec4 = vec4(0.5, 0.5, 0.5, 1.0); +pub const COLOR_MESSAGE_NOTICE_TEXT: Vec4 = vec4(0.5, 0.5, 0.5, 1.0); /// #953800 pub const COLOR_TEXT_WARNING_NOT_FOUND: Vec4 = vec4(0.584, 0.219, 0.0, 1.0); /// #F0F5FF -pub const COLOR_BG_PREVIEW: Vec4 = vec4(0.941, 0.961, 1.0, 1.0); +pub const COLOR_BG_PREVIEW: Vec4 = vec4(0.941, 0.961, 1.0, 1.0); /// #CDEDDF -pub const COLOR_BG_PREVIEW_HOVER: Vec4 = vec4(0.804, 0.929, 0.875, 1.0); +pub const COLOR_BG_PREVIEW_HOVER: Vec4 = vec4(0.804, 0.929, 0.875, 1.0); /// Applies positive (green) button styling to the given button. pub fn apply_positive_button_style(cx: &mut Cx, button: &mut ButtonRef) { diff --git a/src/shared/text_or_image.rs b/src/shared/text_or_image.rs index a535661ff..4f3f4c8d1 100644 --- a/src/shared/text_or_image.rs +++ b/src/shared/text_or_image.rs @@ -54,7 +54,6 @@ script_mod! { } } - /// A view that holds an image or text content, and can switch between the two. /// /// This is useful for displaying alternate text when an image is not (yet) available @@ -62,10 +61,13 @@ script_mod! { /// is being fetched. #[derive(Script, Widget, ScriptHook)] pub struct TextOrImage { - #[deref] view: View, - #[rust] status: TextOrImageStatus, + #[deref] + view: View, + #[rust] + status: TextOrImageStatus, // #[rust(TextOrImageStatus::Text)] status: TextOrImageStatus, - #[rust] size_in_pixels: (usize, usize), + #[rust] + size_in_pixels: (usize, usize), } impl Widget for TextOrImage { @@ -79,7 +81,7 @@ impl Widget for TextOrImage { } Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { cx.widget_action( - self.widget_uid(), + self.widget_uid(), TextOrImageAction::Clicked(mxc_uri.clone()), ); cx.set_cursor(MouseCursor::Default); @@ -108,9 +110,12 @@ impl TextOrImage { /// a message like "Loading..." or an error message. pub fn show_text>(&mut self, cx: &mut Cx, text: T) { self.view(cx, ids!(image_view)).set_visible(cx, false); - self.view(cx, ids!(default_image_view)).set_visible(cx, false); + self.view(cx, ids!(default_image_view)) + .set_visible(cx, false); self.view(cx, ids!(text_view)).set_visible(cx, true); - self.view.label(cx, ids!(text_view.label)).set_text(cx, text.as_ref()); + self.view + .label(cx, ids!(text_view.label)) + .set_text(cx, text.as_ref()); self.status = TextOrImageStatus::Text; } @@ -123,8 +128,14 @@ impl TextOrImage { /// * If successful, the `image_set_function` should return the size of the image /// in pixels as a tuple, `(width, height)`. /// * If `image_set_function` returns an error, no change is made to this `TextOrImage`. - pub fn show_image(&mut self, cx: &mut Cx, source_url: Option, image_set_function: F) -> Result<(), E> - where F: FnOnce(&mut Cx, ImageRef) -> Result<(usize, usize), E> + pub fn show_image( + &mut self, + cx: &mut Cx, + source_url: Option, + image_set_function: F, + ) -> Result<(), E> + where + F: FnOnce(&mut Cx, ImageRef) -> Result<(usize, usize), E>, { let image_ref = self.view.image(cx, ids!(image_view.image)); match image_set_function(cx, image_ref) { @@ -133,7 +144,8 @@ impl TextOrImage { self.size_in_pixels = size_in_pixels; self.view(cx, ids!(image_view)).set_visible(cx, true); self.view(cx, ids!(text_view)).set_visible(cx, false); - self.view(cx, ids!(default_image_view)).set_visible(cx, false); + self.view(cx, ids!(default_image_view)) + .set_visible(cx, false); Ok(()) } Err(e) => { @@ -150,7 +162,8 @@ impl TextOrImage { /// Displays the default image that is used when no image is available. pub fn show_default_image(&self, cx: &mut Cx) { - self.view(cx, ids!(default_image_view)).set_visible(cx, true); + self.view(cx, ids!(default_image_view)) + .set_visible(cx, true); self.view(cx, ids!(text_view)).set_visible(cx, false); self.view(cx, ids!(image_view)).set_visible(cx, false); } @@ -165,8 +178,14 @@ impl TextOrImageRef { } /// See [TextOrImage::show_image()]. - pub fn show_image(&self, cx: &mut Cx, source_url: Option, image_set_function: F) -> Result<(), E> - where F: FnOnce(&mut Cx, ImageRef) -> Result<(usize, usize), E> + pub fn show_image( + &self, + cx: &mut Cx, + source_url: Option, + image_set_function: F, + ) -> Result<(), E> + where + F: FnOnce(&mut Cx, ImageRef) -> Result<(usize, usize), E>, { if let Some(mut inner) = self.borrow_mut() { inner.show_image(cx, source_url, image_set_function) @@ -212,7 +231,7 @@ impl TextOrImageRef { pub enum TextOrImageStatus { #[default] Text, - /// Image source URL stored in this variant to be used + /// Image source URL stored in this variant to be used Image(Option), } @@ -222,5 +241,5 @@ pub enum TextOrImageAction { /// The user has clicked the `TextOrImage`, with source URL stored in this variant. Clicked(Option), #[default] - None + None, } diff --git a/src/shared/timestamp.rs b/src/shared/timestamp.rs index c84935fd3..42585a34d 100644 --- a/src/shared/timestamp.rs +++ b/src/shared/timestamp.rs @@ -4,7 +4,6 @@ use chrono::{DateTime, Local}; use makepad_widgets::*; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -33,9 +32,11 @@ script_mod! { /// See the module-level docs for more detail. #[derive(Script, ScriptHook, Widget)] pub struct Timestamp { - #[deref] view: View, + #[deref] + view: View, - #[rust] dt: DateTime, + #[rust] + dt: DateTime, } impl Widget for Timestamp { @@ -44,20 +45,19 @@ impl Widget for Timestamp { let area = self.view.area(); let should_hover_in = match event.hits(cx, area) { - Hit::FingerLongPress(_) - | Hit::FingerHoverIn(..) => true, + Hit::FingerLongPress(_) | Hit::FingerHoverIn(..) => true, Hit::FingerUp(fue) if fue.is_over && fue.is_primary_hit() => true, Hit::FingerHoverOut(_) => { - cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); false } _ => false, }; if should_hover_in { // TODO: use pure_rust_locales crate to format the time based on the chosen Locale. - let locale_extended_fmt_en_us= "%a %b %-d, %Y, %r"; + let locale_extended_fmt_en_us = "%a %b %-d, %Y, %r"; cx.widget_action( - self.widget_uid(), + self.widget_uid(), TooltipAction::HoverIn { text: self.dt.format(locale_extended_fmt_en_us).to_string(), widget_rect: area.rect(cx), @@ -79,10 +79,8 @@ impl Timestamp { pub fn set_date_time(&mut self, cx: &mut Cx, dt: DateTime) { // TODO: use pure_rust_locales crate to format the time based on the chosen Locale. let locale_fmt_en_us = "%-I:%M %P"; - self.label(cx, ids!(ts_label)).set_text( - cx, - &dt.format(locale_fmt_en_us).to_string() - ); + self.label(cx, ids!(ts_label)) + .set_text(cx, &dt.format(locale_fmt_en_us).to_string()); self.dt = dt; } } diff --git a/src/shared/unread_badge.rs b/src/shared/unread_badge.rs index ab184fa57..1f04894c7 100644 --- a/src/shared/unread_badge.rs +++ b/src/shared/unread_badge.rs @@ -3,7 +3,6 @@ use makepad_widgets::*; - script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -21,7 +20,7 @@ script_mod! { draw_bg +: { badge_color: instance((COLOR_UNREAD_BADGE_MESSAGES)), border_radius: instance(4.0) - // Set this border_size to a larger value to make the oval smaller + // Set this border_size to a larger value to make the oval smaller border_size: instance(2.0) pixel: fn() { @@ -53,14 +52,18 @@ script_mod! { } } - #[derive(Script, ScriptHook, Widget)] pub struct UnreadBadge { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[live] is_marked_unread: bool, - #[live] unread_mentions: u64, - #[live] unread_messages: u64, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[live] + is_marked_unread: bool, + #[live] + unread_mentions: u64, + #[live] + unread_messages: u64, } impl Widget for UnreadBadge { @@ -69,11 +72,10 @@ impl Widget for UnreadBadge { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - /// Helper function to format the badge's rounded rectangle. /// /// The rounded rectangle needs to be wider for longer text. - /// It also adds a plus sign at the end if the unread count is greater than 99. + /// It also adds a plus sign at the end if the unread count is greater than 99. fn format_border_and_truncation(count: u64) -> (f64, &'static str) { let (border_size, plus_sign) = if count > 99 { (0.0, "+") @@ -88,8 +90,10 @@ impl Widget for UnreadBadge { // If there are unread mentions, show red badge and the number of unread mentions if self.unread_mentions > 0 { let (border_size, plus_sign) = format_border_and_truncation(self.unread_mentions); - self.label(cx, ids!(label_count)) - .set_text(cx, &format!("{}{plus_sign}", std::cmp::min(self.unread_mentions, 99))); + self.label(cx, ids!(label_count)).set_text( + cx, + &format!("{}{plus_sign}", std::cmp::min(self.unread_mentions, 99)), + ); let mut rounded_view = self.view(cx, ids!(rounded_view)); script_apply_eval!(cx, rounded_view, { draw_bg +: { @@ -114,8 +118,10 @@ impl Widget for UnreadBadge { // If there are no unread mentions but there are unread messages, show gray badge and the number of unread messages else if self.unread_messages > 0 { let (border_size, plus_sign) = format_border_and_truncation(self.unread_messages); - self.label(cx, ids!(label_count)) - .set_text(cx, &format!("{}{plus_sign}", std::cmp::min(self.unread_messages, 99))); + self.label(cx, ids!(label_count)).set_text( + cx, + &format!("{}{plus_sign}", std::cmp::min(self.unread_messages, 99)), + ); let mut rounded_view = self.view(cx, ids!(rounded_view)); script_apply_eval!(cx, rounded_view, { draw_bg +: { @@ -124,8 +130,7 @@ impl Widget for UnreadBadge { } }); self.visible = true; - } - else { + } else { // If there are no unreads of any kind, hide the badge self.visible = false; } @@ -136,7 +141,12 @@ impl Widget for UnreadBadge { impl UnreadBadgeRef { /// Sets the unread mentions and messages counts without explicitly redrawing the badge. - pub fn update_counts(&self, is_marked_unread: bool, num_unread_mentions: u64, num_unread_messages: u64) { + pub fn update_counts( + &self, + is_marked_unread: bool, + num_unread_mentions: u64, + num_unread_messages: u64, + ) { if let Some(mut inner) = self.borrow_mut() { inner.is_marked_unread = is_marked_unread; inner.unread_mentions = num_unread_mentions; diff --git a/src/shared/verification_badge.rs b/src/shared/verification_badge.rs index 2a0ef3588..e2a4b5b86 100644 --- a/src/shared/verification_badge.rs +++ b/src/shared/verification_badge.rs @@ -7,7 +7,6 @@ use crate::{ verification::VerificationStateAction, }; - // First, define the verification icons component layout script_mod! { use mod.prelude.widgets.* @@ -159,10 +158,7 @@ impl VerificationBadgeRef { please verify Robrix from another client.", Some(COLOR_FG_DANGER_RED), ), - _ => ( - "Verification state is unknown.", - None, - ), + _ => ("Verification state is unknown.", None), } } } diff --git a/src/space_service_sync.rs b/src/space_service_sync.rs index c02bbc8a1..a2c450a28 100644 --- a/src/space_service_sync.rs +++ b/src/space_service_sync.rs @@ -1,16 +1,33 @@ //! Background tasks that subscribe to the Matrix SpaceService in order to //! track changes to the user's joined spaces and send updates the UI. -use std::{collections::{HashMap, HashSet, hash_map::Entry}, iter::Peekable, sync::Arc}; +use std::{ + collections::{HashMap, HashSet, hash_map::Entry}, + iter::Peekable, + sync::Arc, +}; use eyeball_im::VectorDiff; use futures_util::StreamExt; use imbl::Vector; use makepad_widgets::*; use matrix_sdk::{Client, RoomState, media::MediaRequestParameters}; -use matrix_sdk_ui::spaces::{SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState}; +use matrix_sdk_ui::spaces::{ + SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState, +}; use ruma::{OwnedMxcUri, OwnedRoomId, events::room::MediaSource, room::RoomType}; -use tokio::{runtime::Handle, sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::JoinHandle}; -use crate::{home::{rooms_list::{RoomsListUpdate, enqueue_rooms_list_update}, spaces_bar::{JoinedSpaceInfo, SpacesListUpdate, enqueue_spaces_list_update}}, room::FetchedRoomAvatar, utils::{self, RoomNameId}}; +use tokio::{ + runtime::Handle, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinHandle, +}; +use crate::{ + home::{ + rooms_list::{RoomsListUpdate, enqueue_rooms_list_update}, + spaces_bar::{JoinedSpaceInfo, SpacesListUpdate, enqueue_spaces_list_update}, + }, + room::FetchedRoomAvatar, + utils::{self, RoomNameId}, +}; /// Whether to enable verbose logging of all spaces service diff updates. const LOG_SPACE_SERVICE_DIFFS: bool = cfg!(feature = "log_space_service_diffs"); @@ -21,7 +38,6 @@ const LOG_SPACE_SERVICE_DIFFS: bool = cfg!(feature = "log_space_service_diffs"); /// while the last element is the direct parent. pub type ParentChain = SmallVec<[OwnedRoomId; 2]>; - /// Requests related to obtaining info about Spaces, via the background space service. pub enum SpaceRequest { /// Start obtaining the list of rooms in the given space from the homeserver, @@ -34,15 +50,11 @@ pub enum SpaceRequest { /// /// Note: the Matrix SDK offers no way to unsubscribe from a space room list, /// so this just stops the async background task that runs the subscriber loop. - UnsubscribeFromSpaceRoomList { - space_id: OwnedRoomId, - }, + UnsubscribeFromSpaceRoomList { space_id: OwnedRoomId }, /// Leave the given space and all joined rooms within it. /// /// Will emit a [`SpaceRoomListAction::LeaveSpaceResult`] action. - LeaveSpace { - space_name_id: RoomNameId, - }, + LeaveSpace { space_name_id: RoomNameId }, /// Paginate the given space's room list, i.e., fetch the next batch of rooms in the list. /// /// This will result in a [`SpaceRoomListAction::PaginationState`] action being emitted, @@ -70,9 +82,7 @@ pub enum SpaceRequest { /// Get full details about a top-level space. /// /// This will result in a [`SpaceRoomListAction::TopLevelSpaceDetails`] action being emitted. - GetTopLevelSpaceDetails { - space_id: OwnedRoomId, - }, + GetTopLevelSpaceDetails { space_id: OwnedRoomId }, } /// Internal requests sent from the [`space_service_loop`] to a specific space's [`space_room_list_loop`]. @@ -88,13 +98,15 @@ enum SpaceRoomListRequest { Shutdown, } - /// The main async loop task that listens for changes to all top-level joined spaces. pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { // Create a channel for sending space-related requests to this background worker. - let (space_request_sender, mut receiver) = tokio::sync::mpsc::unbounded_channel::(); + let (space_request_sender, mut receiver) = + tokio::sync::mpsc::unbounded_channel::(); // Give the request sender channel endpoint to the RoomsList widget. - enqueue_rooms_list_update(RoomsListUpdate::SpaceRequestSender(space_request_sender.clone())); + enqueue_rooms_list_update(RoomsListUpdate::SpaceRequestSender( + space_request_sender.clone(), + )); // Create the actual space service. let space_service = SpaceService::new(client.clone()).await; @@ -103,247 +115,256 @@ pub async fn space_service_loop(client: Client) -> anyhow::Result<()> { // along with a sender to send `SpaceRoomListRequest`s to those tasks. let mut space_room_list_tasks = HashMap::new(); // A closure to make it easier to use/spawn a `space_room_list_loop` task. - let get_or_spawn_space_room_list = async | - space_room_list_tasks: &mut HashMap, JoinHandle<()>)>, - space_id: &OwnedRoomId, - parent_chain: &ParentChain, - | -> UnboundedSender { + let get_or_spawn_space_room_list = async |space_room_list_tasks: &mut HashMap< + OwnedRoomId, + (UnboundedSender, JoinHandle<()>), + >, + space_id: &OwnedRoomId, + parent_chain: &ParentChain| + -> UnboundedSender { match space_room_list_tasks.entry(space_id.clone()) { Entry::Occupied(occ) => occ.get().0.clone(), Entry::Vacant(vac) => { - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); + let (sender, receiver) = + tokio::sync::mpsc::unbounded_channel::(); let space_room_list = space_service.space_room_list(space_id.clone()).await; - let join_handle = Handle::current().spawn( - space_room_list_loop( - space_id.clone(), - parent_chain.clone(), - receiver, - space_room_list, - space_request_sender.clone(), - ) - ); - vac.insert((sender, join_handle)) - .0.clone() + let join_handle = Handle::current().spawn(space_room_list_loop( + space_id.clone(), + parent_chain.clone(), + receiver, + space_room_list, + space_request_sender.clone(), + )); + vac.insert((sender, join_handle)).0.clone() } } }; // Get the set of top-level (root) spaces that the user has joined. - let (initial_spaces, mut spaces_diff_stream) = space_service.subscribe_to_top_level_joined_spaces().await; + let (initial_spaces, mut spaces_diff_stream) = + space_service.subscribe_to_top_level_joined_spaces().await; for space in &initial_spaces { add_new_space(space, &client).await; } let mut all_joined_spaces: Vector = initial_spaces; - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: initial set: {all_joined_spaces:?}"); } - - - loop { tokio::select! { - // Handle new space requests. - request_opt = receiver.recv() => { - let Some(request) = request_opt else { break }; - match request { - SpaceRequest::GetChildren { space_id, parent_chain } => { - let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; - if sender.send(SpaceRoomListRequest::GetChildren).is_err() { - error!("BUG: failed to send GetRooms request to space room list loop for space {space_id}"); + if LOG_SPACE_SERVICE_DIFFS { + log!("space_service: initial set: {all_joined_spaces:?}"); + } + + loop { + tokio::select! { + // Handle new space requests. + request_opt = receiver.recv() => { + let Some(request) = request_opt else { break }; + match request { + SpaceRequest::GetChildren { space_id, parent_chain } => { + let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; + if sender.send(SpaceRoomListRequest::GetChildren).is_err() { + error!("BUG: failed to send GetRooms request to space room list loop for space {space_id}"); + } } - } - SpaceRequest::SubscribeToSpaceRoomList { space_id, parent_chain } => { - let _sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; - } - SpaceRequest::PaginateSpaceRoomList { space_id, parent_chain } => { - let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; - if sender.send(SpaceRoomListRequest::Paginate).is_err() { - error!("BUG: failed to send paginate request to space room list loop for space {space_id}"); + SpaceRequest::SubscribeToSpaceRoomList { space_id, parent_chain } => { + let _sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; } - } - SpaceRequest::UnsubscribeFromSpaceRoomList { space_id } => { - if let Some((sender, join_handle)) = space_room_list_tasks.remove(&space_id) { - let _ = sender.send(SpaceRoomListRequest::Shutdown); - join_handle.abort(); + SpaceRequest::PaginateSpaceRoomList { space_id, parent_chain } => { + let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; + if sender.send(SpaceRoomListRequest::Paginate).is_err() { + error!("BUG: failed to send paginate request to space room list loop for space {space_id}"); + } } - } - SpaceRequest::LeaveSpace { space_name_id } => { - match space_service.leave_space(space_name_id.room_id()).await { - Ok(leave_handle) => { - match leave_handle.leave(|_| true).await { - Ok(()) => { - if let Some((sender, join_handle)) = space_room_list_tasks.remove(space_name_id.room_id()) { - match sender.send(SpaceRoomListRequest::Shutdown) { - // If we successfully sent shutdown message, just let the space room list loop task - // end gracefully on its own in the background. - Ok(_) => { } - // If we failed to send the shutdown message, just abort the space room list loop task. - Err(_) => join_handle.abort(), + SpaceRequest::UnsubscribeFromSpaceRoomList { space_id } => { + if let Some((sender, join_handle)) = space_room_list_tasks.remove(&space_id) { + let _ = sender.send(SpaceRoomListRequest::Shutdown); + join_handle.abort(); + } + } + SpaceRequest::LeaveSpace { space_name_id } => { + match space_service.leave_space(space_name_id.room_id()).await { + Ok(leave_handle) => { + match leave_handle.leave(|_| true).await { + Ok(()) => { + if let Some((sender, join_handle)) = space_room_list_tasks.remove(space_name_id.room_id()) { + match sender.send(SpaceRoomListRequest::Shutdown) { + // If we successfully sent shutdown message, just let the space room list loop task + // end gracefully on its own in the background. + Ok(_) => { } + // If we failed to send the shutdown message, just abort the space room list loop task. + Err(_) => join_handle.abort(), + } } + Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result: Ok(()), + }); + } + Err(error) => { + error!("LeaveSpace: failed to leave all rooms in space {space_name_id}: {error:?}"); + Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result: Err(error), + }); } - Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { - space_name_id, - result: Ok(()), - }); - } - Err(error) => { - error!("LeaveSpace: failed to leave all rooms in space {space_name_id}: {error:?}"); - Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { - space_name_id, - result: Err(error), - }); } } - } - Err(error) => { - error!("Failed to leave space {space_name_id}: {error:?}"); - Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { - space_name_id, - result: Err(error), - }); + Err(error) => { + error!("Failed to leave space {space_name_id}: {error:?}"); + Cx::post_action(SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result: Err(error), + }); + } } } - } - SpaceRequest::GetDetailedChildren { space_id, parent_chain } => { - let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; - if sender.send(SpaceRoomListRequest::GetDetailedChildren).is_err() { - error!("BUG: failed to send GetDetailedChildren request to space room list loop for space {space_id}"); + SpaceRequest::GetDetailedChildren { space_id, parent_chain } => { + let sender = get_or_spawn_space_room_list(&mut space_room_list_tasks, &space_id, &parent_chain).await; + if sender.send(SpaceRoomListRequest::GetDetailedChildren).is_err() { + error!("BUG: failed to send GetDetailedChildren request to space room list loop for space {space_id}"); + } } - } - SpaceRequest::GetTopLevelSpaceDetails { space_id } => { - if let Some(space) = all_joined_spaces.iter().find(|s| s.room_id == space_id) { - Cx::post_action(SpaceRoomListAction::TopLevelSpaceDetails(space.clone())); - } else { - error!("GetSpaceDetails: space {space_id} not found in all_joined_spaces"); + SpaceRequest::GetTopLevelSpaceDetails { space_id } => { + if let Some(space) = all_joined_spaces.iter().find(|s| s.room_id == space_id) { + Cx::post_action(SpaceRoomListAction::TopLevelSpaceDetails(space.clone())); + } else { + error!("GetSpaceDetails: space {space_id} not found in all_joined_spaces"); + } } } } - } - // Handle updates to the list of spaces. - batch_opt = spaces_diff_stream.next() => { - let Some(batch) = batch_opt else { break }; - let mut peekable_diffs = batch.into_iter().peekable(); - while let Some(diff) = peekable_diffs.next() { - match diff { - VectorDiff::Append { values: new_spaces } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Append {}", new_spaces.len()); } - for new_space in new_spaces { + // Handle updates to the list of spaces. + batch_opt = spaces_diff_stream.next() => { + let Some(batch) = batch_opt else { break }; + let mut peekable_diffs = batch.into_iter().peekable(); + while let Some(diff) = peekable_diffs.next() { + match diff { + VectorDiff::Append { values: new_spaces } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Append {}", new_spaces.len()); } + for new_space in new_spaces { + add_new_space(&new_space, &client).await; + all_joined_spaces.push_back(new_space); + } + } + VectorDiff::Clear => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Clear"); } + all_joined_spaces.clear(); + enqueue_spaces_list_update(SpacesListUpdate::ClearSpaces); + } + VectorDiff::PushFront { value: new_space } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PushFront"); } + add_new_space(&new_space, &client).await; + all_joined_spaces.push_front(new_space); + } + VectorDiff::PushBack { value: new_space } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PushBack"); } add_new_space(&new_space, &client).await; all_joined_spaces.push_back(new_space); } - } - VectorDiff::Clear => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Clear"); } - all_joined_spaces.clear(); - enqueue_spaces_list_update(SpacesListUpdate::ClearSpaces); - } - VectorDiff::PushFront { value: new_space } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PushFront"); } - add_new_space(&new_space, &client).await; - all_joined_spaces.push_front(new_space); - } - VectorDiff::PushBack { value: new_space } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PushBack"); } - add_new_space(&new_space, &client).await; - all_joined_spaces.push_back(new_space); - } - remove_diff @ VectorDiff::PopFront => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PopFront"); } - if let Some(space) = all_joined_spaces.pop_front() { - optimize_remove_then_add_into_update( - remove_diff, - space, - &mut peekable_diffs, - &mut all_joined_spaces, - &client, - ).await; + remove_diff @ VectorDiff::PopFront => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PopFront"); } + if let Some(space) = all_joined_spaces.pop_front() { + optimize_remove_then_add_into_update( + remove_diff, + space, + &mut peekable_diffs, + &mut all_joined_spaces, + &client, + ).await; + } } - } - remove_diff @ VectorDiff::PopBack => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PopBack"); } - if let Some(space) = all_joined_spaces.pop_back() { - optimize_remove_then_add_into_update( - remove_diff, - space, - &mut peekable_diffs, - &mut all_joined_spaces, - &client, - ).await; + remove_diff @ VectorDiff::PopBack => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff PopBack"); } + if let Some(space) = all_joined_spaces.pop_back() { + optimize_remove_then_add_into_update( + remove_diff, + space, + &mut peekable_diffs, + &mut all_joined_spaces, + &client, + ).await; + } } - } - VectorDiff::Insert { index, value: new_space } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Insert at {index}"); } - add_new_space(&new_space, &client).await; - all_joined_spaces.insert(index, new_space); - } - VectorDiff::Set { index, value: changed_space } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Set at {index}"); } - if let Some(old_space) = all_joined_spaces.get(index) { - update_space(old_space, &changed_space, &client).await; - } else { - error!("BUG: space_service diff: Set index {index} was out of bounds."); + VectorDiff::Insert { index, value: new_space } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Insert at {index}"); } + add_new_space(&new_space, &client).await; + all_joined_spaces.insert(index, new_space); } - all_joined_spaces.set(index, changed_space); - } - remove_diff @ VectorDiff::Remove { index: remove_index } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Remove at {remove_index}"); } - if remove_index < all_joined_spaces.len() { - let space = all_joined_spaces.remove(remove_index); - optimize_remove_then_add_into_update( - remove_diff, - space, - &mut peekable_diffs, - &mut all_joined_spaces, - &client, - ).await; - } else { - error!("BUG: space_service: diff Remove index {remove_index} out of bounds, len {}", all_joined_spaces.len()); + VectorDiff::Set { index, value: changed_space } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Set at {index}"); } + if let Some(old_space) = all_joined_spaces.get(index) { + update_space(old_space, &changed_space, &client).await; + } else { + error!("BUG: space_service diff: Set index {index} was out of bounds."); + } + all_joined_spaces.set(index, changed_space); } - } - VectorDiff::Truncate { length } => { - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Truncate to {length}"); } - // Iterate manually so we can know which spaces are being removed. - while all_joined_spaces.len() > length { - if let Some(space) = all_joined_spaces.pop_back() { - remove_space(&space); + remove_diff @ VectorDiff::Remove { index: remove_index } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Remove at {remove_index}"); } + if remove_index < all_joined_spaces.len() { + let space = all_joined_spaces.remove(remove_index); + optimize_remove_then_add_into_update( + remove_diff, + space, + &mut peekable_diffs, + &mut all_joined_spaces, + &client, + ).await; + } else { + error!("BUG: space_service: diff Remove index {remove_index} out of bounds, len {}", all_joined_spaces.len()); } } - all_joined_spaces.truncate(length); // sanity check - } - VectorDiff::Reset { values: new_spaces } => { - // We implement this by clearing all spaces and then adding back the new values. - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Reset, old length {}, new length {}", all_joined_spaces.len(), new_spaces.len()); } - // Iterate manually so we can know which spaces are being removed. - while let Some(space) = all_joined_spaces.pop_back() { - remove_space(&space); + VectorDiff::Truncate { length } => { + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Truncate to {length}"); } + // Iterate manually so we can know which spaces are being removed. + while all_joined_spaces.len() > length { + if let Some(space) = all_joined_spaces.pop_back() { + remove_space(&space); + } + } + all_joined_spaces.truncate(length); // sanity check } - enqueue_spaces_list_update(SpacesListUpdate::ClearSpaces); - for new_space in &new_spaces { - add_new_space(new_space, &client).await; + VectorDiff::Reset { values: new_spaces } => { + // We implement this by clearing all spaces and then adding back the new values. + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: diff Reset, old length {}, new length {}", all_joined_spaces.len(), new_spaces.len()); } + // Iterate manually so we can know which spaces are being removed. + while let Some(space) = all_joined_spaces.pop_back() { + remove_space(&space); + } + enqueue_spaces_list_update(SpacesListUpdate::ClearSpaces); + for new_space in &new_spaces { + add_new_space(new_space, &client).await; + } + all_joined_spaces = new_spaces; } - all_joined_spaces = new_spaces; } } + if LOG_SPACE_SERVICE_DIFFS { log!("space_service: after batch diff: {all_joined_spaces:?}"); } } - if LOG_SPACE_SERVICE_DIFFS { log!("space_service: after batch diff: {all_joined_spaces:?}"); } - } - else => { - break; + else => { + break; + } } - } } + } anyhow::bail!("Space service sync loop ended unexpectedly") } - async fn add_new_space(space: &SpaceRoom, client: &Client) { let space_avatar_opt = if let Some(url) = &space.avatar_url { fetch_space_avatar(url.clone(), client) .await - .inspect_err(|e| error!("Failed to fetch avatar for new space {:?} ({}): {e}", space.display_name, space.room_id)) + .inspect_err(|e| { + error!( + "Failed to fetch avatar for new space {:?} ({}): {e}", + space.display_name, space.room_id + ) + }) .ok() - } else { None }; - let space_avatar = space_avatar_opt.unwrap_or_else( - || utils::avatar_from_room_name(Some(&space.display_name)) - ); + } else { + None + }; + let space_avatar = + space_avatar_opt.unwrap_or_else(|| utils::avatar_from_room_name(Some(&space.display_name))); let jsi = JoinedSpaceInfo { space_name_id: RoomNameId::new( @@ -362,7 +383,6 @@ async fn add_new_space(space: &SpaceRoom, client: &Client) { enqueue_spaces_list_update(SpacesListUpdate::AddJoinedSpace(jsi)); } - /// Attempts to optimize a common SpaceService operation of remove + add. /// /// If a `Remove` diff (or `PopBack` or `PopFront`) is immediately followed by @@ -381,31 +401,37 @@ async fn optimize_remove_then_add_into_update( ) { let next_diff_was_handled: bool; match peekable_diffs.peek() { - Some(VectorDiff::Insert { index: insert_index, value: new_space }) - if space.room_id == new_space.room_id => - { + Some(VectorDiff::Insert { + index: insert_index, + value: new_space, + }) if space.room_id == new_space.room_id => { if LOG_SPACE_SERVICE_DIFFS { - log!("Optimizing {remove_diff:?} + Insert({insert_index}) into Update for space {}", space.room_id); + log!( + "Optimizing {remove_diff:?} + Insert({insert_index}) into Update for space {}", + space.room_id + ); } update_space(&space, new_space, client).await; all_joined_spaces.insert(*insert_index, new_space.clone()); next_diff_was_handled = true; } - Some(VectorDiff::PushFront { value: new_space }) - if space.room_id == new_space.room_id => - { + Some(VectorDiff::PushFront { value: new_space }) if space.room_id == new_space.room_id => { if LOG_SPACE_SERVICE_DIFFS { - log!("Optimizing {remove_diff:?} + PushFront into Update for space {}", space.room_id); + log!( + "Optimizing {remove_diff:?} + PushFront into Update for space {}", + space.room_id + ); } update_space(&space, new_space, client).await; all_joined_spaces.push_front(new_space.clone()); next_diff_was_handled = true; } - Some(VectorDiff::PushBack { value: new_space }) - if space.room_id == new_space.room_id => - { + Some(VectorDiff::PushBack { value: new_space }) if space.room_id == new_space.room_id => { if LOG_SPACE_SERVICE_DIFFS { - log!("Optimizing {remove_diff:?} + PushBack into Update for space {}", space.room_id); + log!( + "Optimizing {remove_diff:?} + PushBack into Update for space {}", + space.room_id + ); } update_space(&space, new_space, client).await; all_joined_spaces.push_back(new_space.clone()); @@ -420,29 +446,35 @@ async fn optimize_remove_then_add_into_update( } } - /// Invoked when the space service has received an update that changes an existing space. -async fn update_space( - old_space: &SpaceRoom, - new_space: &SpaceRoom, - client: &Client, -) { +async fn update_space(old_space: &SpaceRoom, new_space: &SpaceRoom, client: &Client) { let new_space_id = new_space.room_id.clone(); if old_space.room_id == new_space_id { // Handle state transitions for a space. if LOG_SPACE_SERVICE_DIFFS { - log!("Space {:?} ({new_space_id}) state went from {:?} --> {:?}", new_space.display_name, old_space.state, new_space.state); + log!( + "Space {:?} ({new_space_id}) state went from {:?} --> {:?}", + new_space.display_name, + old_space.state, + new_space.state + ); } if old_space.state != new_space.state { match new_space.state { Some(RoomState::Banned) => { // TODO: handle spaces that this user has been banned from. - log!("Removing Banned space: {:?} ({new_space_id})", new_space.display_name); + log!( + "Removing Banned space: {:?} ({new_space_id})", + new_space.display_name + ); remove_space(new_space); return; } Some(RoomState::Left) => { - log!("Removing Left space: {:?} ({new_space_id})", new_space.display_name); + log!( + "Removing Left space: {:?} ({new_space_id})", + new_space.display_name + ); // TODO: instead of removing this, we could optionally add it to // a separate list of left space, which would be collapsed by default. // Upon clicking a left space, we could show a splash page @@ -452,12 +484,18 @@ async fn update_space( return; } Some(RoomState::Joined) => { - log!("update_space(): adding new Joined space: {:?} ({new_space_id})", new_space.display_name); + log!( + "update_space(): adding new Joined space: {:?} ({new_space_id})", + new_space.display_name + ); add_new_space(new_space, client).await; return; } Some(RoomState::Invited) => { - log!("update_space(): adding new Invited space: {:?} ({new_space_id})", new_space.display_name); + log!( + "update_space(): adding new Invited space: {:?} ({new_space_id})", + new_space.display_name + ); add_new_space(new_space, client).await; return; } @@ -466,13 +504,21 @@ async fn update_space( return; } None => { - error!("WARNING: UNTESTED: new space {} ({}) RoomState is None", new_space.display_name, new_space.room_id); + error!( + "WARNING: UNTESTED: new space {} ({}) RoomState is None", + new_space.display_name, new_space.room_id + ); } } } if old_space.canonical_alias != new_space.canonical_alias { - log!("Updating space {} alias: {:?} --> {:?}", new_space_id, old_space.canonical_alias, new_space.canonical_alias); + log!( + "Updating space {} alias: {:?} --> {:?}", + new_space_id, + old_space.canonical_alias, + new_space.canonical_alias + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateCanonicalAlias { space_id: new_space_id.clone(), new_canonical_alias: new_space.canonical_alias.clone(), @@ -480,7 +526,12 @@ async fn update_space( } if old_space.display_name != new_space.display_name { - log!("Updating space {} name: {:?} --> {:?}", new_space_id, old_space.display_name, new_space.display_name); + log!( + "Updating space {} name: {:?} --> {:?}", + new_space_id, + old_space.display_name, + new_space.display_name + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateSpaceName { space_id: new_space_id.clone(), new_space_name: new_space.display_name.clone(), @@ -488,7 +539,12 @@ async fn update_space( } if old_space.topic != new_space.topic { - log!("Updating space {} topic:\n {:?}\n -->\n {:?}", new_space_id, old_space.topic, new_space.topic); + log!( + "Updating space {} topic:\n {:?}\n -->\n {:?}", + new_space_id, + old_space.topic, + new_space.topic + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateSpaceTopic { space_id: new_space_id.clone(), topic: new_space.topic.clone(), @@ -507,18 +563,32 @@ async fn update_space( let space_avatar_opt = if let Some(url) = url_opt { fetch_space_avatar(url, &client2) .await - .inspect_err(|e| error!("Failed to fetch avatar for space {:?} ({}): {e}", space_display_name, space_id)) + .inspect_err(|e| { + error!( + "Failed to fetch avatar for space {:?} ({}): {e}", + space_display_name, space_id + ) + }) .ok() - } else { None }; - let avatar = space_avatar_opt.unwrap_or_else( - || utils::avatar_from_room_name(Some(&space_display_name)) - ); - enqueue_spaces_list_update(SpacesListUpdate::UpdateSpaceAvatar { space_id, avatar }); + } else { + None + }; + let avatar = space_avatar_opt + .unwrap_or_else(|| utils::avatar_from_room_name(Some(&space_display_name))); + enqueue_spaces_list_update(SpacesListUpdate::UpdateSpaceAvatar { + space_id, + avatar, + }); }); } if old_space.num_joined_members != new_space.num_joined_members { - log!("Updating space {} joined members: {} --> {}", new_space_id, old_space.num_joined_members, new_space.num_joined_members); + log!( + "Updating space {} joined members: {} --> {}", + new_space_id, + old_space.num_joined_members, + new_space.num_joined_members + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateNumJoinedMembers { space_id: new_space_id.clone(), num_joined_members: new_space.num_joined_members, @@ -526,7 +596,12 @@ async fn update_space( } if old_space.join_rule != new_space.join_rule { - log!("Updating space {} join rule: {:?} --> {:?}", new_space_id, old_space.join_rule, new_space.join_rule); + log!( + "Updating space {} join rule: {:?} --> {:?}", + new_space_id, + old_space.join_rule, + new_space.join_rule + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateJoinRule { space_id: new_space_id.clone(), join_rule: new_space.join_rule.clone(), @@ -534,7 +609,12 @@ async fn update_space( } if old_space.world_readable != new_space.world_readable { - log!("Updating space {} world readable: {:?} --> {:?}", new_space_id, old_space.world_readable, new_space.world_readable); + log!( + "Updating space {} world readable: {:?} --> {:?}", + new_space_id, + old_space.world_readable, + new_space.world_readable + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateWorldReadable { space_id: new_space_id.clone(), world_readable: new_space.world_readable, @@ -542,7 +622,12 @@ async fn update_space( } if old_space.guest_can_join != new_space.guest_can_join { - log!("Updating space {} guest can join: {:?} --> {:?}", new_space_id, old_space.guest_can_join, new_space.guest_can_join); + log!( + "Updating space {} guest can join: {:?} --> {:?}", + new_space_id, + old_space.guest_can_join, + new_space.guest_can_join + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateGuestCanJoin { space_id: new_space_id.clone(), guest_can_join: new_space.guest_can_join, @@ -550,23 +635,28 @@ async fn update_space( } if old_space.children_count != new_space.children_count { - log!("Updating space {} children count: {:?} --> {:?}", new_space_id, old_space.children_count, new_space.children_count); + log!( + "Updating space {} children count: {:?} --> {:?}", + new_space_id, + old_space.children_count, + new_space.children_count + ); enqueue_spaces_list_update(SpacesListUpdate::UpdateChildrenCount { space_id: new_space_id.clone(), children_count: new_space.children_count, }); } - } - else { - warning!("UNTESTED SCENARIO: update_space(): removing old room {}, replacing with new room {}", - old_space.room_id, new_space_id, + } else { + warning!( + "UNTESTED SCENARIO: update_space(): removing old room {}, replacing with new room {}", + old_space.room_id, + new_space_id, ); remove_space(old_space); add_new_space(new_space, client).await; } } - /// Invoked when the space service has received an update to remove an existing space. fn remove_space(space: &SpaceRoom) { enqueue_spaces_list_update(SpacesListUpdate::RemoveSpace { @@ -575,23 +665,24 @@ fn remove_space(space: &SpaceRoom) { }); } - /// Fetches the avatar for the space at the given URL. /// /// Returns `Some` if the avatar image was successfully fetched. -async fn fetch_space_avatar(url: OwnedMxcUri, client: &Client) -> matrix_sdk::Result { +async fn fetch_space_avatar( + url: OwnedMxcUri, + client: &Client, +) -> matrix_sdk::Result { let request = MediaRequestParameters { source: MediaSource::Plain(url), format: utils::AVATAR_THUMBNAIL_FORMAT.into(), }; - client.media() + client + .media() .get_media_content(&request, true) .await .map(|img_data| FetchedRoomAvatar::Image(img_data.into())) } - - /// Extension trait for `SpaceRoom` to provide utility methods. pub trait SpaceRoomExt { /// Returns true if this `SpaceRoom` is a space itself; @@ -605,8 +696,6 @@ impl SpaceRoomExt for SpaceRoom { } } - - /// A loop that listens for changes to the set of rooms in a given space. async fn space_room_list_loop( space_id: OwnedRoomId, @@ -628,87 +717,96 @@ async fn space_room_list_loop( }), }; - // First, we paginate the space once to get at least *some* child rooms. + // First, we paginate the space once to get at least *some* child rooms. paginate_once().await; // The set of subspaces within this `space_id` that are already known to us. let mut known_subspaces = HashSet::new(); - let (mut all_rooms_in_space, mut space_room_stream) = space_room_list.subscribe_to_room_updates(); - handle_subspaces(&space_id, &parent_chain, &mut known_subspaces, all_rooms_in_space.iter(), &request_sender); + let (mut all_rooms_in_space, mut space_room_stream) = + space_room_list.subscribe_to_room_updates(); + handle_subspaces( + &space_id, + &parent_chain, + &mut known_subspaces, + all_rooms_in_space.iter(), + &request_sender, + ); // A tuple of: the latest `(direct child rooms, and direct subspaces)` within this space. // This makes it very cheap & fast to repeatedly handle `GetChildren` requests. let mut cached_hash_sets = space_children_to_hash_sets(&all_rooms_in_space); - loop { tokio::select! { - // Handle new requests. - request_opt = receiver.recv() => { - let Some(request) = request_opt else { break }; - match request { - SpaceRoomListRequest::GetChildren => { - Cx::post_action(SpaceRoomListAction::UpdatedChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - direct_child_rooms: Arc::clone(&cached_hash_sets.0), - direct_subspaces: Arc::clone(&cached_hash_sets.1), - }); - } - SpaceRoomListRequest::GetDetailedChildren => { - Cx::post_action(SpaceRoomListAction::DetailedChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - // The `imbl::Vector` type is very cheap to clone here - // because we're not modifying it, so we just send that value directly. - children: all_rooms_in_space.clone(), - }); - } - SpaceRoomListRequest::Paginate => { - paginate_once().await; + loop { + tokio::select! { + // Handle new requests. + request_opt = receiver.recv() => { + let Some(request) = request_opt else { break }; + match request { + SpaceRoomListRequest::GetChildren => { + Cx::post_action(SpaceRoomListAction::UpdatedChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + direct_child_rooms: Arc::clone(&cached_hash_sets.0), + direct_subspaces: Arc::clone(&cached_hash_sets.1), + }); + } + SpaceRoomListRequest::GetDetailedChildren => { + Cx::post_action(SpaceRoomListAction::DetailedChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + // The `imbl::Vector` type is very cheap to clone here + // because we're not modifying it, so we just send that value directly. + children: all_rooms_in_space.clone(), + }); + } + SpaceRoomListRequest::Paginate => { + paginate_once().await; + } + SpaceRoomListRequest::Shutdown => return, } - SpaceRoomListRequest::Shutdown => return, } - } - // Handle updates to the list of rooms and subspaces in this space. - batch_opt = space_room_stream.next() => { - let Some(batch) = batch_opt else { break }; - for diff in batch { - // Manually inspect any diff that could result in new space room(s), - // such that we can check to see if any of them are nested subspaces. - match &diff { - VectorDiff::Append { values } - | VectorDiff::Reset { values } => handle_subspaces( - &space_id, - &parent_chain, - &mut known_subspaces, - values.iter(), - &request_sender, - ), - VectorDiff::PushFront { value } - | VectorDiff::PushBack { value } - | VectorDiff::Insert { value, .. } - | VectorDiff::Set { value, .. } => handle_subspaces( - &space_id, - &parent_chain, - &mut known_subspaces, - std::iter::once(value), - &request_sender, - ), - _ => { } - }; - diff.apply(&mut all_rooms_in_space); + // Handle updates to the list of rooms and subspaces in this space. + batch_opt = space_room_stream.next() => { + let Some(batch) = batch_opt else { break }; + for diff in batch { + // Manually inspect any diff that could result in new space room(s), + // such that we can check to see if any of them are nested subspaces. + match &diff { + VectorDiff::Append { values } + | VectorDiff::Reset { values } => handle_subspaces( + &space_id, + &parent_chain, + &mut known_subspaces, + values.iter(), + &request_sender, + ), + VectorDiff::PushFront { value } + | VectorDiff::PushBack { value } + | VectorDiff::Insert { value, .. } + | VectorDiff::Set { value, .. } => handle_subspaces( + &space_id, + &parent_chain, + &mut known_subspaces, + std::iter::once(value), + &request_sender, + ), + _ => { } + }; + diff.apply(&mut all_rooms_in_space); + } + // Here: children have changed, so we re-calculate the sets of child rooms and subspaces. + cached_hash_sets = space_children_to_hash_sets(&all_rooms_in_space); + Cx::post_action(SpaceRoomListAction::UpdatedChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + direct_child_rooms: Arc::clone(&cached_hash_sets.0), + direct_subspaces: Arc::clone(&cached_hash_sets.1), + }); } - // Here: children have changed, so we re-calculate the sets of child rooms and subspaces. - cached_hash_sets = space_children_to_hash_sets(&all_rooms_in_space); - Cx::post_action(SpaceRoomListAction::UpdatedChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - direct_child_rooms: Arc::clone(&cached_hash_sets.0), - direct_subspaces: Arc::clone(&cached_hash_sets.1), - }); } - } } + } } /// Finds nested/subspaces within a list of space rooms and submits a request @@ -720,7 +818,7 @@ fn handle_subspaces<'a>( changed_space_rooms: impl Iterator, request_sender: &UnboundedSender, ) { - for sr in changed_space_rooms.filter(|&sr| sr.is_space()) { + for sr in changed_space_rooms.filter(|&sr| sr.is_space()) { if known_subspaces.contains(&sr.room_id) { continue; } @@ -732,11 +830,17 @@ fn handle_subspaces<'a>( npc.push(parent_space_id.clone()); npc }; - if request_sender.send(SpaceRequest::SubscribeToSpaceRoomList { - space_id: sr.room_id.clone(), - parent_chain: new_parent_chain, - }).is_err() { - error!("BUG: failed to send subscribe request to nested/subspace {}.", sr.room_id); + if request_sender + .send(SpaceRequest::SubscribeToSpaceRoomList { + space_id: sr.room_id.clone(), + parent_chain: new_parent_chain, + }) + .is_err() + { + error!( + "BUG: failed to send subscribe request to nested/subspace {}.", + sr.room_id + ); } } } @@ -745,7 +849,7 @@ fn handle_subspaces<'a>( /// 1. the set of child rooms directly within this space. /// 2. the set of subspaces directly within this space. fn space_children_to_hash_sets( - all_rooms_in_space: &Vector + all_rooms_in_space: &Vector, ) -> (Arc>, Arc>) { let mut direct_child_rooms = HashSet::new(); let mut direct_subspaces = HashSet::new(); @@ -807,45 +911,55 @@ pub enum SpaceRoomListAction { impl std::fmt::Debug for SpaceRoomListAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - SpaceRoomListAction::UpdatedChildren { space_id, parent_chain, direct_child_rooms, direct_subspaces } => { - f.debug_struct("SpaceRoomListAction::UpdatedChildren") - .field("space_id", space_id) - .field("parent_chain", &parent_chain) - .field("num_direct_child_rooms", &direct_child_rooms.len()) - .field("num_direct_subspaces", &direct_subspaces.len()) - .finish() - } - SpaceRoomListAction::PaginationState { space_id, parent_chain, state } => { - f.debug_struct("SpaceRoomListAction::PaginationState") - .field("space_id", space_id) - .field("parent_chain", &parent_chain) - .field("state", state) - .finish() - } - SpaceRoomListAction::PaginationError { space_id, error } => { - f.debug_struct("SpaceRoomListAction::PaginationError") - .field("space_id", space_id) - .field("error", error) - .finish() - } - SpaceRoomListAction::DetailedChildren { space_id, parent_chain, children } => { - f.debug_struct("SpaceRoomListAction::DetailedChildren") - .field("space_id", space_id) - .field("parent_chain", &parent_chain) - .field("num_children", &children.len()) - .finish() - } - SpaceRoomListAction::TopLevelSpaceDetails(space) => { - f.debug_tuple("SpaceRoomListAction::TopLevelSpaceDetails") - .field(space) - .finish() - } - SpaceRoomListAction::LeaveSpaceResult { space_name_id, result } => { - f.debug_struct("SpaceRoomListAction::LeaveSpaceResult") - .field("space_name_id", space_name_id) - .field("result", result) - .finish() - } + SpaceRoomListAction::UpdatedChildren { + space_id, + parent_chain, + direct_child_rooms, + direct_subspaces, + } => f + .debug_struct("SpaceRoomListAction::UpdatedChildren") + .field("space_id", space_id) + .field("parent_chain", &parent_chain) + .field("num_direct_child_rooms", &direct_child_rooms.len()) + .field("num_direct_subspaces", &direct_subspaces.len()) + .finish(), + SpaceRoomListAction::PaginationState { + space_id, + parent_chain, + state, + } => f + .debug_struct("SpaceRoomListAction::PaginationState") + .field("space_id", space_id) + .field("parent_chain", &parent_chain) + .field("state", state) + .finish(), + SpaceRoomListAction::PaginationError { space_id, error } => f + .debug_struct("SpaceRoomListAction::PaginationError") + .field("space_id", space_id) + .field("error", error) + .finish(), + SpaceRoomListAction::DetailedChildren { + space_id, + parent_chain, + children, + } => f + .debug_struct("SpaceRoomListAction::DetailedChildren") + .field("space_id", space_id) + .field("parent_chain", &parent_chain) + .field("num_children", &children.len()) + .finish(), + SpaceRoomListAction::TopLevelSpaceDetails(space) => f + .debug_tuple("SpaceRoomListAction::TopLevelSpaceDetails") + .field(space) + .finish(), + SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result, + } => f + .debug_struct("SpaceRoomListAction::LeaveSpaceResult") + .field("space_name_id", space_name_id) + .field("result", result) + .finish(), } } } diff --git a/src/temp_storage.rs b/src/temp_storage.rs index 9142020c6..37c232c07 100644 --- a/src/temp_storage.rs +++ b/src/temp_storage.rs @@ -1,6 +1,5 @@ use std::{sync::OnceLock, path::PathBuf}; - /// Creates and returns the path to a temp directory for storage. /// /// This is very efficient to call multiple times because the result is cached @@ -16,4 +15,3 @@ pub fn get_temp_dir_path() -> &'static PathBuf { path }) } - diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index f51e8bccb..abb722faf 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -4,7 +4,6 @@ use makepad_widgets::*; use crate::tsp; - script_mod! { link tsp_enabled @@ -249,12 +248,14 @@ enum CreateDidModalState { IdentityCreationError, } - #[derive(Script, ScriptHook, Widget)] pub struct CreateDidModal { - #[deref] view: View, - #[rust] state: CreateDidModalState, - #[rust] is_showing_error: bool, + #[deref] + view: View, + #[rust] + state: CreateDidModalState, + #[rust] + is_showing_error: bool, } impl Widget for CreateDidModal { @@ -275,8 +276,10 @@ impl WidgetMatchEvent for CreateDidModal { // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); - if cancel_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // If the modal was dismissed by clicking outside of it, we MUST NOT emit // a `CreateDidModalAction::Close` action, as that would cause @@ -338,7 +341,7 @@ impl WidgetMatchEvent for CreateDidModal { username: username.to_string(), alias, server, - did_server + did_server, }); self.state = CreateDidModalState::WaitingForIdentityCreation; @@ -360,11 +363,10 @@ impl WidgetMatchEvent for CreateDidModal { needs_redraw = true; } - _ => { } + _ => {} } } - // If the user changes any of the input fields, clear the error message // and reset the accept button to its default state. if self.is_showing_error { @@ -389,7 +391,7 @@ impl WidgetMatchEvent for CreateDidModal { for action in actions { match action.downcast_ref() { - Some(tsp::TspIdentityAction::DidCreationResult(Ok(did)))=> { + Some(tsp::TspIdentityAction::DidCreationResult(Ok(did))) => { self.state = CreateDidModalState::IdentityCreated; self.is_showing_error = false; let message = format!("Successfully created and published DID: \"{}\"", did); @@ -418,7 +420,7 @@ impl WidgetMatchEvent for CreateDidModal { // Upon an error, update the status label and disable the accept button. // Re-enable the input fields so the user can change the input values to try again. - Some(tsp::TspIdentityAction::DidCreationResult(Err(e)))=> { + Some(tsp::TspIdentityAction::DidCreationResult(Err(e))) => { self.state = CreateDidModalState::IdentityCreationError; self.is_showing_error = true; let message = format!("Failed to create DID: {e}"); @@ -437,10 +439,10 @@ impl WidgetMatchEvent for CreateDidModal { needs_redraw = true; } - _ => { } + _ => {} } } - + if needs_redraw { self.view.redraw(cx); } @@ -461,19 +463,29 @@ impl CreateDidModal { accept_button.set_visible(cx, true); cancel_button.set_visible(cx, true); // TODO: return buttons to their default state/appearance - self.view.text_input(cx, ids!(username_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(alias_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(server_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(did_server_input)).set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(username_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(alias_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(server_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(did_server_input)) + .set_is_read_only(cx, false); self.view.label(cx, ids!(status_label)).set_text(cx, ""); self.is_showing_error = false; - self.view.redraw(cx); + self.view.redraw(cx); } } impl CreateDidModalRef { pub fn show(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx); } } diff --git a/src/tsp/create_wallet_modal.rs b/src/tsp/create_wallet_modal.rs index 79c477597..a64e71288 100644 --- a/src/tsp/create_wallet_modal.rs +++ b/src/tsp/create_wallet_modal.rs @@ -4,7 +4,6 @@ use makepad_widgets::*; use crate::tsp::{self, TspWalletMetadata}; - script_mod! { link tsp_enabled @@ -201,12 +200,14 @@ enum CreateWalletModalState { WalletCreationError, } - #[derive(Script, ScriptHook, Widget)] pub struct CreateWalletModal { - #[deref] view: View, - #[rust] state: CreateWalletModalState, - #[rust] is_showing_error: bool, + #[deref] + view: View, + #[rust] + state: CreateWalletModalState, + #[rust] + is_showing_error: bool, } impl Widget for CreateWalletModal { @@ -227,8 +228,10 @@ impl WidgetMatchEvent for CreateWalletModal { // Handle canceling/closing the modal. let cancel_clicked = cancel_button.clicked(actions); - if cancel_clicked || - actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { // If the modal was dismissed by clicking outside of it, we MUST NOT emit // a `CreateWalletModalAction::Close` action, as that would cause @@ -294,7 +297,7 @@ impl WidgetMatchEvent for CreateWalletModal { empty if empty.is_empty() => wallet_file_name_input.empty_text(), non_empty => tsp::sanitize_wallet_name(&non_empty), } - .as_str() + .as_str(), ); let metadata = TspWalletMetadata { wallet_name, @@ -322,11 +325,10 @@ impl WidgetMatchEvent for CreateWalletModal { needs_redraw = true; } - _ => { } + _ => {} } } - // Clear the error message if the user changes any of the input fields. if self.is_showing_error { if wallet_name_input.changed(actions).is_some() @@ -357,11 +359,17 @@ impl WidgetMatchEvent for CreateWalletModal { for action in actions { match action.downcast_ref() { // Handle the wallet creation success action. - Some(tsp::TspWalletAction::CreateWalletSuccess { metadata, is_default }) => { + Some(tsp::TspWalletAction::CreateWalletSuccess { + metadata, + is_default, + }) => { self.state = CreateWalletModalState::WalletCreated; self.is_showing_error = false; let message = if *is_default { - format!("Wallet \"{}\" created successfully and set as the default.", metadata.wallet_name) + format!( + "Wallet \"{}\" created successfully and set as the default.", + metadata.wallet_name + ) } else { format!("Wallet \"{}\" created successfully.", metadata.wallet_name) }; @@ -406,10 +414,10 @@ impl WidgetMatchEvent for CreateWalletModal { confirm_password_input.set_is_read_only(cx, false); } - _ => { } + _ => {} } } - + if needs_redraw { self.view.redraw(cx); } @@ -430,19 +438,29 @@ impl CreateWalletModal { accept_button.set_visible(cx, true); cancel_button.set_visible(cx, true); // TODO: return buttons to their default state/appearance - self.view.text_input(cx, ids!(wallet_name_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(wallet_file_name_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(password_input)).set_is_read_only(cx, false); - self.view.text_input(cx, ids!(confirm_password_input)).set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(wallet_name_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(wallet_file_name_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(password_input)) + .set_is_read_only(cx, false); + self.view + .text_input(cx, ids!(confirm_password_input)) + .set_is_read_only(cx, false); self.view.label(cx, ids!(status_label)).set_text(cx, ""); self.is_showing_error = false; - self.view.redraw(cx); + self.view.redraw(cx); } } impl CreateWalletModalRef { pub fn show(&self, cx: &mut Cx) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx); } } diff --git a/src/tsp/mod.rs b/src/tsp/mod.rs index 17335d889..54c07f418 100644 --- a/src/tsp/mod.rs +++ b/src/tsp/mod.rs @@ -1,4 +1,10 @@ -use std::{borrow::Cow, collections::BTreeMap, ops::Deref, path::Path, sync::{Arc, Mutex, OnceLock}}; +use std::{ + borrow::Cow, + collections::BTreeMap, + ops::Deref, + path::Path, + sync::{Arc, Mutex, OnceLock}, +}; use anyhow::anyhow; use futures_util::StreamExt; @@ -6,12 +12,28 @@ use makepad_widgets::*; use matrix_sdk::ruma::{OwnedUserId, UserId}; use quinn::rustls::crypto::{CryptoProvider, aws_lc_rs}; use serde::{Deserialize, Serialize}; -use tokio::{task::JoinHandle, runtime::Handle, sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}}; -use tsp_sdk::{definitions::{PublicKeyData, PublicVerificationKeyData, VidEncryptionKeyType, VidSignatureKeyType}, vid::{verify_vid, VidError}, AskarSecureStorage, AsyncSecureStore, OwnedVid, ReceivedTspMessage, SecureStorage, VerifiedVid, Vid}; +use tokio::{ + task::JoinHandle, + runtime::Handle, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, +}; +use tsp_sdk::{ + definitions::{ + PublicKeyData, PublicVerificationKeyData, VidEncryptionKeyType, VidSignatureKeyType, + }, + vid::{verify_vid, VidError}, + AskarSecureStorage, AsyncSecureStore, OwnedVid, ReceivedTspMessage, SecureStorage, VerifiedVid, + Vid, +}; use url::Url; -use crate::{persistence::{self, tsp_wallets_dir, SavedTspState}, shared::popup_list::{enqueue_popup_notification, PopupKind}, sliding_sync::current_user_id, tsp::tsp_verification_modal::TspVerificationModalAction, utils::DebugWrapper}; - +use crate::{ + persistence::{self, tsp_wallets_dir, SavedTspState}, + shared::popup_list::{enqueue_popup_notification, PopupKind}, + sliding_sync::current_user_id, + tsp::tsp_verification_modal::TspVerificationModalAction, + utils::DebugWrapper, +}; pub mod create_did_modal; pub mod create_wallet_modal; @@ -68,7 +90,6 @@ struct ReceiveLoopTask { sender: UnboundedSender, } - /// The global singleton TSP state, storing all known TSP wallets. static TSP_STATE: OnceLock> = OnceLock::new(); pub fn tsp_state_ref() -> &'static Mutex { @@ -143,21 +164,29 @@ impl TspState { log!("Restored current local VID {saved_local_vid} from in default wallet."); current_local_vid = Some(saved_local_vid); } else { - warning!("Previously-saved local VID {saved_local_vid} was not found in default wallet."); + warning!( + "Previously-saved local VID {saved_local_vid} was not found in default wallet." + ); enqueue_popup_notification( - format!("Previously-saved local VID \"{saved_local_vid}\" \ + format!( + "Previously-saved local VID \"{saved_local_vid}\" \ was not found in default wallet.\n\n\ - Please select a default wallet and then a new default VID."), - PopupKind::Warning, - None, + Please select a default wallet and then a new default VID." + ), + PopupKind::Warning, + None, ); } } else { - warning!("Found a previously-saved local VID {saved_local_vid}, but not the default wallet that contained it."); + warning!( + "Found a previously-saved local VID {saved_local_vid}, but not the default wallet that contained it." + ); enqueue_popup_notification( - format!("Found a previously-saved local VID \"{saved_local_vid}\", \ + format!( + "Found a previously-saved local VID \"{saved_local_vid}\", \ but not the default wallet that contained it.\n\n\ - Please select or create a default wallet and a new default VID."), + Please select or create a default wallet and a new default VID." + ), PopupKind::Warning, None, ); @@ -185,7 +214,7 @@ impl TspState { pub async fn close_and_serialize(self) -> Result { let mut default_wallet = None; let mut wallets = Vec::::with_capacity( - self.current_wallet.is_some() as usize + self.other_wallets.len() + self.current_wallet.is_some() as usize + self.other_wallets.len(), ); if let Some(current_wallet) = self.current_wallet { @@ -216,12 +245,10 @@ impl TspState { /// Returns the verified VID for a given Matrix user ID, if the association exists /// and the user's associated DID is in the current default wallet. - pub fn get_verified_vid_for( - &self, - user_id: &UserId, - ) -> Option> { + pub fn get_verified_vid_for(&self, user_id: &UserId) -> Option> { let did = self.get_associated_did(user_id)?; - self.current_wallet.as_ref()? + self.current_wallet + .as_ref()? .db .as_store() .get_verified_vid(did) @@ -242,12 +269,17 @@ impl TspState { } let (sender, receiver) = unbounded_channel::(); - let join_handle = rt_handle.spawn( - receive_messages_for_vid(wallet_db.clone(), vid.to_string(), receiver) - ); + let join_handle = rt_handle.spawn(receive_messages_for_vid( + wallet_db.clone(), + vid.to_string(), + receiver, + )); let old = self.receive_loop_tasks.insert( vid.to_string(), - ReceiveLoopTask { join_handle, sender: sender.clone() } + ReceiveLoopTask { + join_handle, + sender: sender.clone(), + }, ); if let Some(old) = old { warning!("BUG: aborting previous receive loop for VID \"{}\".", vid); @@ -257,7 +289,6 @@ impl TspState { } } - /// A TSP wallet entry known to Robrix. Derefs to `TspWalletMetadata`. #[derive(Debug)] pub enum TspWalletEntry { @@ -284,7 +315,6 @@ impl TspWalletEntry { } } - /// A TSP wallet that exists and is currently opened / ready to use. pub struct OpenedTspWallet { pub vault: AskarSecureStorage, @@ -378,7 +408,9 @@ pub fn tsp_init(rt_handle: tokio::runtime::Handle) -> anyhow::Result<()> { // Create a channel to be used between UI thread(s) and the TSP async worker thread. // We do this early on in order to allow TSP init routines to submit requests. let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); - TSP_REQUEST_SENDER.set(sender).expect("BUG: TSP_REQUEST_SENDER already set!"); + TSP_REQUEST_SENDER + .set(sender) + .expect("BUG: TSP_REQUEST_SENDER already set!"); // Start a high-level async task that will start and monitor all other tasks. let _monitor = rt_handle.spawn(async move { @@ -445,7 +477,6 @@ pub fn tsp_init(rt_handle: tokio::runtime::Handle) -> anyhow::Result<()> { Ok(()) } - async fn inner_tsp_init() -> anyhow::Result<()> { // Load the TSP state from persistent storage. let saved_tsp_state = persistence::load_tsp_state().await?; @@ -460,21 +491,17 @@ async fn inner_tsp_init() -> anyhow::Result<()> { } // If there is a private VID and a current wallet, spawn a receive loop // to listen for incoming messages for that private VID. - if let (Some(private_vid), Some(cw)) = - (new_tsp_state.current_local_vid.clone(), new_tsp_state.current_wallet.as_ref()) - { + if let (Some(private_vid), Some(cw)) = ( + new_tsp_state.current_local_vid.clone(), + new_tsp_state.current_wallet.as_ref(), + ) { log!("Starting receive loop for private VID \"{}\".", private_vid); - new_tsp_state.get_or_spawn_receive_loop( - Handle::current(), - &cw.db.clone(), - &private_vid, - ); + new_tsp_state.get_or_spawn_receive_loop(Handle::current(), &cw.db.clone(), &private_vid); } *tsp_state_ref().lock().unwrap() = new_tsp_state; Ok(()) } - /// Actions related to TSP wallets. #[derive(Debug)] pub enum TspWalletAction { @@ -514,10 +541,7 @@ pub enum TspIdentityAction { /// with their Matrix user ID. /// /// This does *NOT* mean that the response has been received yet. - SentDidAssociationRequest { - did: String, - user_id: OwnedUserId, - }, + SentDidAssociationRequest { did: String, user_id: OwnedUserId }, /// An error occurred while sending the request to associate another /// user's DID with their Matrix user ID. ErrorSendingDidAssociationRequest { @@ -548,21 +572,16 @@ pub enum TspIdentityAction { }, } - /// Requests that can be sent to the TSP async worker thread. pub enum TspRequest { /// Request to create a new TSP wallet. - CreateWallet { - metadata: TspWalletMetadata, - }, + CreateWallet { metadata: TspWalletMetadata }, /// Request to open an existing TSP wallet. /// /// This does not modify the current active/default wallet. /// If the wallet exists in the list of other wallets, it will be opened in-place, /// otherwise it will be opened and added to the end of the other wallets list. - OpenWallet { - metadata: TspWalletMetadata, - }, + OpenWallet { metadata: TspWalletMetadata }, /// Request to set an existing open wallet as the default. SetDefaultWallet(TspWalletMetadata), /// Request to remove a TSP wallet from the list without deleting it. @@ -580,18 +599,13 @@ pub enum TspRequest { /// Request to re-publish/re-upload our own DID back up to the DID server. /// /// The given `did` must already exist in the current default wallet. - RepublishDid { - did: String, - }, + RepublishDid { did: String }, /// Request to associate another user's identity (DID) with their Matrix User ID. /// /// This will verify the DID and store it in the current default wallet /// (using their Matrix User ID as the alias for that new verified ID), /// and then send a verification/relationship request to that new verified ID. - AssociateDidWithUserId { - did: String, - user_id: OwnedUserId, - }, + AssociateDidWithUserId { did: String, user_id: OwnedUserId }, /// Request to respond to a previously-received `DidAssociationRequest`. RespondToDidAssociationRequest { details: TspVerificationDetails, @@ -603,14 +617,12 @@ pub enum TspRequest { // CancelAssociateDidRequest(TspVerificationDetails), } - fn create_reqwest_client() -> reqwest::Result { reqwest::ClientBuilder::new() .user_agent(format!("Robrix v{}", env!("CARGO_PKG_VERSION"))) .build() } - /// The entry point for an async worker thread that processes TSP-related async tasks. /// /// All this task does is wait for [`TspRequests`] from other threads @@ -623,218 +635,266 @@ async fn async_tsp_worker( // Allow lazy initialization of the reqwest client. let mut __reqwest_client = None; let mut get_reqwest_client = || { - __reqwest_client.get_or_insert_with(|| create_reqwest_client().unwrap()).clone() + __reqwest_client + .get_or_insert_with(|| create_reqwest_client().unwrap()) + .clone() }; - while let Some(req) = request_receiver.recv().await { match req { - TspRequest::CreateWallet { metadata } => { - log!("Received TspRequest::CreateWallet({metadata:?})"); - Handle::current().spawn(async move { - if let Some(sqlite_path) = metadata.url.get_path() { - if let Ok(true) = tokio::fs::try_exists(sqlite_path).await { - error!("Wallet already exists at path: {}", sqlite_path.display()); - Cx::post_action(TspWalletAction::CreateWalletError { - metadata: metadata.clone(), - error: anyhow!("Wallet already exists at path: {}", sqlite_path.display()), - }); - return; - } - if let Some(parent_dir) = sqlite_path.parent() { - log!("Ensuring that new wallet's parent dir exists: {}", parent_dir.display()); - if let Err(e) = tokio::fs::create_dir_all(parent_dir).await { - error!("Failed to create directory to hold new wallet: {e:?}"); + while let Some(req) = request_receiver.recv().await { + match req { + TspRequest::CreateWallet { metadata } => { + log!("Received TspRequest::CreateWallet({metadata:?})"); + Handle::current().spawn(async move { + if let Some(sqlite_path) = metadata.url.get_path() { + if let Ok(true) = tokio::fs::try_exists(sqlite_path).await { + error!("Wallet already exists at path: {}", sqlite_path.display()); Cx::post_action(TspWalletAction::CreateWalletError { metadata: metadata.clone(), - error: anyhow!("Failed to create directory for new wallet: {}, error: {}", parent_dir.display(), e), + error: anyhow!( + "Wallet already exists at path: {}", + sqlite_path.display() + ), }); return; } - } - } - let encoded_url = metadata.url.to_url_encoded(); - log!("Attempting to create new wallet at:\n Reg: {}\n Enc: {}", metadata.url, encoded_url); - match AskarSecureStorage::new(&encoded_url, metadata.password.as_bytes()).await { - Ok(vault) => { - log!("Successfully created new wallet: {metadata:?}"); - let db = AsyncSecureStore::new(); - let mut tsp_state = tsp_state_ref().lock().unwrap(); - let opened_wallet = OpenedTspWallet { - vault, - db, - metadata: metadata.clone(), - }; - let is_default: bool; - if tsp_state.current_wallet.is_none() { - tsp_state.current_wallet = Some(opened_wallet); - is_default = true; - } else { - tsp_state.other_wallets.push(TspWalletEntry::Opened(opened_wallet)); - is_default = false; + if let Some(parent_dir) = sqlite_path.parent() { + log!( + "Ensuring that new wallet's parent dir exists: {}", + parent_dir.display() + ); + if let Err(e) = tokio::fs::create_dir_all(parent_dir).await { + error!("Failed to create directory to hold new wallet: {e:?}"); + Cx::post_action(TspWalletAction::CreateWalletError { + metadata: metadata.clone(), + error: anyhow!( + "Failed to create directory for new wallet: {}, error: {}", + parent_dir.display(), + e + ), + }); + return; + } } - Cx::post_action( - TspWalletAction::CreateWalletSuccess { + } + let encoded_url = metadata.url.to_url_encoded(); + log!( + "Attempting to create new wallet at:\n Reg: {}\n Enc: {}", + metadata.url, + encoded_url + ); + match AskarSecureStorage::new(&encoded_url, metadata.password.as_bytes()).await + { + Ok(vault) => { + log!("Successfully created new wallet: {metadata:?}"); + let db = AsyncSecureStore::new(); + let mut tsp_state = tsp_state_ref().lock().unwrap(); + let opened_wallet = OpenedTspWallet { + vault, + db, + metadata: metadata.clone(), + }; + let is_default: bool; + if tsp_state.current_wallet.is_none() { + tsp_state.current_wallet = Some(opened_wallet); + is_default = true; + } else { + tsp_state + .other_wallets + .push(TspWalletEntry::Opened(opened_wallet)); + is_default = false; + } + Cx::post_action(TspWalletAction::CreateWalletSuccess { metadata, is_default, - } - ); - } - Err(error) => { - error!("Failed to create new wallet: {error:?}"); - Cx::post_action( - TspWalletAction::CreateWalletError { + }); + } + Err(error) => { + error!("Failed to create new wallet: {error:?}"); + Cx::post_action(TspWalletAction::CreateWalletError { metadata: metadata.clone(), error: error.into(), - } - ); + }); + } } - } - }); - } - - TspRequest::SetDefaultWallet(metadata) => { - log!("Received TspRequest::SetDefaultWallet({metadata:?})"); - match tsp_state_ref().lock().unwrap().current_wallet.as_ref() { - Some(cw) if cw.metadata == metadata => { - log!("Wallet was already set as default: {metadata:?}"); - continue; - } - _ => {} + }); } - // If the new default wallet exists and is already opened, set it as default. - Handle::current().spawn(async move { - let mut result = Err(()); - let mut tsp_state = tsp_state_ref().lock().unwrap(); - if let Some(TspWalletEntry::Opened(opened)) = tsp_state.other_wallets.iter() - .position(|w| match w { - TspWalletEntry::Opened(opened) => opened.metadata == metadata, - _ => false, - }) - .map(|idx| tsp_state.other_wallets.remove(idx)) - { - let prev_opt = tsp_state.current_wallet.replace(opened); - if let Some(previous_active) = prev_opt { - tsp_state.other_wallets.insert(0, TspWalletEntry::Opened(previous_active)); + TspRequest::SetDefaultWallet(metadata) => { + log!("Received TspRequest::SetDefaultWallet({metadata:?})"); + match tsp_state_ref().lock().unwrap().current_wallet.as_ref() { + Some(cw) if cw.metadata == metadata => { + log!("Wallet was already set as default: {metadata:?}"); + continue; } - result = Ok(metadata); + _ => {} } - Cx::post_action(TspWalletAction::DefaultWalletChanged(result)); - }); - } - TspRequest::OpenWallet { metadata } => { - log!("Received TspRequest::OpenWallet({metadata:?})"); - Handle::current().spawn(async move { - let result = match metadata.open_wallet().await { - Ok(opened_wallet) => { - log!("Successfully opened wallet: {metadata:?}"); - let mut tsp_state = tsp_state_ref().lock().unwrap(); - // If the newly-opened wallet exists in the other wallets list, - // convert it into an opened wallet in-place. - // Otherwise, add it to the end of the other wallet list - if let Some(w) = tsp_state.other_wallets.iter_mut().find(|w| w.metadata() == &metadata) { - *w = TspWalletEntry::Opened(opened_wallet); - } else { - tsp_state.other_wallets.push(TspWalletEntry::Opened(opened_wallet)); + // If the new default wallet exists and is already opened, set it as default. + Handle::current().spawn(async move { + let mut result = Err(()); + let mut tsp_state = tsp_state_ref().lock().unwrap(); + if let Some(TspWalletEntry::Opened(opened)) = tsp_state + .other_wallets + .iter() + .position(|w| match w { + TspWalletEntry::Opened(opened) => opened.metadata == metadata, + _ => false, + }) + .map(|idx| tsp_state.other_wallets.remove(idx)) + { + let prev_opt = tsp_state.current_wallet.replace(opened); + if let Some(previous_active) = prev_opt { + tsp_state + .other_wallets + .insert(0, TspWalletEntry::Opened(previous_active)); } - Ok(metadata) - } - Err(error) => { - error!("Error opening wallet {metadata:?}: {error:?}"); - Err(error) + result = Ok(metadata); } - }; - Cx::post_action(TspWalletAction::WalletOpened(result)); - }); - } + Cx::post_action(TspWalletAction::DefaultWalletChanged(result)); + }); + } - TspRequest::RemoveWallet(metadata) => { - log!("Received TspRequest::RemoveWallet({metadata:?})"); - Handle::current().spawn(async move { - let mut tsp_state = tsp_state_ref().lock().unwrap(); - let was_default = if tsp_state.current_wallet.as_ref().is_some_and(|cw| cw.metadata == metadata) { - tsp_state.current_wallet = None; - true - } - else if let Some(i) = tsp_state.other_wallets.iter().position(|w| w.metadata() == &metadata) { - tsp_state.other_wallets.remove(i); - false - } else { - error!("BUG: failed to remove wallet not found in TSP state: {metadata:?}"); - return; - }; - Cx::post_action(TspWalletAction::WalletRemoved { metadata, was_default }); - }); - } + TspRequest::OpenWallet { metadata } => { + log!("Received TspRequest::OpenWallet({metadata:?})"); + Handle::current().spawn(async move { + let result = match metadata.open_wallet().await { + Ok(opened_wallet) => { + log!("Successfully opened wallet: {metadata:?}"); + let mut tsp_state = tsp_state_ref().lock().unwrap(); + // If the newly-opened wallet exists in the other wallets list, + // convert it into an opened wallet in-place. + // Otherwise, add it to the end of the other wallet list + if let Some(w) = tsp_state + .other_wallets + .iter_mut() + .find(|w| w.metadata() == &metadata) + { + *w = TspWalletEntry::Opened(opened_wallet); + } else { + tsp_state + .other_wallets + .push(TspWalletEntry::Opened(opened_wallet)); + } + Ok(metadata) + } + Err(error) => { + error!("Error opening wallet {metadata:?}: {error:?}"); + Err(error) + } + }; + Cx::post_action(TspWalletAction::WalletOpened(result)); + }); + } - TspRequest::DeleteWallet(metadata) => { - log!("Received TspRequest::DeleteWallet({metadata:?})"); - todo!("handle deleting a wallet"); - } + TspRequest::RemoveWallet(metadata) => { + log!("Received TspRequest::RemoveWallet({metadata:?})"); + Handle::current().spawn(async move { + let mut tsp_state = tsp_state_ref().lock().unwrap(); + let was_default = if tsp_state + .current_wallet + .as_ref() + .is_some_and(|cw| cw.metadata == metadata) + { + tsp_state.current_wallet = None; + true + } else if let Some(i) = tsp_state + .other_wallets + .iter() + .position(|w| w.metadata() == &metadata) + { + tsp_state.other_wallets.remove(i); + false + } else { + error!("BUG: failed to remove wallet not found in TSP state: {metadata:?}"); + return; + }; + Cx::post_action(TspWalletAction::WalletRemoved { + metadata, + was_default, + }); + }); + } - TspRequest::CreateDid { username, alias, server, did_server } => { - log!("Received TspRequest::CreateDid(username: {username}, alias: {alias:?}, server: {server}, did_server: {did_server})"); - let client = get_reqwest_client(); - - Handle::current().spawn(async move { - let result = create_did_and_add_to_wallet( - &client, - username, - alias, - server, - did_server, - ).await; - Cx::post_action(TspIdentityAction::DidCreationResult(result)); - }); - } + TspRequest::DeleteWallet(metadata) => { + log!("Received TspRequest::DeleteWallet({metadata:?})"); + todo!("handle deleting a wallet"); + } - TspRequest::RepublishDid { did } => { - log!("Received TspRequest::RepublishDid(did: {did})"); - let client = get_reqwest_client(); + TspRequest::CreateDid { + username, + alias, + server, + did_server, + } => { + log!( + "Received TspRequest::CreateDid(username: {username}, alias: {alias:?}, server: {server}, did_server: {did_server})" + ); + let client = get_reqwest_client(); - Handle::current().spawn(async move { - let result = republish_did(&did, &client).await - .map(|_| did); - Cx::post_action(TspIdentityAction::DidRepublishResult(result)); - }); - } + Handle::current().spawn(async move { + let result = + create_did_and_add_to_wallet(&client, username, alias, server, did_server) + .await; + Cx::post_action(TspIdentityAction::DidCreationResult(result)); + }); + } - TspRequest::AssociateDidWithUserId { did, user_id } => { - log!("Received TspRequest::AssociateDidWithUserId(did: {did}, user_id: {user_id})"); - Handle::current().spawn(async move { - let action = match associate_did_with_user_id(&did, &user_id).await { - Ok(_) => TspIdentityAction::SentDidAssociationRequest { did, user_id }, - Err(error) => TspIdentityAction::ErrorSendingDidAssociationRequest { did, user_id, error }, - }; - Cx::post_action(action); - }); - } + TspRequest::RepublishDid { did } => { + log!("Received TspRequest::RepublishDid(did: {did})"); + let client = get_reqwest_client(); - TspRequest::RespondToDidAssociationRequest { details, wallet_db, accepted } => { - log!("Received TspRequest::RespondToDidAssociationRequest(details: {details:?}, accepted: {accepted})"); - Handle::current().spawn(async move { - let result = respond_to_did_association_request(&details, &wallet_db, accepted).await; - // If all was successful, add this new association to the TSP state. - if result.is_ok() { - tsp_state_ref().lock().unwrap().associations.insert( - details.initiating_user_id.clone(), - details.initiating_vid.clone(), - ); - } - Cx::post_action(TspVerificationModalAction::SentDidAssociationResponse { - details, - result, + Handle::current().spawn(async move { + let result = republish_did(&did, &client).await.map(|_| did); + Cx::post_action(TspIdentityAction::DidRepublishResult(result)); + }); + } + + TspRequest::AssociateDidWithUserId { did, user_id } => { + log!("Received TspRequest::AssociateDidWithUserId(did: {did}, user_id: {user_id})"); + Handle::current().spawn(async move { + let action = match associate_did_with_user_id(&did, &user_id).await { + Ok(_) => TspIdentityAction::SentDidAssociationRequest { did, user_id }, + Err(error) => TspIdentityAction::ErrorSendingDidAssociationRequest { + did, + user_id, + error, + }, + }; + Cx::post_action(action); + }); + } + + TspRequest::RespondToDidAssociationRequest { + details, + wallet_db, + accepted, + } => { + log!( + "Received TspRequest::RespondToDidAssociationRequest(details: {details:?}, accepted: {accepted})" + ); + Handle::current().spawn(async move { + let result = + respond_to_did_association_request(&details, &wallet_db, accepted).await; + // If all was successful, add this new association to the TSP state. + if result.is_ok() { + tsp_state_ref().lock().unwrap().associations.insert( + details.initiating_user_id.clone(), + details.initiating_vid.clone(), + ); + } + Cx::post_action(TspVerificationModalAction::SentDidAssociationResponse { + details, + result, + }); }); - }); + } } } -} error!("async_tsp_worker task ended unexpectedly"); anyhow::bail!("async_tsp_worker task ended unexpectedly") } - /// Creates & publishes a new DID, adds it to the default wallet, /// and sets the new private VID to be default if none exists. /// @@ -846,13 +906,18 @@ async fn create_did_and_add_to_wallet( server: String, did_server: String, ) -> Result { - let cw_db = tsp_state_ref().lock().unwrap() - .current_wallet.as_ref() + let cw_db = tsp_state_ref() + .lock() + .unwrap() + .current_wallet + .as_ref() .map(|w| w.db.clone()) .ok_or_else(|| anyhow!("Please choose a default TSP wallet to hold the DID."))?; - let (did, private_vid, metadata) = create_did_web(&did_server, &server, &username, client).await?; + let (did, private_vid, metadata) = + create_did_web(&did_server, &server, &username, client).await?; let new_vid = private_vid.identifier().to_string(); - log!("Successfully created & published new DID: {did}.\n\ + log!( + "Successfully created & published new DID: {did}.\n\ Adding private VID {new_vid} to current wallet...", ); let did = store_did_in_wallet(&cw_db, private_vid, metadata, alias, did)?; @@ -863,13 +928,13 @@ async fn create_did_and_add_to_wallet( // and start a receive loop to listen for incoming requests for it. let mut tsp_state = tsp_state_ref().lock().unwrap(); if tsp_state.current_local_vid.is_none() { - log!("Setting new VID \"{}\" (from DID \"{}\") as current local VID and starting receive loop...", new_vid, did); - tsp_state.current_local_vid = Some(new_vid.clone()); - tsp_state.get_or_spawn_receive_loop( - Handle::current(), - &cw_db, - &new_vid, + log!( + "Setting new VID \"{}\" (from DID \"{}\") as current local VID and starting receive loop...", + new_vid, + did ); + tsp_state.current_local_vid = Some(new_vid.clone()); + tsp_state.get_or_spawn_receive_loop(Handle::current(), &cw_db, &new_vid); if let Some(user_id) = current_user_id() { tsp_state.associations .entry(user_id.clone()) @@ -902,12 +967,12 @@ async fn create_did_web( username, ); - let transport = Url::parse( - &format!("https://{}/endpoint/{}", - server, - &did.replace("%", "%25") - ) - ).map_err(|e| anyhow!("Invalid transport URL: {e}"))?; + let transport = Url::parse(&format!( + "https://{}/endpoint/{}", + server, + &did.replace("%", "%25") + )) + .map_err(|e| anyhow!("Invalid transport URL: {e}"))?; let private_vid = OwnedVid::bind(&did, transport); log!("created identity {}", private_vid.identifier()); @@ -921,12 +986,15 @@ async fn create_did_web( .map_err(|e| anyhow!("Could not publish VID. The DID server responded with error: {e}"))?; let vid_result: Result = match response.status() { - r if r.is_success() => { - response.json().await - .map_err(|e| anyhow!("Could not decode response from DID server as a valid VID: {e}")) - } + r if r.is_success() => response + .json() + .await + .map_err(|e| anyhow!("Could not decode response from DID server as a valid VID: {e}")), r => { - let text = response.text().await.unwrap_or_else(|_| "[Unknown]".to_string()); + let text = response + .text() + .await + .unwrap_or_else(|_| "[Unknown]".to_string()); if r.as_u16() == 500 { return Err(anyhow!( "The DID server returned error code 500. The DID username may already exist, \ @@ -943,7 +1011,8 @@ async fn create_did_web( let _vid = vid_result?; - log!("published DID document at {}", + log!( + "published DID document at {}", tsp_sdk::vid::did::get_resolve_url(&did)?.to_string() ); @@ -954,7 +1023,6 @@ async fn create_did_web( Ok((did, private_vid, metadata)) } - /// Stores the given private VID in the current default TSP wallet, /// and optionally establishes an alias for the given `did`. /// @@ -974,13 +1042,8 @@ fn store_did_in_wallet( Ok(did) } - /// Re-publishes/re-uploads our own DID to the DID server it was originally created on. -async fn republish_did( - did: &str, - client: &reqwest::Client, -) -> Result<(), anyhow::Error> { - +async fn republish_did(did: &str, client: &reqwest::Client) -> Result<(), anyhow::Error> { /// A copy of the Vid struct that we can actually instantiate /// from an existing VID in a local wallet. /// @@ -999,15 +1062,20 @@ async fn republish_did( public_enckey: PublicKeyData, } - let our_vid = { let tsp_state = tsp_state_ref().lock().unwrap(); - tsp_state.current_wallet.as_ref() + tsp_state + .current_wallet + .as_ref() .ok_or_else(no_default_wallet_error)? .db .as_store() .get_verified_vid(did) - .map_err(|_e| anyhow!("The DID to republish \"{did}\" was not found in the current default wallet."))? + .map_err(|_e| { + anyhow!( + "The DID to republish \"{did}\" was not found in the current default wallet." + ) + })? }; let vid_dup = VidDuplicate { @@ -1022,11 +1090,16 @@ async fn republish_did( let did_transport_url = tsp_sdk::vid::did::get_resolve_url(did)?; let response = client - .post(format!("{}/add-vid", did_transport_url.origin().ascii_serialization())) + .post(format!( + "{}/add-vid", + did_transport_url.origin().ascii_serialization() + )) .json(&vid_dup) .send() .await - .map_err(|e| anyhow!("Could not republish VID. The DID server responded with error: {e}"))?; + .map_err(|e| { + anyhow!("Could not republish VID. The DID server responded with error: {e}") + })?; match response.status() { r if r.is_success() => { @@ -1034,7 +1107,10 @@ async fn republish_did( Ok(()) } r => { - let text = response.text().await.unwrap_or_else(|_| "[Unknown]".to_string()); + let text = response + .text() + .await + .unwrap_or_else(|_| "[Unknown]".to_string()); if r.as_u16() == 500 { Err(anyhow!( "The DID server returned error code 500. The DID username may already exist, \ @@ -1050,14 +1126,16 @@ async fn republish_did( } } - async fn receive_messages_for_vid( wallet_db: AsyncSecureStore, private_vid_to_receive_on: String, mut request_rx: UnboundedReceiver, ) -> Result<(), anyhow::Error> { // Ensure that our receiving VID is currently published to the DID server. - if republish_did(&private_vid_to_receive_on, &create_reqwest_client()?).await.is_ok() { + if republish_did(&private_vid_to_receive_on, &create_reqwest_client()?) + .await + .is_ok() + { log!("Auto-republished DID \"{private_vid_to_receive_on}\" to its DID server."); } @@ -1135,32 +1213,36 @@ async fn receive_messages_for_vid( Ok(()) } - fn no_default_wallet_error() -> anyhow::Error { anyhow!("Please choose a default TSP wallet.") } fn no_default_vid_error() -> anyhow::Error { - anyhow!("Please choose a default VID from your default \ - TSP wallet to represent your own Matrix account.") + anyhow!( + "Please choose a default VID from your default \ + TSP wallet to represent your own Matrix account." + ) } - /// Associates the given DID with a Matrix User ID. /// /// This function only performs the local verification of the given DID into /// the local default wallet, and then sends a verification request to the user. /// It does not wait to receive a verification response. -async fn associate_did_with_user_id( - did: &str, - user_id: &OwnedUserId, -) -> Result<(), anyhow::Error> { - let our_user_id = crate::sliding_sync::current_user_id() - .ok_or_else(|| anyhow!("Must be logged into Matrix in order to associate a DID with a Matrix User ID."))?; +async fn associate_did_with_user_id(did: &str, user_id: &OwnedUserId) -> Result<(), anyhow::Error> { + let our_user_id = crate::sliding_sync::current_user_id().ok_or_else(|| { + anyhow!("Must be logged into Matrix in order to associate a DID with a Matrix User ID.") + })?; let (wallet_db, our_vid) = { let tsp_state = tsp_state_ref().lock().unwrap(); - let wallet = tsp_state.current_wallet.as_ref().ok_or_else(no_default_wallet_error)?; - let our_vid = tsp_state.current_local_vid.clone().ok_or_else(no_default_vid_error)?; + let wallet = tsp_state + .current_wallet + .as_ref() + .ok_or_else(no_default_wallet_error)?; + let our_vid = tsp_state + .current_local_vid + .clone() + .ok_or_else(no_default_vid_error)?; (wallet.db.clone(), our_vid) }; if !wallet_db.has_verified_vid(did)? { @@ -1182,15 +1264,21 @@ async fn associate_did_with_user_id( .collect() }, }; - tsp_state_ref().lock().unwrap().pending_verification_requests.push(verification_details.clone()); + tsp_state_ref() + .lock() + .unwrap() + .pending_verification_requests + .push(verification_details.clone()); let request_msg = TspMessage::VerificationRequest(verification_details); - wallet_db.send( - &our_vid, - did, - // This is just for debugging and should be removed before production. - Some(format!("Verification from {our_user_id} to {user_id}").as_bytes()), - serde_json::to_string(&request_msg)?.as_bytes(), - ).await?; + wallet_db + .send( + &our_vid, + did, + // This is just for debugging and should be removed before production. + Some(format!("Verification from {our_user_id} to {user_id}").as_bytes()), + serde_json::to_string(&request_msg)?.as_bytes(), + ) + .await?; // Note: the receive loop will wait to receive the verification response, // upon which the verification procedure will be completed @@ -1198,27 +1286,42 @@ async fn associate_did_with_user_id( Ok(()) } - /// Sends a positive/negative response to a previous incoming DID association request. async fn respond_to_did_association_request( details: &TspVerificationDetails, wallet_db: &AsyncSecureStore, accepted: bool, ) -> Result<(), anyhow::Error> { - wallet_db.verify_vid(&details.initiating_vid, Some(details.initiating_user_id.to_string())).await?; - log!("Verification requester's initiating DID {} was verified and added to your wallet.", details.initiating_vid); + wallet_db + .verify_vid( + &details.initiating_vid, + Some(details.initiating_user_id.to_string()), + ) + .await?; + log!( + "Verification requester's initiating DID {} was verified and added to your wallet.", + details.initiating_vid + ); let response_msg = TspMessage::VerificationResponse { details: details.clone(), accepted, }; - wallet_db.send( - &details.responding_vid, - &details.initiating_vid, - // This is just for debugging and should be removed before production. - Some(format!("Verification Response ({accepted}) from {} to {}", details.responding_user_id, details.initiating_user_id).as_bytes()), - serde_json::to_string(&response_msg)?.as_bytes(), - ).await?; + wallet_db + .send( + &details.responding_vid, + &details.initiating_vid, + // This is just for debugging and should be removed before production. + Some( + format!( + "Verification Response ({accepted}) from {} to {}", + details.responding_user_id, details.initiating_user_id + ) + .as_bytes(), + ), + serde_json::to_string(&response_msg)?.as_bytes(), + ) + .await?; Ok(()) } @@ -1228,15 +1331,22 @@ pub fn sign_anycast_with_default_vid(message: &[u8]) -> Result, anyhow:: let (wallet_db, signing_vid) = { let tsp_state = tsp_state_ref().lock().unwrap(); ( - tsp_state.current_wallet.as_ref().ok_or_else(no_default_wallet_error)?.db.clone(), - tsp_state.current_local_vid.clone().ok_or_else(no_default_vid_error)?, + tsp_state + .current_wallet + .as_ref() + .ok_or_else(no_default_wallet_error)? + .db + .clone(), + tsp_state + .current_local_vid + .clone() + .ok_or_else(no_default_vid_error)?, ) }; let signed = wallet_db.as_store().sign_anycast(&signing_vid, message)?; Ok(signed) } - /// The types/schema of messages that we send over the TSP protocol. #[derive(Debug, Serialize, Deserialize)] enum TspMessage { @@ -1269,11 +1379,9 @@ pub struct TspVerificationDetails { /// Sanitizes a wallet name to ensure it is safe to use in file paths. pub fn sanitize_wallet_name(name: &str) -> String { - sanitize_filename::sanitize(name) - .replace(char::is_whitespace, "_") + sanitize_filename::sanitize(name).replace(char::is_whitespace, "_") } - /// Represents a SQLite URL for a TSP wallet, which is *NOT* percent-encoded yet. /// /// Currently the scheme is always "sqlite://" (or "sqlite:///" for absolute paths), @@ -1308,14 +1416,15 @@ impl TspWalletSqliteUrl { pub fn get_path(&self) -> Option<&Path> { let url = &self.0; // Handle URLs with a scheme for absolute paths, e.g., "sqlite:///" - if let Some(p) = url.find(":///").and_then(|pos| url.get(pos + 4 ..)) { + if let Some(p) = url.find(":///").and_then(|pos| url.get(pos + 4..)) { Some(Path::new(p)) } // Handle URLs with a scheme for relative paths, e.g., "sqlite://" - else if let Some(p) = url.find("://").and_then(|pos| url.get(pos + 3 ..)) { + else if let Some(p) = url.find("://").and_then(|pos| url.get(pos + 3..)) { Some(Path::new(p)) + } else { + None } - else { None } } /// Returns the URL as a string that is not percent-encoded. @@ -1327,7 +1436,6 @@ impl TspWalletSqliteUrl { &self.0 } - /// Converts this wallet URL to a percent-encoded URL. /// /// ## Usage notes @@ -1338,7 +1446,7 @@ impl TspWalletSqliteUrl { /// We cannot use the `Path`/`PathBuf` type because the sqlite backend /// always expects URLs with filename paths encoded using Unix-style `/` path separators, /// even on Windows. Therefore, we manually percent-encode each part of the path - /// and push them in between manually-added `/` separators, instead of using + /// and push them in between manually-added `/` separators, instead of using /// the Rust `std::path` functions like `Path::join()` or `PathBuf::push()`. pub fn to_url_encoded(&self) -> Cow<'_, str> { const DELIMITER_ABS: &str = ":///"; @@ -1351,14 +1459,19 @@ impl TspWalletSqliteUrl { let try_encode = |delim: &str| -> Option { if let Some(idx) = self.0.find(delim) { - let before = self.0.get(.. (idx + delim.len())).unwrap_or(""); - let after = self.0.get((idx + delim.len()) ..).unwrap_or(""); + let before = self.0.get(..(idx + delim.len())).unwrap_or(""); + let after = self.0.get((idx + delim.len())..).unwrap_or(""); let mut after_encoded = String::new(); for component in Path::new(after).components() { match component { std::path::Component::Prefix(prefix) => { // Windows drive prefixes must not be percent-encoded. - after_encoded = format!("{}{}{}", after_encoded, SEPARATOR, prefix.as_os_str().to_string_lossy()); + after_encoded = format!( + "{}{}{}", + after_encoded, + SEPARATOR, + prefix.as_os_str().to_string_lossy() + ); } std::path::Component::RootDir => { // ignore, since we already manually add '/' between components. @@ -1366,12 +1479,18 @@ impl TspWalletSqliteUrl { std::path::Component::Normal(p) => { let percent_encoded = percent_encoding::percent_encode( p.as_encoded_bytes(), - percent_encoding::NON_ALPHANUMERIC + percent_encoding::NON_ALPHANUMERIC, ); - after_encoded = format!("{}{}{}", after_encoded, SEPARATOR_PE, percent_encoded); + after_encoded = + format!("{}{}{}", after_encoded, SEPARATOR_PE, percent_encoded); } other => { - after_encoded = format!("{}{}{}", after_encoded, SEPARATOR_PE, other.as_os_str().to_string_lossy()); + after_encoded = format!( + "{}{}{}", + after_encoded, + SEPARATOR_PE, + other.as_os_str().to_string_lossy() + ); } } } @@ -1385,10 +1504,8 @@ impl TspWalletSqliteUrl { .or_else(|| try_encode(DELIMITER_REG)) .map(Cow::from) .unwrap_or_else(|| { - percent_encoding::utf8_percent_encode( - &self.0, - percent_encoding::NON_ALPHANUMERIC, - ).into() + percent_encoding::utf8_percent_encode(&self.0, percent_encoding::NON_ALPHANUMERIC) + .into() }) } } diff --git a/src/tsp/tsp_settings_screen.rs b/src/tsp/tsp_settings_screen.rs index 83d0e6f87..822ebd6ea 100644 --- a/src/tsp/tsp_settings_screen.rs +++ b/src/tsp/tsp_settings_screen.rs @@ -1,7 +1,16 @@ - 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}}; +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"; @@ -164,16 +173,19 @@ impl WalletState { fn get(&self, index: usize) -> Option<(&TspWalletMetadata, WalletStatusAndDefault)> { if let Some(active) = self.active_wallet.as_ref() { if index == 0 { - Some((active, WalletStatusAndDefault::new(WalletStatus::Opened, true))) + Some(( + active, + WalletStatusAndDefault::new(WalletStatus::Opened, true), + )) } else { - self.other_wallets.get(index - 1).map(|(m, s)| - (m, WalletStatusAndDefault::new(*s, false)) - ) + self.other_wallets + .get(index - 1) + .map(|(m, s)| (m, WalletStatusAndDefault::new(*s, false))) } } else { - self.other_wallets.get(index).map(|(m, s)| - (m, WalletStatusAndDefault::new(*s, false)) - ) + self.other_wallets + .get(index) + .map(|(m, s)| (m, WalletStatusAndDefault::new(*s, false))) } } } @@ -198,7 +210,8 @@ impl WalletStatusAndDefault { /// The view containing all TSP-related settings. #[derive(Script, ScriptHook, Widget)] pub struct TspSettingsScreen { - #[deref] view: View, + #[deref] + view: View, /// The list of wallets that are known by this widget. /// @@ -210,7 +223,8 @@ pub struct TspSettingsScreen { /// This is sort of a "cache" of the wallets that have been drawn /// to avoid having to re-fetch them from the shared TSP state every time, /// as that requires locking the mutex and can be expensive. - #[rust] wallets: Option, + #[rust] + wallets: Option, } impl Widget for TspSettingsScreen { @@ -227,36 +241,49 @@ impl Widget for TspSettingsScreen { } // Draw the current identity label and republish button based on the active identity. - let (current_did_text, current_did_text_color, show_republish_button) = match - self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) + let (current_did_text, current_did_text_color, show_republish_button) = match self + .wallets + .as_ref() + .and_then(|ws| ws.active_identity.as_deref()) { Some(current_did) => (current_did.to_string(), COLOR_FG_ACCEPT_GREEN, true), - None => ("No default identity has been set.".to_string(), COLOR_TEXT_WARNING_NOT_FOUND, false), + None => ( + "No default identity has been set.".to_string(), + COLOR_TEXT_WARNING_NOT_FOUND, + false, + ), }; let mut current_identity_label = self.view.label(cx, ids!(current_identity_label)); script_apply_eval!(cx, current_identity_label, { text: #(current_did_text), draw_text +: { color: #(current_did_text_color) }, }); - self.view.button(cx, ids!(republish_identity_button)).set_visible(cx, show_republish_button); - + self.view + .button(cx, ids!(republish_identity_button)) + .set_visible(cx, show_republish_button); // If we don't have any wallets, show the "no wallets" label. let is_wallets_empty = self.wallets.as_ref().is_none_or(|w| w.is_empty()); - self.view.view(cx, ids!(no_wallets_label)).set_visible(cx, is_wallets_empty); + self.view + .view(cx, ids!(no_wallets_label)) + .set_visible(cx, is_wallets_empty); while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // Here, we only need to handle drawing the wallet list. let flat_list_ref = subview.as_flat_list(); let Some(mut list) = flat_list_ref.borrow_mut() else { - error!("!!! TspSettingsScreen::draw_walk(): BUG: expected a FlatList widget, but got something else"); + error!( + "!!! TspSettingsScreen::draw_walk(): BUG: expected a FlatList widget, but got something else" + ); continue; }; let Some(wallets) = self.wallets.as_ref() else { return DrawStep::done(); }; - for (metadata, mut status_and_default) in (0..wallets.len()).filter_map(|i| wallets.get(i)) { + for (metadata, mut status_and_default) in + (0..wallets.len()).filter_map(|i| wallets.get(i)) + { let item_live_id = LiveId::from_str(metadata.url.as_url_unencoded()); let item = list.item(cx, item_live_id, id!(wallet_entry)).unwrap(); // Pass the wallet metadata in through Scope via props, @@ -276,27 +303,39 @@ impl MatchEvent for TspSettingsScreen { for action in actions { match action.downcast_ref() { // Add the new wallet to the list of drawn wallets. - Some(TspWalletAction::CreateWalletSuccess { metadata, is_default }) => { + Some(TspWalletAction::CreateWalletSuccess { + metadata, + is_default, + }) => { let wallets = self.wallets.get_or_insert_default(); if *is_default { wallets.active_wallet = Some(metadata.clone()); } else { - wallets.other_wallets.push((metadata.clone(), WalletStatus::Opened)); + wallets + .other_wallets + .push((metadata.clone(), WalletStatus::Opened)); } self.view.redraw(cx); continue; } // Remove the wallet from the list of drawn wallets. - Some(TspWalletAction::WalletRemoved { metadata, was_default }) => { - let Some(wallets) = &mut self.wallets.as_mut() else { continue }; + Some(TspWalletAction::WalletRemoved { + metadata, + was_default, + }) => { + let Some(wallets) = &mut self.wallets.as_mut() else { + continue; + }; if *was_default { wallets.active_wallet = None; - } - else if let Some(pos) = wallets.other_wallets.iter().position(|(w, _)| w == metadata) { + } else if let Some(pos) = wallets + .other_wallets + .iter() + .position(|(w, _)| w == metadata) + { wallets.other_wallets.remove(pos); - } - else { + } else { continue; } enqueue_popup_notification( @@ -324,11 +363,17 @@ impl MatchEvent for TspSettingsScreen { let previous_active = wallets.active_wallet.replace(metadata.clone()); // If the newly-default wallet was in the other wallets list, remove it // and then add the previous active wallet back to that other wallets list. - if let Some(idx_to_remove) = wallets.other_wallets.iter().position(|(w, _)| w == metadata) { + if let Some(idx_to_remove) = wallets + .other_wallets + .iter() + .position(|(w, _)| w == metadata) + { wallets.other_wallets.remove(idx_to_remove); } if let Some(previous_active) = previous_active { - wallets.other_wallets.insert(0, (previous_active, WalletStatus::Opened)); + wallets + .other_wallets + .insert(0, (previous_active, WalletStatus::Opened)); } self.view.redraw(cx); continue; @@ -345,10 +390,16 @@ impl MatchEvent for TspSettingsScreen { // Handle a newly-opened wallet. Some(TspWalletAction::WalletOpened(Ok(metadata))) => { let wallets = self.wallets.get_or_insert_default(); - if let Some((_m, status)) = wallets.other_wallets.iter_mut().find(|(w, _)| w == metadata) { + if let Some((_m, status)) = wallets + .other_wallets + .iter_mut() + .find(|(w, _)| w == metadata) + { *status = WalletStatus::Opened; } else { - wallets.other_wallets.push((metadata.clone(), WalletStatus::Opened)); + wallets + .other_wallets + .push((metadata.clone(), WalletStatus::Opened)); } self.view.redraw(cx); continue; @@ -363,8 +414,10 @@ impl MatchEvent for TspSettingsScreen { } // This is handled in the CreateWalletModal - Some(TspWalletAction::CreateWalletError { .. }) => { continue; } - None => { } + Some(TspWalletAction::CreateWalletError { .. }) => { + continue; + } + None => {} } match action.downcast_ref() { @@ -386,7 +439,10 @@ impl MatchEvent for TspSettingsScreen { match result { Ok(did) => { enqueue_popup_notification( - format!("Successfully republished identity \"{}\" to the DID server.", did), + format!( + "Successfully republished identity \"{}\" to the DID server.", + did + ), PopupKind::Success, Some(5.0), ); @@ -401,18 +457,35 @@ impl MatchEvent for TspSettingsScreen { } continue; } - Some(TspIdentityAction::SentDidAssociationRequest { .. }) => { continue; } // handled in the TspVerifyUser widget - Some(TspIdentityAction::ErrorSendingDidAssociationRequest { .. }) => { continue; } // handled in the TspVerifyUser widget - Some(TspIdentityAction::ReceivedDidAssociationResponse { .. }) => { continue; } // handled in the TspVerifyUser widget - Some(TspIdentityAction::ReceivedDidAssociationRequest { .. }) => { continue; } // handled in the TspVerificationModal widget - Some(TspIdentityAction::ReceiveLoopError { .. }) => { continue; } // handled in the top-level app - None => { } + Some(TspIdentityAction::SentDidAssociationRequest { .. }) => { + continue; + } // handled in the TspVerifyUser widget + Some(TspIdentityAction::ErrorSendingDidAssociationRequest { .. }) => { + continue; + } // handled in the TspVerifyUser widget + Some(TspIdentityAction::ReceivedDidAssociationResponse { .. }) => { + continue; + } // handled in the TspVerifyUser widget + Some(TspIdentityAction::ReceivedDidAssociationRequest { .. }) => { + continue; + } // handled in the TspVerificationModal widget + Some(TspIdentityAction::ReceiveLoopError { .. }) => { + continue; + } // handled in the top-level app + None => {} } } - - if self.view.button(cx, ids!(copy_identity_button)).clicked(actions) { - if let Some(did) = self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) { + if self + .view + .button(cx, ids!(copy_identity_button)) + .clicked(actions) + { + if let Some(did) = self + .wallets + .as_ref() + .and_then(|ws| ws.active_identity.as_deref()) + { cx.copy_to_clipboard(did); enqueue_popup_notification( "Copied your default TSP identity to the clipboard.", @@ -431,15 +504,25 @@ impl MatchEvent for TspSettingsScreen { // Allow the user to republish their identity to the DID server. // This is primarily needed because some DID servers (e.g., the test servers) // frequently wipe their identity storage after a certain period of time. - if self.view.button(cx, ids!(republish_identity_button)).clicked(actions) { + if self + .view + .button(cx, ids!(republish_identity_button)) + .clicked(actions) + { if self.has_default_wallet() { - if let Some(our_did) = self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) { + if let Some(our_did) = self + .wallets + .as_ref() + .and_then(|ws| ws.active_identity.as_deref()) + { script_apply_eval!(cx, republish_identity_button, { enabled: false, text: "Republishing DID now...", }); - submit_tsp_request(TspRequest::RepublishDid { did: our_did.to_string() }); + submit_tsp_request(TspRequest::RepublishDid { + did: our_did.to_string(), + }); } else { enqueue_popup_notification( "You must set a default TSP identity to be republished.", @@ -450,17 +533,29 @@ impl MatchEvent for TspSettingsScreen { } } - if self.view.button(cx, ids!(create_wallet_button)).clicked(actions) { + if self + .view + .button(cx, ids!(create_wallet_button)) + .clicked(actions) + { cx.action(CreateWalletModalAction::Open); } - if self.view.button(cx, ids!(create_did_button)).clicked(actions) { + if self + .view + .button(cx, ids!(create_did_button)) + .clicked(actions) + { if self.has_default_wallet() { cx.action(CreateDidModalAction::Open); } } - if self.view.button(cx, ids!(import_wallet_button)).clicked(actions) { + if self + .view + .button(cx, ids!(import_wallet_button)) + .clicked(actions) + { // TODO: support importing an existing wallet. enqueue_popup_notification( "Importing an existing wallet is not yet implemented.", @@ -475,8 +570,12 @@ impl TspSettingsScreen { /// Re-fetches the TSP state and populates this widget's list of wallets. fn refresh_wallets(&mut self) { let tsp_state = tsp_state_ref().lock().unwrap(); - let current_wallet = tsp_state.current_wallet.as_ref().map(|w| w.metadata.clone()); - let other_wallets = tsp_state.other_wallets + let current_wallet = tsp_state + .current_wallet + .as_ref() + .map(|w| w.metadata.clone()); + let other_wallets = tsp_state + .other_wallets .iter() .map(|entry| match entry { TspWalletEntry::Opened(opened) => (opened.metadata.clone(), WalletStatus::Opened), diff --git a/src/tsp/tsp_sign_indicator.rs b/src/tsp/tsp_sign_indicator.rs index 2c95bd168..3375b9775 100644 --- a/src/tsp/tsp_sign_indicator.rs +++ b/src/tsp/tsp_sign_indicator.rs @@ -47,7 +47,6 @@ pub enum TspSignState { WrongSignature, } - /// An indicator that is shown nearby a message that has a TSP signature. /// /// This widget is basically just a clickable icon group that shows @@ -61,8 +60,10 @@ pub enum TspSignState { /// #[derive(Script, ScriptHook, Widget)] pub struct TspSignIndicator { - #[deref] view: View, - #[rust] state: TspSignState, + #[deref] + view: View, + #[rust] + state: TspSignState, } impl Widget for TspSignIndicator { @@ -71,15 +72,14 @@ impl Widget for TspSignIndicator { let area = self.view.area(); let should_hover_in = match event.hits(cx, area) { - Hit::FingerLongPress(_) - | Hit::FingerHoverIn(..) => true, + Hit::FingerLongPress(_) | Hit::FingerHoverIn(..) => true, // TODO: show user profile and TSP info on click // Hit::FingerUp(fue) if fue.is_over && fue.is_primary_hit() => { // log!("todo: show user profile and TSP info."); // false // }, Hit::FingerHoverOut(_) => { - cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); + cx.widget_action(self.widget_uid(), TooltipAction::HoverOut); false } _ => false, @@ -92,7 +92,7 @@ impl Widget for TspSignIndicator { ), TspSignState::Verified => ( "This message was signed with the user's verified TSP identity.", - COLOR_FG_ACCEPT_GREEN, + COLOR_FG_ACCEPT_GREEN, ), TspSignState::WrongSignature => ( "Warning: this message's TSP signature does NOT match the expected sender signature.", @@ -100,7 +100,7 @@ impl Widget for TspSignIndicator { ), }; cx.widget_action( - self.widget_uid(), + self.widget_uid(), TooltipAction::HoverIn { text: text.to_string(), widget_rect: area.rect(cx), @@ -124,15 +124,9 @@ impl TspSignIndicator { let tsp_html_ref = self.view.html(cx, ids!(tsp_html)); if let Some(mut tsp_html) = tsp_html_ref.borrow_mut() { let (text, font_color) = match state { - TspSignState::Unknown => { - ("TSP ❔", COLOR_MESSAGE_NOTICE_TEXT) - } - TspSignState::Verified => { - ("TSP ✅", COLOR_FG_ACCEPT_GREEN) - } - TspSignState::WrongSignature => { - ("❗TSP❗", COLOR_FG_DANGER_RED) - } + TspSignState::Unknown => ("TSP ❔", COLOR_MESSAGE_NOTICE_TEXT), + TspSignState::Verified => ("TSP ✅", COLOR_FG_ACCEPT_GREEN), + TspSignState::WrongSignature => ("❗TSP❗", COLOR_FG_DANGER_RED), }; tsp_html.set_text(cx, text); tsp_html.font_color = font_color; @@ -152,7 +146,6 @@ impl TspSignIndicatorRef { } } - /// Actions emitted by an `TspSignIndicator` widget. #[derive(Clone, Debug, Default)] pub enum TspSignIndicatorAction { diff --git a/src/tsp/tsp_verification_modal.rs b/src/tsp/tsp_verification_modal.rs index 95a97089d..2e6aec302 100644 --- a/src/tsp/tsp_verification_modal.rs +++ b/src/tsp/tsp_verification_modal.rs @@ -1,8 +1,10 @@ - use makepad_widgets::*; use tsp_sdk::AsyncSecureStore; -use crate::{sliding_sync::current_user_id, tsp::{submit_tsp_request, TspRequest, TspVerificationDetails}}; +use crate::{ + sliding_sync::current_user_id, + tsp::{submit_tsp_request, TspRequest, TspVerificationDetails}, +}; script_mod! { link tsp_enabled @@ -90,8 +92,10 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct TspVerificationModal { - #[deref] view: View, - #[rust] state: TspVerificationModalState, + #[deref] + view: View, + #[rust] + state: TspVerificationModalState, } #[derive(Default)] @@ -185,7 +189,10 @@ impl WidgetMatchEvent for TspVerificationModal { // the wallet. If not, we need to show an error instructing the user // to add that VID to their wallet first and then retry the verification process. // Then, we need to send a negative response to the initiator of the request. - let error_text = if !wallet_db.has_private_vid(&details.responding_vid).is_ok_and(|v| v) { + let error_text = if !wallet_db + .has_private_vid(&details.responding_vid) + .is_ok_and(|v| v) + { Some(format!( "Error: the VID \"{}\" was not found in your current wallet.\n\n\ Either the requestor has the wrong VID for you, or you have not yet added that VID to your wallet.\n\n\ @@ -224,16 +231,17 @@ impl WidgetMatchEvent for TspVerificationModal { }, }); new_state = TspVerificationModalState::RequestDeclined; - } - else { - let prompt = format!("You have accepted the TSP verification request.\n\n\ + } else { + let prompt = format!( + "You have accepted the TSP verification request.\n\n\ Please confirm that the following code matches for both users:\n\n\ Code: \"{}\"\n", details.random_str, ); prompt_label.set_text(cx, &prompt); accept_button.set_text(cx, "Yes, they match!"); - new_state = TspVerificationModalState::RequestAccepted { details, wallet_db }; + new_state = + TspVerificationModalState::RequestAccepted { details, wallet_db }; } } @@ -246,7 +254,7 @@ impl WidgetMatchEvent for TspVerificationModal { let prompt_text = "You have confirmed the TSP verification request.\n\nSending a response now..."; prompt_label.set_text(cx, prompt_text); accept_button.set_enabled(cx, false); - // stay in this same state until we get an acknowledgment back + // stay in this same state until we get an acknowledgment back // that we sent the response (the `SentDidAssociationResponse` action). new_state = TspVerificationModalState::RequestAccepted { details, wallet_db }; } @@ -263,16 +271,22 @@ impl WidgetMatchEvent for TspVerificationModal { for action in actions { match action.downcast_ref() { - Some(TspVerificationModalAction::SentDidAssociationResponse { details, result }) - if self.state.details().is_some_and(|d| d == details) => - { + Some(TspVerificationModalAction::SentDidAssociationResponse { + details, + result, + }) if self.state.details().is_some_and(|d| d == details) => { match result { Ok(()) => { self.label(cx, ids!(prompt)).set_text(cx, "The TSP verification process has completed successfully.\n\nYou may now close this."); self.state = TspVerificationModalState::RequestVerified; } Err(e) => { - self.label(cx, ids!(prompt)).set_text(cx, &format!("Error: failed to complete the TSP verification process:\n\n{e}")); + self.label(cx, ids!(prompt)).set_text( + cx, + &format!( + "Error: failed to complete the TSP verification process:\n\n{e}" + ), + ); self.state = TspVerificationModalState::RequestDeclined; } } @@ -306,7 +320,8 @@ impl TspVerificationModal { wallet_db: AsyncSecureStore, ) { log!("Initializing TSP verification modal with: {:?}", details); - let prompt_text = format!("Matrix User \"{}\" is requesting to verify your identity via TSP.\n\ + let prompt_text = format!( + "Matrix User \"{}\" is requesting to verify your identity via TSP.\n\ Their TSP identity is: \"{}\".\n\n\ They want to verify your TSP identity \"{}\" associated with Matrix User ID \"{}\".\n\n\ If you recognize these details, would you like to accept this request?", @@ -328,10 +343,7 @@ impl TspVerificationModal { cancel_button.set_visible(cx, true); cancel_button.reset_hover(cx); - self.state = TspVerificationModalState::ReceivedRequest { - details, - wallet_db, - }; + self.state = TspVerificationModalState::ReceivedRequest { details, wallet_db }; } } @@ -339,7 +351,7 @@ impl TspVerificationModalRef { /// Initialize this modal with the details of a TSP verification request. pub fn initialize_with_details( &self, - cx: &mut Cx, + cx: &mut Cx, details: TspVerificationDetails, wallet_db: AsyncSecureStore, ) { diff --git a/src/tsp/verify_user.rs b/src/tsp/verify_user.rs index e41593f43..a28091292 100644 --- a/src/tsp/verify_user.rs +++ b/src/tsp/verify_user.rs @@ -1,8 +1,10 @@ - use makepad_widgets::*; use matrix_sdk::ruma::OwnedUserId; -use crate::{shared::popup_list::{enqueue_popup_notification, PopupKind}, tsp::{submit_tsp_request, tsp_state_ref, TspIdentityAction, TspRequest}}; +use crate::{ + shared::popup_list::{enqueue_popup_notification, PopupKind}, + tsp::{submit_tsp_request, tsp_state_ref, TspIdentityAction, TspRequest}, +}; script_mod! { link tsp_enabled @@ -113,11 +115,14 @@ pub enum TspVerifiedInfo { #[derive(Script, ScriptHook, Widget)] pub struct TspVerifyUser { - #[deref] view: View, + #[deref] + view: View, /// The Matrix User ID of the other user that we want to verify. - #[rust] user_id: Option, + #[rust] + user_id: Option, /// Info about whether the other user has or has not been verified via TSP. - #[rust] verified_info: TspVerifiedInfo, + #[rust] + verified_info: TspVerifiedInfo, } impl Widget for TspVerifyUser { @@ -132,7 +137,11 @@ impl Widget for TspVerifyUser { } impl MatchEvent for TspVerifyUser { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - if self.view.button(cx, ids!(remove_tsp_association_button)).clicked(actions) { + if self + .view + .button(cx, ids!(remove_tsp_association_button)) + .clicked(actions) + { enqueue_popup_notification( "Removing a TSP association is not yet implemented", PopupKind::Warning, @@ -165,14 +174,18 @@ impl MatchEvent for TspVerifyUser { { verify_user_button.set_text(cx, "Sent request!"); enqueue_popup_notification( - format!("Sent TSP verification request.\n\nWaiting for \"{user_id}\" to respond..."), + format!( + "Sent TSP verification request.\n\nWaiting for \"{user_id}\" to respond..." + ), PopupKind::Info, Some(5.0), ); } - Some(TspIdentityAction::ErrorSendingDidAssociationRequest { user_id, error, .. }) - if Some(user_id) == self.user_id.as_ref() => - { + Some(TspIdentityAction::ErrorSendingDidAssociationRequest { + user_id, + error, + .. + }) if Some(user_id) == self.user_id.as_ref() => { verify_user_button.set_enabled(cx, true); verify_user_button.set_text(cx, "Verify this user via TSP"); enqueue_popup_notification( @@ -181,9 +194,11 @@ impl MatchEvent for TspVerifyUser { None, ); } - Some(TspIdentityAction::ReceivedDidAssociationResponse { did, user_id, accepted }) - if Some(user_id) == self.user_id.as_ref() => - { + Some(TspIdentityAction::ReceivedDidAssociationResponse { + did, + user_id, + accepted, + }) if Some(user_id) == self.user_id.as_ref() => { if *accepted { enqueue_popup_notification( format!("User \"{user_id}\" accepted your TSP verification request."), @@ -217,12 +232,16 @@ impl TspVerifyUser { TspVerifiedInfo::Verified { did } => { verified_tsp_view.set_visible(cx, true); unverified_tsp_view.set_visible(cx, false); - verified_tsp_view.text_input(cx, ids!(tsp_did_read_only_input)).set_text(cx, did); + verified_tsp_view + .text_input(cx, ids!(tsp_did_read_only_input)) + .set_text(cx, did); } TspVerifiedInfo::Unverified => { verified_tsp_view.set_visible(cx, false); unverified_tsp_view.set_visible(cx, true); - unverified_tsp_view.text_input(cx, ids!(tsp_did_input)).set_text(cx, ""); + unverified_tsp_view + .text_input(cx, ids!(tsp_did_input)) + .set_text(cx, ""); let verify_user_button = unverified_tsp_view.button(cx, ids!(verify_user_button)); verify_user_button.set_enabled(cx, true); verify_user_button.set_text(cx, "Verify this user via TSP"); @@ -231,12 +250,15 @@ impl TspVerifyUser { } fn show(&mut self, cx: &mut Cx, user_id: OwnedUserId) { - let verified_info = tsp_state_ref().lock().unwrap() + let verified_info = tsp_state_ref() + .lock() + .unwrap() .get_associated_did(&user_id) - .map_or( - TspVerifiedInfo::Unverified, - |did| TspVerifiedInfo::Verified { did: did.to_string() }, - ); + .map_or(TspVerifiedInfo::Unverified, |did| { + TspVerifiedInfo::Verified { + did: did.to_string(), + } + }); self.verified_info = verified_info; self.user_id = Some(user_id); @@ -246,7 +268,9 @@ impl TspVerifyUser { impl TspVerifyUserRef { pub fn show(&self, cx: &mut Cx, user_id: OwnedUserId) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show(cx, user_id); } } diff --git a/src/tsp/wallet_entry/mod.rs b/src/tsp/wallet_entry/mod.rs index 2c2de8ab4..991bd0d5b 100644 --- a/src/tsp/wallet_entry/mod.rs +++ b/src/tsp/wallet_entry/mod.rs @@ -1,12 +1,18 @@ - use std::cell::RefCell; use makepad_widgets::*; use crate::{ app::ConfirmDeleteAction, - shared::{confirmation_modal::ConfirmationModalContent, popup_list::{enqueue_popup_notification, PopupKind}}, - tsp::{submit_tsp_request, tsp_settings_screen::{WalletStatus, WalletStatusAndDefault}, TspRequest, TspWalletMetadata} + shared::{ + confirmation_modal::ConfirmationModalContent, + popup_list::{enqueue_popup_notification, PopupKind}, + }, + tsp::{ + submit_tsp_request, + tsp_settings_screen::{WalletStatus, WalletStatusAndDefault}, + TspRequest, TspWalletMetadata, + }, }; script_mod! { @@ -108,26 +114,37 @@ script_mod! { } - /// A view showing the details of a single TSP wallet (one entry in the wallets list). #[derive(Script, ScriptHook, Widget)] pub struct WalletEntry { - #[deref] view: View, + #[deref] + view: View, - #[rust] metadata: Option, + #[rust] + metadata: Option, } impl Widget for WalletEntry { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); - let Some(metadata) = self.metadata.as_ref() else { return }; + let Some(metadata) = self.metadata.as_ref() else { + return; + }; if let Event::Actions(actions) = event { - if self.view.button(cx, ids!(set_default_wallet_button)).clicked(actions) { + if self + .view + .button(cx, ids!(set_default_wallet_button)) + .clicked(actions) + { submit_tsp_request(TspRequest::SetDefaultWallet(metadata.clone())); } - if self.view.button(cx, ids!(remove_wallet_button)).clicked(actions) { + if self + .view + .button(cx, ids!(remove_wallet_button)) + .clicked(actions) + { let metadata_clone = metadata.clone(); let content = ConfirmationModalContent { title_text: "Remove Wallet".into(), @@ -135,7 +152,8 @@ impl Widget for WalletEntry { "Are you sure you want to remove the wallet \"{}\" \ from the list?\n\nThis won't delete the actual wallet file.", metadata.wallet_name - ).into(), + ) + .into(), accept_button_text: Some("Remove".into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_tsp_request(TspRequest::RemoveWallet(metadata_clone)); @@ -145,7 +163,11 @@ impl Widget for WalletEntry { cx.action(ConfirmDeleteAction::Show(RefCell::new(Some(content)))); } - if self.view.button(cx, ids!(delete_wallet_button)).clicked(actions) { + if self + .view + .button(cx, ids!(delete_wallet_button)) + .clicked(actions) + { // TODO: Implement the delete wallet feature. enqueue_popup_notification( "Delete wallet feature is not yet implemented.", @@ -165,35 +187,23 @@ impl Widget for WalletEntry { self.metadata = Some(metadata.clone()); } - self.label(cx, ids!(wallet_name)).set_text( - cx, - &metadata.wallet_name, - ); - self.label(cx, ids!(wallet_path)).set_text( - cx, - metadata.url.as_url_unencoded() - ); + self.label(cx, ids!(wallet_name)) + .set_text(cx, &metadata.wallet_name); + self.label(cx, ids!(wallet_path)) + .set_text(cx, metadata.url.as_url_unencoded()); // There is a weird makepad bug where if we re-style one instance of the // `set_default_wallet_button` in one WalletEntry, all other instances of that button // get their styling messed up in weird ways. // So, as a workaround, we just hide the button entirely and show a `is_default_label_view` instead. - self.view(cx, ids!(is_default_label_view)).set_visible( - cx, - sd.is_default - ); - self.view(cx, ids!(not_found_label_view)).set_visible( - cx, - sd.status == WalletStatus::NotFound, - ); - self.button(cx, ids!(set_default_wallet_button)).set_visible( - cx, - !sd.is_default && sd.status != WalletStatus::NotFound, - ); - self.button(cx, ids!(delete_wallet_button)).set_visible( - cx, - sd.status != WalletStatus::NotFound, - ); + self.view(cx, ids!(is_default_label_view)) + .set_visible(cx, sd.is_default); + self.view(cx, ids!(not_found_label_view)) + .set_visible(cx, sd.status == WalletStatus::NotFound); + self.button(cx, ids!(set_default_wallet_button)) + .set_visible(cx, !sd.is_default && sd.status != WalletStatus::NotFound); + self.button(cx, ids!(delete_wallet_button)) + .set_visible(cx, sd.status != WalletStatus::NotFound); self.view.draw_walk(cx, scope, walk) } diff --git a/src/utils.rs b/src/utils.rs index aa3ac8143..25d68db19 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,22 @@ -use std::{borrow::Cow, ops::{Deref, DerefMut}, time::SystemTime}; +use std::{ + borrow::Cow, + ops::{Deref, DerefMut}, + time::SystemTime, +}; use serde::{Deserialize, Serialize}; use url::Url; use unicode_segmentation::UnicodeSegmentation; use chrono::{DateTime, Duration, Local, TimeZone}; use makepad_widgets::{Cx, Event, ImageRef, error, image_cache::ImageError}; -use matrix_sdk::{media::{MediaFormat, MediaThumbnailSettings}, ruma::{api::client::media::get_content_thumbnail::v3::Method, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId}, RoomDisplayName}; +use matrix_sdk::{ + media::{MediaFormat, MediaThumbnailSettings}, + ruma::{ + api::client::media::get_content_thumbnail::v3::Method, MilliSecondsSinceUnixEpoch, + OwnedRoomId, RoomId, + }, + RoomDisplayName, +}; use matrix_sdk_ui::timeline::{EventTimelineItem, PaginationError, TimelineDetails}; use crate::{ @@ -16,7 +27,6 @@ use crate::{ /// The scheme for GEO links, used for location messages in Matrix. pub const GEO_URI_SCHEME: &str = "geo:"; - /// A wrapper type that implements the `Debug` trait for non-`Debug` types. pub struct DebugWrapper(T); impl std::fmt::Debug for DebugWrapper { @@ -58,16 +68,16 @@ pub fn is_interactive_hit_event(event: &Event) -> bool { matches!( event, Event::MouseDown(..) - | Event::MouseUp(..) - | Event::MouseMove(..) - | Event::MouseLeave(..) - | Event::TouchUpdate(..) - | Event::Scroll(..) - | Event::KeyDown(..) - | Event::KeyUp(..) - | Event::TextInput(..) - | Event::TextCopy(..) - | Event::TextCut(..) + | Event::MouseUp(..) + | Event::MouseMove(..) + | Event::MouseLeave(..) + | Event::TouchUpdate(..) + | Event::Scroll(..) + | Event::KeyDown(..) + | Event::KeyUp(..) + | Event::TextInput(..) + | Event::TextCopy(..) + | Event::TextCut(..) ) } @@ -94,7 +104,6 @@ impl ImageFormat { /// /// Returns an error if either load fails or if the image format is unknown. pub fn load_png_or_jpg(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), ImageError> { - fn attempt_both(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), ImageError> { img.load_png_from_data(cx, data) .or_else(|_| img.load_jpg_from_data(cx, data)) @@ -130,14 +139,16 @@ pub fn load_png_or_jpg(img: &ImageRef, cx: &mut Cx, data: &[u8]) -> Result<(), I ); path.push(filename); path.set_extension("unknown"); - error!("Failed to load PNG/JPG: {err}. Dumping bad image: {:?}", path); + error!( + "Failed to load PNG/JPG: {err}. Dumping bad image: {:?}", + path + ); let _ = std::fs::write(path, data) .inspect_err(|e| error!("Failed to write bad image to disk: {e}")); } res } - /// Parses a CSS-style hex color string into a `Vec4` with RGBA components in `[0.0, 1.0]`. /// /// Supports the following formats (with or without a leading `#`): @@ -211,7 +222,6 @@ pub enum VecDiff { Truncate { length: usize }, } - pub fn unix_time_millis_to_datetime(millis: MilliSecondsSinceUnixEpoch) -> Option> { let millis: i64 = millis.get().into(); Local.timestamp_millis_opt(millis).single() @@ -338,10 +348,10 @@ pub fn replace_linebreaks_separators<'a>(s: &'a str, is_html: bool) -> Cow<'a, s /// pub fn remove_mx_reply(html_message_body: &str) -> &str { const MX_REPLY_START: &str = ""; - const MX_REPLY_END: &str = ""; + const MX_REPLY_END: &str = ""; if html_message_body.trim().starts_with(MX_REPLY_START) { if let Some(end) = html_message_body.find(MX_REPLY_END) { - if let Some(after) = html_message_body.get(end + MX_REPLY_END.len() ..) { + if let Some(after) = html_message_body.get(end + MX_REPLY_END.len()..) { return after; } } @@ -361,9 +371,13 @@ pub fn stringify_join_leave_error( // We get the string representation of the error and then search for the "got" state. matrix_sdk::Error::WrongRoomState(wrs) => { if was_join && wrs.to_string().contains(", got: Joined") { - Some(format!("Failed to join {room_name_id}: it has already been joined.")) + Some(format!( + "Failed to join {room_name_id}: it has already been joined." + )) } else if !was_join && wrs.to_string().contains(", got: Left") { - Some(format!("Failed to leave {room_name_id}: it has already been left.")) + Some(format!( + "Failed to leave {room_name_id}: it has already been left." + )) } else { None } @@ -372,27 +386,35 @@ pub fn stringify_join_leave_error( // This avoids the weird "no known servers" error, which is misleading and incorrect. // See: . matrix_sdk::Error::Http(error) - if error.as_client_api_error().is_some_and(|e| e.status_code.as_u16() == 404) => + if error + .as_client_api_error() + .is_some_and(|e| e.status_code.as_u16() == 404) => { Some(format!( "Failed to {} {room_name_id}: the room no longer exists on the server.{}", if was_join { "join" } else { "leave" }, - if was_join && was_invite { "\n\nYou may safely reject this invite." } else { "" }, + if was_join && was_invite { + "\n\nYou may safely reject this invite." + } else { + "" + }, )) } _ => None, }; - msg_opt.unwrap_or_else(|| format!( - "Failed to {} {}: {}", - match (was_join, was_invite) { - (true, true) => "accept invite to", - (true, false) => "join", - (false, true) => "reject invite to", - (false, false) => "leave", - }, - room_name_id, - error - )) + msg_opt.unwrap_or_else(|| { + format!( + "Failed to {} {}: {}", + match (was_join, was_invite) { + (true, true) => "accept invite to", + (true, false) => "join", + (false, true) => "reject invite to", + (false, false) => "leave", + }, + room_name_id, + error + ) + }) } /// Returns a string error message for pagination errors, @@ -409,10 +431,12 @@ pub fn stringify_pagination_error( match sdk_error { matrix_sdk::Error::Http(http_error) => match http_error.deref() { matrix_sdk::HttpError::Reqwest(reqwest_error) if reqwest_error.is_timeout() => { - return Some(format!("Failed to load earlier messages in \"{room_name}\": request timed out.")); + return Some(format!( + "Failed to load earlier messages in \"{room_name}\": request timed out." + )); } _ => {} - } + }, _ => {} } None @@ -420,14 +444,17 @@ pub fn stringify_pagination_error( match error { TimelineError::PaginationError(PaginationError::NotSupported) => { - return format!("Failed to load earlier messages in \"{room_name}\": \ - pagination is not supported in this timeline focus mode."); + return format!( + "Failed to load earlier messages in \"{room_name}\": \ + pagination is not supported in this timeline focus mode." + ); } - TimelineError::PaginationError(PaginationError::Paginator(PaginatorError::SdkError(sdk_error))) - | TimelineError::EventCacheError(EventCacheError::BackpaginationError(sdk_error)) => - { + TimelineError::PaginationError(PaginationError::Paginator(PaginatorError::SdkError( + sdk_error, + ))) + | TimelineError::EventCacheError(EventCacheError::BackpaginationError(sdk_error)) => { if let Some(message) = match_sdk_error(sdk_error) { - return message; + return message; } } _ => {} @@ -435,8 +462,6 @@ pub fn stringify_pagination_error( format!("Failed to load earlier messages in \"{room_name}\": {error}") } - - /// Formats a given Unix timestamp in milliseconds into a relative human-readable date. /// /// # Cases: @@ -463,7 +488,11 @@ pub fn relative_format(millis: MilliSecondsSinceUnixEpoch) -> Option { if duration < Duration::seconds(60) { Some("Now".to_string()) } else if duration < Duration::minutes(60) { - let minutes_text = if duration.num_minutes() == 1 { "min" } else { "mins" }; + let minutes_text = if duration.num_minutes() == 1 { + "min" + } else { + "mins" + }; Some(format!("{} {} ago", duration.num_minutes(), minutes_text)) } else if duration < Duration::hours(24) && now.date_naive() == datetime.date_naive() { Some(format!("{}", datetime.format("%H:%M"))) // "HH:MM" format for today @@ -485,12 +514,9 @@ pub fn relative_format(millis: MilliSecondsSinceUnixEpoch) -> Option { /// skipping any leading "@" characters. pub fn user_name_first_letter(user_name: &str) -> Option<&str> { use unicode_segmentation::UnicodeSegmentation; - user_name - .graphemes(true) - .find(|&g| g != "@") + user_name.graphemes(true).find(|&g| g != "@") } - /// A const-compatible version of [`MediaFormat`]. #[derive(Clone, Debug)] pub enum MediaFormatConst { @@ -538,26 +564,23 @@ impl From for MediaThumbnailSettings { } } - /// The thumbnail format to use for user and room avatars. -pub const AVATAR_THUMBNAIL_FORMAT: MediaFormatConst = MediaFormatConst::Thumbnail( - MediaThumbnailSettingsConst { +pub const AVATAR_THUMBNAIL_FORMAT: MediaFormatConst = + MediaFormatConst::Thumbnail(MediaThumbnailSettingsConst { method: Method::Scale, width: 40, height: 40, animated: false, - } -); + }); /// The thumbnail format to use for regular media images. -pub const MEDIA_THUMBNAIL_FORMAT: MediaFormatConst = MediaFormatConst::Thumbnail( - MediaThumbnailSettingsConst { +pub const MEDIA_THUMBNAIL_FORMAT: MediaFormatConst = + MediaFormatConst::Thumbnail(MediaThumbnailSettingsConst { method: Method::Scale, width: 400, height: 400, animated: false, - } -); + }); /// Removes leading whitespace and HTML whitespace tags (`

You can enter a room/space address using either:

  • An alias, starting with #, like #robrix:matrix.org.
  • An ID, starting with !, like !moVNEIUPxJZpxRHDUv:matrix.org.
  • A Matrix link, like https:matrix.to/... or matrix:....
", + "add_room.popup.cannot_add_self": "You cannot add yourself as a friend.", + "add_room.popup.invalid_user_id": "Invalid Matrix user ID.\n\nError: {error}", + "add_room.popup.parse_error": "Could not parse the text as a valid room address.\nError: {error}.", + "add_room.popup.fetch_error": "Failed to fetch room info.\n\nError: {error}.", + "add_room.popup.knock_success": "Successfully knocked on {room_type} {room_name}.", + "add_room.popup.knock_failed": "Failed to knock on room.\n\nError: {error}.", + "add_room.popup.join_success": "Successfully joined {room_type} {room_name}.", + "add_room.popup.join_failed": "Failed to join room.\n\nError: {error}.", + "add_room.popup.created_room_success": "Successfully created room \"{room_name}\".", + "add_room.popup.created_room_space_link_suffix": "\n\nThe room was created, but it could not be linked into the selected space.\nError: {error}", + "add_room.popup.create_room_failed": "Failed to create room \"{room_name}\".\n\nError: {error}", + "add_room.feedback.create_room_failed": "Failed to create room: {error}", + "add_room.feedback.creating_room": "Creating room...", + "add_room.feedback.room_created_syncing": "Room created. Syncing it into the space...", + "add_room.feedback.room_created_link_failed_opening": "Room created, but linking it into the space failed. Opening the room...", + "add_room.feedback.room_created_opening": "Room created. Opening the room...", + "add_room.loading.fetching": "Fetching {target}...", + "add_room.fetched.room_name.unnamed": "Unnamed {room_or_space_uc}, ID: {room_id}", + "add_room.fetched.main_alias_and_id": "Main {room_or_space_uc} Alias and ID", + "add_room.fetched.alias.not_set": "not set", + "add_room.fetched.alias": "Alias: {alias}", + "add_room.fetched.id": "ID: {room_id}", + "add_room.fetched.topic_title": "{room_or_space_uc} Topic", + "add_room.fetched.topic.not_set_html": "No topic set", + "add_room.summary.already_joined": "You have already joined this {room_or_space_lc}.", + "add_room.summary.banned": "You have been banned from this {room_or_space_lc}.", + "add_room.summary.already_invited": "You have already been invited to this {room_or_space_lc}.", + "add_room.summary.already_knocked": "You have already knocked on this {room_or_space_lc}.", + "add_room.summary.previously_left": "You previously left this {room_or_space_lc}.", + "add_room.summary.member_count": "This is a {directness} {room_or_space_lc} with {num_members} {member_word}.", + "add_room.summary.knocked_waiting": "You have knocked on this {room_or_space_lc} and must now wait for someone to invite you in.", + "add_room.summary.joined_loading": "You have joined this {room_or_space_lc}. It is now being loaded from the homeserver; please wait...", + "add_room.summary.loaded": "You have {verb} this {room_or_space_lc}.", + "add_room.button.go_to": "Go to {room_or_space_lc}", + "add_room.button.cannot_join_until_unbanned": "Cannot join until un-banned", + "add_room.button.go_to_invitation": "Go to invitation", + "add_room.button.knock_again": "Knock again (be nice!)", + "add_room.button.rejoin": "Re-join this {room_or_space_lc}", + "add_room.button.rejoin_requires_invite": "Re-joining {room_or_space_lc} requires an invite", + "add_room.button.knock_to_rejoin": "Knock to re-join {room_or_space_lc}", + "add_room.button.rejoin_requires_other_membership": "Re-joining {room_or_space_lc} requires an invite or other room membership", + "add_room.button.not_allowed_to_rejoin": "Not allowed to re-join this {room_or_space_lc}", + "add_room.button.join": "Join this {room_or_space_lc}", + "add_room.button.join_requires_invite": "Joining {room_or_space_lc} requires an invite", + "add_room.button.knock_to_join": "Knock to join {room_or_space_lc}", + "add_room.button.join_requires_other_membership": "Joining {room_or_space_lc} requires an invite or other room membership", + "add_room.button.not_allowed_to_join": "Not allowed to join this {room_or_space_lc}", + "add_room.button.successfully_knocked": "Successfully knocked!", + "add_room.button.successfully_joined": "Successfully joined!", + "add_room.button.go_to_loaded": "Go to {adj} {room_or_space_lc}", + "add_room.word.direct": "direct", + "add_room.word.regular": "regular", + "add_room.word.member": "member", + "add_room.word.members": "members", + "add_room.word.room_lc": "room", + "add_room.word.space_lc": "space", + "add_room.word.room_uc": "Room", + "add_room.word.space_uc": "Space", + "add_room.word.verb.invited": "been invited to", + "add_room.word.verb.joined": "fully joined", + "add_room.word.adj.invited": "invited", + "add_room.word.adj.joined": "joined", + + "settings.account.title": "Account Settings", + "settings.account.section.your_avatar": "Your Avatar:", + "settings.account.section.your_display_name": "Your Display Name:", + "settings.account.section.your_user_id": "Your User ID:", + "settings.account.section.other_actions": "Other actions:", + "settings.account.display_name.placeholder": "Add a display name...", + "settings.account.user_id.not_logged_in": "You are not logged in.", + "settings.account.button.upload_avatar": "Upload Avatar", + "settings.account.button.delete_avatar": "Delete Avatar", + "settings.account.button.cancel": "Cancel", + "settings.account.button.save_name": "Save Name", + "settings.account.button.manage_account": "Manage Account", + "settings.account.button.log_out": "Log out", + "settings.account.button.logging_out": "Logging out...", + "settings.account.tooltip.copy_user_id": "Copy User ID", + "settings.account.popup.avatar_updated": "Successfully updated avatar.", + "settings.account.popup.avatar_deleted": "Successfully deleted avatar.", + "settings.account.popup.avatar_upload_not_implemented": "Avatar uploading is not yet implemented.", + "settings.account.popup.deleting_avatar": "Deleting your avatar...", + "settings.account.popup.display_name_updated": "Successfully updated display name.", + "settings.account.popup.display_name_removed": "Successfully removed display name.", + "settings.account.popup.uploading_display_name": "Uploading new display name...", + "settings.account.popup.copied_user_id": "Copied your User ID to the clipboard.", + "settings.account.popup.account_management_not_implemented": "Account management is not yet implemented.", + "settings.account.modal.delete_avatar.title": "Delete Avatar", + "settings.account.modal.delete_avatar.body": "Are you sure you want to delete your avatar?", + "settings.account.modal.delete_avatar.accept": "Delete", + + "settings.labs.app_service.title": "App Service", + "settings.labs.app_service.description": "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands.", + "settings.labs.app_service.enable_label": "Enable App Service", + "settings.labs.app_service.botfather_user_id": "BotFather User ID:", + "settings.labs.app_service.botfather_placeholder": "bot or @bot:server", + "settings.labs.app_service.button.enable": "Enable App Service", + "settings.labs.app_service.button.disable": "Disable App Service", + "settings.labs.app_service.button.save": "Save", + "settings.labs.app_service.popup.saved": "Saved Matrix app service settings.", + + "invite_screen.message.invited_by": "has invited you to join:", + "invite_screen.message.invited_generic": "You have been invited to join:", + "invite_screen.button.reject": "Reject Invite", + "invite_screen.button.join": "Join Room", + "invite_screen.button.joining": "Joining...", + "invite_screen.button.rejecting": "Rejecting...", + "invite_screen.button.joined": "Joined!", + "invite_screen.popup.joined_success": "Successfully joined room.", + "invite_screen.popup.rejected_success": "Successfully rejected invite.", + "invite_screen.popup.reject_failed": "Failed to reject invite: {error}", + "invite_screen.completion.rejected": "Invite successfully rejected. You may close this invite.", + "invite_modal.title.invite_to_room_name": "Invite to {room_name}", + "invite_modal.input.placeholder": "@user:example.org", + "invite_modal.button.cancel": "Cancel", + "invite_modal.button.invite": "Invite", + "invite_modal.button.okay": "Okay", + "invite_modal.status.enter_user_id": "Please enter a user ID.", + "invite_modal.status.sending": "Sending invite...", + "invite_modal.status.invalid_user_id": "Invalid User ID. Expected format: @user:server.xyz", + "invite_modal.status.success_invited": "Successfully invited {user_id}!", + "invite_modal.status.send_failed": "Failed to send invite: {error}", + "rooms_list_entry.invited.by_name_and_user": "Invited by {display_name} ({user_id})", + "rooms_list_entry.invited.by_user": "Invited by {user_id}", + "rooms_list_entry.invited.generic": "You were invited", + + "loading_pane.title.default": "Loading content...", + "loading_pane.title.searching_older": "Searching older messages...", + "loading_pane.status.searching_event": "Looking for event {target_event_id}\n\nFetched {events_paginated} messages so far...", + "loading_pane.title.error": "Error loading content", + "loading_pane.button.cancel": "Cancel", + "loading_pane.button.okay": "Okay", + + "rooms_list_header.title.all_rooms": "All Rooms", + "rooms_list_header.popup.offline": "Cannot reach the Matrix homeserver. Please check your connection.", + "rooms_list_header.tooltip.syncing": "Syncing...", + "rooms_list_header.tooltip.offline": "Offline", + "rooms_list_header.tooltip.synced": "Fully synced", + + "room_filter_input.placeholder": "Filter rooms & spaces...", + "search_messages.button.todo": "Search (TODO)", + + "welcome_screen.title": "Welcome to Robrix!", + "welcome_screen.body_html": "

Our Matrix client is under heavy development. Currently, you can access the rooms and spaces that you've joined in other clients.


But don't worry, we're constantly expanding the featureset of Robrix!


Look for the latest announcements in our Matrix channel:

#robrix:matrix.org

", + + "room_screen.bot.delete.error.empty_user_id": "Please enter the bot Matrix user ID to delete.", + "room_screen.bot.delete.error.invalid_user_id": "Invalid Matrix user ID: {full_user_id}", + "room_screen.bot.delete.error.current_user_unavailable": "Current user ID is unavailable, so the bot homeserver cannot be resolved.", + "room_screen.tooltip.reacted_with_suffix": " reacted with: {reaction}", + "room_screen.modal.invite.title": "Send Invitation", + "room_screen.modal.invite.body": "Are you sure you want to invite {username} to this room?", + "room_screen.modal.invite.accept": "Invite", + "room_screen.popup.invite.sent_success": "Sent invite successfully.", + "room_screen.popup.invite.failed": "Failed to send invite.\n\nError: {error}", + "room_screen.popup.app_service.enable_before_create": "Enable App Service before creating bots in a room.", + "room_screen.popup.app_service.bind_before_create": "Bind BotFather to this room before creating a bot.", + "room_screen.popup.app_service.state_unavailable_create": "App state is unavailable, so bot creation is temporarily unavailable.", + "room_screen.popup.app_service.enable_before_delete": "Enable App Service before deleting bots in a room.", + "room_screen.popup.app_service.bind_before_delete": "Bind BotFather to this room before deleting a bot.", + "room_screen.popup.app_service.state_unavailable_delete": "App state is unavailable, so bot deletion is temporarily unavailable.", + "room_screen.popup.app_service.room_not_bound": "This room is not currently bound to BotFather.", + "room_screen.popup.app_service.removing_botfather": "Removing BotFather {bot_user_id} from this room...", + "room_screen.popup.app_service.state_unavailable_unbind": "App state is unavailable, so BotFather could not be removed from this room.", + "room_screen.popup.bot.main_timeline_only": "Bot commands are only supported in the main room timeline.", + "room_screen.popup.bot.enable_in_settings_before_bot": "Enable App Service in Settings before using /bot.", + "room_screen.popup.bot.bind_before_bot": "Bind BotFather to this room before using /bot.", + "room_screen.popup.bot.enable_before_commands": "Enable App Service before using BotFather commands in a room.", + "room_screen.popup.bot.bind_before_commands": "Bind BotFather to this room before using BotFather commands.", + "room_screen.popup.bot.creation_main_timeline_only": "Bot creation commands are only supported in the main room timeline.", + "room_screen.popup.bot.sent_listbots": "Sent `/listbots` to BotFather.", + "room_screen.popup.bot.sent_bothelp": "Sent `/bothelp` to BotFather.", + "room_screen.popup.bot.sent_createbot": "Sent `/createbot` for `{username}` to BotFather.", + "room_screen.popup.bot.sent_deletebot": "Sent `/deletebot` for {matrix_user_id} to BotFather.", + "room_screen.popup.bot.state_unavailable_create_command": "App state is unavailable, so the create-bot command was not sent.", + "room_screen.popup.bot.state_unavailable_delete_command": "App state is unavailable, so the delete-bot command was not sent.", + "room_screen.fallback.unnamed_room": "Unnamed Room", + "room_screen.unsupported.prefix": "[Unsupported]", + "room_screen.read_marker.new_messages": "New Messages", + "room_screen.top_space.loading_earlier": "Loading earlier messages...", + "room_screen.loading.found_related_message": "Successfully found replied-to message!", + "room_screen.loading.related_message_not_found": "Unable to find related message; it may have been deleted.", + "room_screen.popup.pin.pinned_success": "Successfully pinned event.", + "room_screen.popup.pin.unpinned_success": "Successfully unpinned event.", + "room_screen.popup.pin.already_pinned": "Message was already pinned.", + "room_screen.popup.pin.already_unpinned": "Message was already unpinned.", + "room_screen.popup.pin.pin_failed": "Failed to pin event. Error: {error}", + "room_screen.popup.pin.unpin_failed": "Failed to unpin event. Error: {error}", + "room_screen.popup.already_viewing_room": "You are already viewing that room.", + "room_screen.popup.open_url_failed": "Could not open URL: {url}", + "room_screen.popup.message.reply_not_found": "Could not find message in timeline to reply to. Please try again.", + "room_screen.popup.message.edit_not_found": "Could not find message in timeline to edit. Please try again.", + "room_screen.popup.message.no_recent_editable": "No recent message available to edit. Please manually select a message to edit.", + "room_screen.popup.message.cannot_pin": "This event cannot be pinned.", + "room_screen.popup.message.cannot_unpin": "This event cannot be unpinned.", + "room_screen.popup.message.copy_text_not_found": "Could not find message in timeline to copy text from. Please try again.", + "room_screen.popup.message.copy_html_not_found": "Could not find message in timeline to copy HTML from. Please try again.", + "room_screen.popup.message.copy_link_failed": "Couldn't create permalink to message. Please try again.", + "room_screen.popup.message.view_source_not_found": "Could not find message in timeline to view source.", + "room_screen.popup.message.related_not_found": "Could not find related message or event in timeline.", + "room_screen.modal.delete_message.title": "Delete Message", + "room_screen.modal.delete_message.body": "Are you sure you want to delete this message? This cannot be undone.", + "room_screen.modal.delete_message.accept": "Delete", + "room_screen.server_notice.title": "Server notice:", + "room_screen.server_notice.notice_type": "Notice type", + "room_screen.server_notice.limit_type": "Limit type", + "room_screen.server_notice.admin_contact": "Admin contact", + "room_screen.server_notice.username": "Server notice", + "room_screen.verification.sent_prefix": "Sent a ", + "room_screen.verification.request": "verification request", + "room_screen.verification.sent_to_suffix": " to {user_id}.", + "room_screen.verification.supported_methods": "Supported methods", + "room_screen.image.unsupported_type": "{body}\n\nUnsupported type {mime}", + "room_screen.image.failed_to_display": "{body}\n\nFailed to display image: {error}", + "room_screen.image.failed_to_fetch": "{body}\n\nFailed to fetch image from {mxc_uri}", + "room_screen.image.encrypted_todo": "{body}\n\n[TODO] fetch encrypted image at {url}", + "room_screen.image.no_source_url": "{body}\n\nImage message had no source URL.", + "room_screen.file.preview_html": "{filename}{size}{caption}
File download not yet supported.", + "room_screen.audio.preview_html": "Audio: {filename}{mime}{duration}{size}{caption}
Audio playback not yet supported.", + "room_screen.video.preview_html": "Video: {filename}{mime}{duration}{size}{dimensions}{caption}
Video playback not yet supported.", + "room_screen.location.label": "Location:", + "room_screen.location.open_osm": "Open in OpenStreetMap", + "room_screen.location.open_google_maps": "Open in Google Maps", + "room_screen.location.open_apple_maps": "Open in Apple Maps", + "room_screen.location.invalid_html": "[Location invalid] {body}", + "room_screen.redacted.self_with_reason": "⛔ Deleted their own message. Reason: \"{reason}\".", + "room_screen.redacted.self": "⛔ Deleted their own message.", + "room_screen.redacted.other_with_reason": "⛔ {redactor} deleted this message. Reason: \"{reason}\".", + "room_screen.redacted.other": "⛔ {redactor} deleted this message.", + "room_screen.redacted.generic": "⛔ Message deleted.", + "room_screen.reply_preview.error_username": "[Error fetching username]", + "room_screen.reply_preview.error_event": "[Error fetching replied-to event]", + "room_screen.reply_preview.loading_username": "[Loading username...]", + "room_screen.reply_preview.loading_event": "[Loading replied-to message...]", + "room_screen.thread_summary.loading_latest_reply": "Loading latest reply...", + "room_screen.thread_summary.error_latest_reply": "Unable to load latest reply", + "room_screen.thread_summary.one_reply": "1 reply", + "room_screen.thread_summary.n_replies": "{n} replies", + "room_screen.small_state.invite_to_room": "Invite to Room", + "room_screen.app_service.sender_name": "BotFather", + "room_screen.app_service.sender_tag": "bot", + "room_screen.app_service.title": "App Service Actions", + "room_screen.app_service.subtitle": "Create a bot through BotFather. Robrix only sends the matching slash command.", + "room_screen.app_service.timestamp_now": "now", + "room_screen.app_service.button.create_bot": "Create Bot", + "room_screen.app_service.button.list_bots": "List Bots", + "room_screen.app_service.button.delete_bot": "Delete Bot", + "room_screen.app_service.button.bot_help": "Bot Help", + "room_screen.app_service.button.unbind": "Unbind", + + "spaces_bar.tooltip.unknown_space_name": "Unknown Space Name", + "spaces_bar.status.none_matching": "Found no\nmatching spaces.", + "spaces_bar.status.none_joined": "Found no\njoined spaces.", + "spaces_bar.status.one_matching": "Found 1\nmatching space.", + "spaces_bar.status.one_joined": "Found 1\njoined space.", + "spaces_bar.status.n_matching": "Found {count}\nmatching spaces.", + "spaces_bar.status.n_joined": "Found {count}\njoined spaces.", + "spaces_bar.status.many_matching": "Found 99+\nmatching spaces.", + "spaces_bar.status.many_joined": "Found 99+\njoined spaces.", + + "tsp.settings.title": "TSP Wallet Settings", + "tsp.settings.section.active_identity": "Your active identity:", + "tsp.settings.section.wallets": "Your Wallets:", + "tsp.settings.wallet.none": "No wallets found. Create or import a wallet.", + "tsp.settings.identity.none_set": "No default identity has been set.", + "tsp.settings.button.republish_identity": "Republish Current Identity to DID Server", + "tsp.settings.button.republishing_now": "Republishing DID now...", + "tsp.settings.button.create_identity": "Create New Identity (DID)", + "tsp.settings.button.create_wallet": "Create New Wallet", + "tsp.settings.button.import_wallet": "Import Existing Wallet", + "tsp.settings.popup.wallet.removed": "Removed wallet \"{wallet_name}\".", + "tsp.settings.popup.wallet.default_removed_warning": "The default wallet was removed.\n\nTSP features will not work properly until you set a default wallet.", + "tsp.settings.popup.wallet.set_default_failed": "Failed to set default wallet, could not find or open selected wallet.", + "tsp.settings.popup.wallet.open_failed": "Failed to open wallet: {error}", + "tsp.settings.popup.wallet.import_not_implemented": "Importing an existing wallet is not yet implemented.", + "tsp.settings.popup.wallet.none_found": "No TSP wallets found.\n\nPlease create or import a wallet.", + "tsp.settings.popup.wallet.no_default": "No default TSP wallet is set.\n\nPlease select or create a default wallet.", + "tsp.settings.popup.identity.republish_success": "Successfully republished identity \"{did}\" to the DID server.", + "tsp.settings.popup.identity.republish_failed": "Failed to republish identity to the DID server: {error}", + "tsp.settings.popup.identity.copied": "Copied your default TSP identity to the clipboard.", + "tsp.settings.popup.identity.none_set": "No default TSP identity has been set.", + "tsp.settings.popup.identity.must_set_default": "You must set a default TSP identity to be republished.", + "tsp_dummy.message.disabled": "TSP features are not included in this build.\nTo use TSP, build Robrix with the 'tsp' feature enabled.", + "tsp.wallet_entry.default_label": "✅ Default", + "tsp.wallet_entry.not_found": "Wallet not found!", + "tsp.wallet_entry.button.set_default": "Set As Default", + "tsp.wallet_entry.button.remove": "Remove From List", + "tsp.wallet_entry.button.delete": "Delete Wallet", + "tsp.wallet_entry.modal.remove.title": "Remove Wallet", + "tsp.wallet_entry.modal.remove.body": "Are you sure you want to remove the wallet \"{wallet_name}\" from the list?\n\nThis won't delete the actual wallet file.", + "tsp.wallet_entry.modal.remove.accept": "Remove", + "tsp.wallet_entry.popup.delete_not_implemented": "Delete wallet feature is not yet implemented.", + + "app.room_filter.search_results_title": "Search Results", + "app.room_filter.empty_hint": "Type to search rooms and spaces...", + "app.room_filter.no_local_results": "No local results for \"{keywords}\". Choose a type below to search server.", + "app.room_filter.searching_remote": "Searching {kind} on server...", + "app.room_filter.remote.people": "People", + "app.room_filter.remote.rooms": "Rooms", + "app.room_filter.remote.spaces": "Spaces", + "app.room_filter.remote.kind.people": "people", + "app.room_filter.remote.kind.rooms": "rooms", + "app.room_filter.remote.kind.spaces": "spaces", + + "rooms_list.category.invites": "Invites", + "rooms_list.category.favorites": "Favorites", + "rooms_list.category.rooms": "Rooms", + "rooms_list.category.people": "People", + "rooms_list.category.low_priority": "Low Priority", + "rooms_list.category.left_rooms": "Left Rooms", + + "space_lobby.entry.explore_space": "Explore this Space", + "space_lobby.header.welcome": "Welcome to the space:", + "space_lobby.header.public_space": "🌐 Public space", + "space_lobby.header.private_space": "🔒 Private space", + "space_lobby.header.member_one": "1 member", + "space_lobby.header.member_n": "{count} members", + "space_lobby.header.button.new_room": "New Room", + "space_lobby.header.button.invite": "Invite", + "space_lobby.status.loading_rooms_spaces": "Loading rooms and spaces...", + "space_lobby.status.no_rooms_spaces": "No rooms or spaces found.", + "space_lobby.status.loading": "Loading...", + "space_lobby.item.button.join": "Join", + "space_lobby.item.button.view": "View", + "space_lobby.item.button.leave": "Leave", + "space_lobby.item.state.joined": "✅ Joined", + "space_lobby.item.state.left": "Left", + "space_lobby.item.state.invited": "Invited", + "space_lobby.item.state.knocked": "Knocked", + "space_lobby.item.state.banned": "Banned", + "space_lobby.item.member_one": "1 member", + "space_lobby.item.member_n": "{count} members", + "space_lobby.item.child_room_one": "~{count} room", + "space_lobby.item.child_room_n": "~{count} rooms" +} diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json new file mode 100644 index 000000000..d170c0f38 --- /dev/null +++ b/resources/i18n/zh-CN.json @@ -0,0 +1,420 @@ +{ + "settings.all_settings_title": "全部设置", + "settings.category.account": "账号", + "settings.category.preferences": "偏好", + "settings.category.labs": "实验室", + "settings.preferences.language.title": "语言", + "settings.preferences.language.application_label": "应用语言", + "settings.preferences.language.reload_hint": "选择其他语言后,应用将重新加载", + "language.option.english": "English", + "language.option.chinese_simplified": "简体中文", + + "login.title.login_to_robrix": "登录 Robrix", + "login.title.create_account": "创建你的 Robrix 账号", + "login.input.user_id": "用户 ID", + "login.input.password": "密码", + "login.input.confirm_password": "确认密码", + "login.input.homeserver": "matrix.org", + "login.label.homeserver_optional": "Homeserver URL(可选)", + "login.button.login": "登录", + "login.button.create_account": "创建账号", + "login.sso.prompt": "或者,使用 SSO 提供商登录:", + "login.account_prompt.no_account": "还没有账号?", + "login.account_prompt.already_have": "已经有账号了?", + "login.mode_toggle.sign_up_here": "去注册", + "login.mode_toggle.back_to_login": "返回登录", + "login.status.missing_user_id.title": "缺少用户 ID", + "login.status.missing_user_id.body": "请输入有效的用户 ID。", + "login.status.missing_password.title": "缺少密码", + "login.status.missing_password.body": "请输入有效密码。", + "login.status.password_mismatch.title": "两次密码不一致", + "login.status.password_mismatch.body": "请在两个密码输入框中输入相同的密码。", + "login.status.creating_account.title": "正在创建账号...", + "login.status.creating_account.body": "正在等待服务器创建你的账号...", + "login.status.logging_in.title": "正在登录...", + "login.status.logging_in.body": "正在等待登录响应...", + "login.status.logging_in_cli.title": "正在通过命令行自动登录...", + "login.status.auto_logging_in_as_user": "正在以用户 {user_id} 自动登录...", + "login.status.account_creation_failed": "账号创建失败。", + "login.status.login_failed": "登录失败。", + "login.status.okay": "确定", + "login.status.cancel": "取消", + "login_status_modal.title": "登录状态", + "login_status_modal.button.cancel": "取消", + + "room_context_menu.button.mark_unread": "标记为未读", + "room_context_menu.button.mark_read": "标记为已读", + "room_context_menu.button.favorite": "收藏", + "room_context_menu.button.unfavorite": "取消收藏", + "room_context_menu.button.set_low_priority": "设为低优先级", + "room_context_menu.button.unset_low_priority": "取消低优先级", + "room_context_menu.button.copy_link_to_room": "复制房间链接", + "room_context_menu.button.settings": "设置", + "room_context_menu.button.notifications": "通知", + "room_context_menu.button.invite": "邀请", + "room_context_menu.button.bind_botfather": "绑定 BotFather", + "room_context_menu.button.unbind_botfather": "解绑 BotFather", + "room_context_menu.button.leave_room": "离开房间", + "room_context_menu.popup.settings_not_implemented": "房间设置页面暂未实现。", + "room_context_menu.popup.notifications_not_implemented": "房间通知页面暂未实现。", + "room_context_menu.popup.removing_botfather": "正在将 BotFather {bot_user_id} 从该房间移除...", + "room_context_menu.popup.inviting_botfather": "正在邀请 BotFather {bot_user_id} 加入该房间...", + "room_context_menu.popup.bot_settings_unavailable": "当前无法获取机器人设置。", + + "add_room.title": "添加/探索房间与空间", + "add_room.section.create_new_room": "创建新房间:", + "add_room.section.add_friend": "添加好友:", + "add_room.section.join_existing": "加入已有房间或空间:", + "add_room.create_room.help.default": "你可以创建独立房间,或将房间创建到你有权限的空间下。", + "add_room.create_room.help.fixed_parent": "输入房间名后,将直接在当前空间中创建该房间。", + "add_room.create_room.dropdown.no_space": "不放入任何空间", + "add_room.create_room.dropdown.hint.choose_space": "选择一个你有权限创建子房间的空间。", + "add_room.create_room.dropdown.hint.no_creatable_spaces": "当前没有你可创建子房间的已加入空间。", + "add_room.create_room.dropdown.hint.new_room_under": "新房间将创建在:{selected_name}", + "add_room.create_room.dropdown.hint.default": "可创建独立房间,或在下拉框中选择一个空间。", + "add_room.create_room.input.placeholder": "输入新房间名称...", + "add_room.create_room.button.create": "创建房间", + "add_room.create_room.button.syncing": "同步中...", + "add_room.create_room.modal.title": "创建新房间", + "add_room.create_room.modal.subtitle": "在当前选中的空间中直接创建一个新房间。", + "add_room.button.cancel": "取消", + "add_room.add_friend.help": "输入 Matrix 用户 ID 以打开或创建一个私聊房间。", + "add_room.add_friend.input.placeholder": "输入 Matrix 用户 ID,例如 @alice:matrix.org...", + "add_room.add_friend.button": "添加好友", + "add_room.join.input.placeholder": "输入别名、ID 或 Matrix 链接...", + "add_room.join.button.go": "前往", + "add_room.join.help_html": "

你可以使用以下任一种方式输入房间/空间地址:

  • # 开头的别名,例如 #robrix:matrix.org
  • ! 开头的ID,例如 !moVNEIUPxJZpxRHDUv:matrix.org
  • Matrix 链接,例如 https:matrix.to/...matrix:...
", + "add_room.popup.cannot_add_self": "你不能把自己添加为好友。", + "add_room.popup.invalid_user_id": "无效的 Matrix 用户 ID。\n\n错误:{error}", + "add_room.popup.parse_error": "无法将输入解析为有效的房间地址。\n错误:{error}。", + "add_room.popup.fetch_error": "获取房间信息失败。\n\n错误:{error}。", + "add_room.popup.knock_success": "已成功向{room_type} {room_name} 发起敲门请求。", + "add_room.popup.knock_failed": "敲门请求失败。\n\n错误:{error}。", + "add_room.popup.join_success": "已成功加入{room_type} {room_name}。", + "add_room.popup.join_failed": "加入房间失败。\n\n错误:{error}。", + "add_room.popup.created_room_success": "已成功创建房间“{room_name}”。", + "add_room.popup.created_room_space_link_suffix": "\n\n房间已创建,但无法关联到所选空间。\n错误:{error}", + "add_room.popup.create_room_failed": "创建房间“{room_name}”失败。\n\n错误:{error}", + "add_room.feedback.create_room_failed": "创建房间失败:{error}", + "add_room.feedback.creating_room": "正在创建房间...", + "add_room.feedback.room_created_syncing": "房间已创建,正在同步到该空间...", + "add_room.feedback.room_created_link_failed_opening": "房间已创建,但关联到空间失败,正在打开房间...", + "add_room.feedback.room_created_opening": "房间已创建,正在打开房间...", + "add_room.loading.fetching": "正在获取 {target}...", + "add_room.fetched.room_name.unnamed": "未命名{room_or_space_uc},ID:{room_id}", + "add_room.fetched.main_alias_and_id": "主要{room_or_space_uc}别名与 ID", + "add_room.fetched.alias.not_set": "未设置", + "add_room.fetched.alias": "别名:{alias}", + "add_room.fetched.id": "ID:{room_id}", + "add_room.fetched.topic_title": "{room_or_space_uc}主题", + "add_room.fetched.topic.not_set_html": "未设置主题", + "add_room.summary.already_joined": "你已经加入此{room_or_space_lc}。", + "add_room.summary.banned": "你已被此{room_or_space_lc}封禁。", + "add_room.summary.already_invited": "你已被邀请到此{room_or_space_lc}。", + "add_room.summary.already_knocked": "你已经向此{room_or_space_lc}敲门过。", + "add_room.summary.previously_left": "你之前离开了此{room_or_space_lc}。", + "add_room.summary.member_count": "这是一个{directness}{room_or_space_lc},共有 {num_members} 位{member_word}。", + "add_room.summary.knocked_waiting": "你已向此{room_or_space_lc}敲门,正在等待对方邀请你加入。", + "add_room.summary.joined_loading": "你已加入此{room_or_space_lc}。正在从服务器加载,请稍候...", + "add_room.summary.loaded": "你已{verb}此{room_or_space_lc}。", + "add_room.button.go_to": "前往{room_or_space_lc}", + "add_room.button.cannot_join_until_unbanned": "解除封禁后才能加入", + "add_room.button.go_to_invitation": "前往邀请", + "add_room.button.knock_again": "再次敲门(礼貌点)", + "add_room.button.rejoin": "重新加入此{room_or_space_lc}", + "add_room.button.rejoin_requires_invite": "重新加入{room_or_space_lc}需要邀请", + "add_room.button.knock_to_rejoin": "敲门以重新加入{room_or_space_lc}", + "add_room.button.rejoin_requires_other_membership": "重新加入{room_or_space_lc}需要邀请或其他房间成员身份", + "add_room.button.not_allowed_to_rejoin": "不允许重新加入此{room_or_space_lc}", + "add_room.button.join": "加入此{room_or_space_lc}", + "add_room.button.join_requires_invite": "加入{room_or_space_lc}需要邀请", + "add_room.button.knock_to_join": "敲门以加入{room_or_space_lc}", + "add_room.button.join_requires_other_membership": "加入{room_or_space_lc}需要邀请或其他房间成员身份", + "add_room.button.not_allowed_to_join": "不允许加入此{room_or_space_lc}", + "add_room.button.successfully_knocked": "已成功敲门!", + "add_room.button.successfully_joined": "已成功加入!", + "add_room.button.go_to_loaded": "前往{adj}{room_or_space_lc}", + "add_room.word.direct": "私聊", + "add_room.word.regular": "普通", + "add_room.word.member": "成员", + "add_room.word.members": "成员", + "add_room.word.room_lc": "房间", + "add_room.word.space_lc": "空间", + "add_room.word.room_uc": "房间", + "add_room.word.space_uc": "空间", + "add_room.word.verb.invited": "被邀请到", + "add_room.word.verb.joined": "完整加入", + "add_room.word.adj.invited": "受邀", + "add_room.word.adj.joined": "已加入", + + "settings.account.title": "账号设置", + "settings.account.section.your_avatar": "你的头像:", + "settings.account.section.your_display_name": "你的显示名称:", + "settings.account.section.your_user_id": "你的用户 ID:", + "settings.account.section.other_actions": "其他操作:", + "settings.account.display_name.placeholder": "添加显示名称...", + "settings.account.user_id.not_logged_in": "你尚未登录。", + "settings.account.button.upload_avatar": "上传头像", + "settings.account.button.delete_avatar": "删除头像", + "settings.account.button.cancel": "取消", + "settings.account.button.save_name": "保存名称", + "settings.account.button.manage_account": "管理账号", + "settings.account.button.log_out": "退出登录", + "settings.account.button.logging_out": "正在退出登录...", + "settings.account.tooltip.copy_user_id": "复制用户 ID", + "settings.account.popup.avatar_updated": "头像更新成功。", + "settings.account.popup.avatar_deleted": "头像删除成功。", + "settings.account.popup.avatar_upload_not_implemented": "头像上传功能暂未实现。", + "settings.account.popup.deleting_avatar": "正在删除你的头像...", + "settings.account.popup.display_name_updated": "显示名称更新成功。", + "settings.account.popup.display_name_removed": "显示名称已移除。", + "settings.account.popup.uploading_display_name": "正在上传新的显示名称...", + "settings.account.popup.copied_user_id": "已将你的用户 ID 复制到剪贴板。", + "settings.account.popup.account_management_not_implemented": "账号管理功能暂未实现。", + "settings.account.modal.delete_avatar.title": "删除头像", + "settings.account.modal.delete_avatar.body": "你确定要删除你的头像吗?", + "settings.account.modal.delete_avatar.accept": "删除", + + "settings.labs.app_service.title": "应用服务", + "settings.labs.app_service.description": "在这里启用 Matrix 应用服务支持。Robrix 仍然是普通 Matrix 客户端:它会把 BotFather 绑定到房间,并发送对应的斜杠命令。", + "settings.labs.app_service.enable_label": "启用应用服务", + "settings.labs.app_service.botfather_user_id": "BotFather 用户 ID:", + "settings.labs.app_service.botfather_placeholder": "bot 或 @bot:server", + "settings.labs.app_service.button.enable": "启用应用服务", + "settings.labs.app_service.button.disable": "禁用应用服务", + "settings.labs.app_service.button.save": "保存", + "settings.labs.app_service.popup.saved": "已保存 Matrix 应用服务设置。", + + "invite_screen.message.invited_by": "邀请你加入:", + "invite_screen.message.invited_generic": "你被邀请加入:", + "invite_screen.button.reject": "拒绝邀请", + "invite_screen.button.join": "加入房间", + "invite_screen.button.joining": "加入中...", + "invite_screen.button.rejecting": "拒绝中...", + "invite_screen.button.joined": "已加入!", + "invite_screen.popup.joined_success": "已成功加入房间。", + "invite_screen.popup.rejected_success": "已成功拒绝邀请。", + "invite_screen.popup.reject_failed": "拒绝邀请失败:{error}", + "invite_screen.completion.rejected": "已成功拒绝邀请。你现在可以关闭该邀请页面。", + "invite_modal.title.invite_to_room_name": "邀请加入 {room_name}", + "invite_modal.input.placeholder": "@user:example.org", + "invite_modal.button.cancel": "取消", + "invite_modal.button.invite": "邀请", + "invite_modal.button.okay": "确定", + "invite_modal.status.enter_user_id": "请输入用户 ID。", + "invite_modal.status.sending": "正在发送邀请...", + "invite_modal.status.invalid_user_id": "无效的用户 ID。应为格式:@user:server.xyz", + "invite_modal.status.success_invited": "已成功邀请 {user_id}!", + "invite_modal.status.send_failed": "发送邀请失败:{error}", + "rooms_list_entry.invited.by_name_and_user": "由 {display_name} ({user_id}) 邀请", + "rooms_list_entry.invited.by_user": "由 {user_id} 邀请", + "rooms_list_entry.invited.generic": "你收到了邀请", + + "loading_pane.title.default": "正在加载内容...", + "loading_pane.title.searching_older": "正在搜索更早的消息...", + "loading_pane.status.searching_event": "正在查找事件 {target_event_id}\n\n目前已拉取 {events_paginated} 条消息...", + "loading_pane.title.error": "内容加载失败", + "loading_pane.button.cancel": "取消", + "loading_pane.button.okay": "确定", + + "rooms_list_header.title.all_rooms": "全部房间", + "rooms_list_header.popup.offline": "无法连接 Matrix 服务器,请检查网络连接。", + "rooms_list_header.tooltip.syncing": "同步中...", + "rooms_list_header.tooltip.offline": "离线", + "rooms_list_header.tooltip.synced": "已完全同步", + + "room_filter_input.placeholder": "筛选房间与空间...", + "search_messages.button.todo": "搜索(待实现)", + + "welcome_screen.title": "欢迎来到 Robrix!", + "welcome_screen.body_html": "

我们的 Matrix 客户端仍在快速开发中。目前,你可以访问你在其他客户端中已加入的房间和空间。


不过别担心,我们正在持续扩展 Robrix 的功能!


欢迎在我们的 Matrix 频道查看最新公告:

#robrix:matrix.org

", + + "room_screen.bot.delete.error.empty_user_id": "请输入要删除的机器人 Matrix 用户 ID。", + "room_screen.bot.delete.error.invalid_user_id": "无效的 Matrix 用户 ID:{full_user_id}", + "room_screen.bot.delete.error.current_user_unavailable": "当前用户 ID 不可用,无法解析机器人的 homeserver。", + "room_screen.tooltip.reacted_with_suffix": " 反应:{reaction}", + "room_screen.modal.invite.title": "发送邀请", + "room_screen.modal.invite.body": "确认要邀请 {username} 加入这个房间吗?", + "room_screen.modal.invite.accept": "邀请", + "room_screen.popup.invite.sent_success": "邀请已发送。", + "room_screen.popup.invite.failed": "发送邀请失败。\n\n错误:{error}", + "room_screen.popup.app_service.enable_before_create": "请先启用 App Service,再在房间中创建机器人。", + "room_screen.popup.app_service.bind_before_create": "请先将 BotFather 绑定到此房间,再创建机器人。", + "room_screen.popup.app_service.state_unavailable_create": "应用状态当前不可用,暂时无法创建机器人。", + "room_screen.popup.app_service.enable_before_delete": "请先启用 App Service,再在房间中删除机器人。", + "room_screen.popup.app_service.bind_before_delete": "请先将 BotFather 绑定到此房间,再删除机器人。", + "room_screen.popup.app_service.state_unavailable_delete": "应用状态当前不可用,暂时无法删除机器人。", + "room_screen.popup.app_service.room_not_bound": "该房间当前未绑定 BotFather。", + "room_screen.popup.app_service.removing_botfather": "正在将 BotFather {bot_user_id} 从该房间移除...", + "room_screen.popup.app_service.state_unavailable_unbind": "应用状态当前不可用,无法从该房间移除 BotFather。", + "room_screen.popup.bot.main_timeline_only": "机器人命令仅支持在主房间时间线中使用。", + "room_screen.popup.bot.enable_in_settings_before_bot": "使用 /bot 前请先在设置中启用 App Service。", + "room_screen.popup.bot.bind_before_bot": "使用 /bot 前请先将 BotFather 绑定到此房间。", + "room_screen.popup.bot.enable_before_commands": "在房间中使用 BotFather 命令前请先启用 App Service。", + "room_screen.popup.bot.bind_before_commands": "使用 BotFather 命令前请先将 BotFather 绑定到此房间。", + "room_screen.popup.bot.creation_main_timeline_only": "创建机器人命令仅支持在主房间时间线中使用。", + "room_screen.popup.bot.sent_listbots": "已向 BotFather 发送 `/listbots`。", + "room_screen.popup.bot.sent_bothelp": "已向 BotFather 发送 `/bothelp`。", + "room_screen.popup.bot.sent_createbot": "已向 BotFather 发送 `/createbot`(`{username}`)。", + "room_screen.popup.bot.sent_deletebot": "已向 BotFather 发送 `/deletebot`({matrix_user_id})。", + "room_screen.popup.bot.state_unavailable_create_command": "应用状态当前不可用,未发送创建机器人命令。", + "room_screen.popup.bot.state_unavailable_delete_command": "应用状态当前不可用,未发送删除机器人命令。", + "room_screen.fallback.unnamed_room": "未命名房间", + "room_screen.unsupported.prefix": "[不支持]", + "room_screen.read_marker.new_messages": "新消息", + "room_screen.top_space.loading_earlier": "正在加载更早的消息...", + "room_screen.loading.found_related_message": "已成功找到被回复的消息!", + "room_screen.loading.related_message_not_found": "未找到关联消息,可能已被删除。", + "room_screen.popup.pin.pinned_success": "已成功置顶事件。", + "room_screen.popup.pin.unpinned_success": "已成功取消置顶事件。", + "room_screen.popup.pin.already_pinned": "该消息已置顶。", + "room_screen.popup.pin.already_unpinned": "该消息尚未置顶。", + "room_screen.popup.pin.pin_failed": "置顶事件失败。错误:{error}", + "room_screen.popup.pin.unpin_failed": "取消置顶事件失败。错误:{error}", + "room_screen.popup.already_viewing_room": "你已经在查看这个房间了。", + "room_screen.popup.open_url_failed": "无法打开 URL:{url}", + "room_screen.popup.message.reply_not_found": "在时间线中找不到要回复的消息,请重试。", + "room_screen.popup.message.edit_not_found": "在时间线中找不到要编辑的消息,请重试。", + "room_screen.popup.message.no_recent_editable": "没有可编辑的近期消息,请手动选择一条消息进行编辑。", + "room_screen.popup.message.cannot_pin": "该事件无法置顶。", + "room_screen.popup.message.cannot_unpin": "该事件无法取消置顶。", + "room_screen.popup.message.copy_text_not_found": "在时间线中找不到可复制文本的消息,请重试。", + "room_screen.popup.message.copy_html_not_found": "在时间线中找不到可复制 HTML 的消息,请重试。", + "room_screen.popup.message.copy_link_failed": "无法创建消息永久链接,请重试。", + "room_screen.popup.message.view_source_not_found": "在时间线中找不到要查看源码的消息。", + "room_screen.popup.message.related_not_found": "在时间线中找不到关联消息或事件。", + "room_screen.modal.delete_message.title": "删除消息", + "room_screen.modal.delete_message.body": "确认要删除这条消息吗?此操作无法撤销。", + "room_screen.modal.delete_message.accept": "删除", + "room_screen.server_notice.title": "服务器通知:", + "room_screen.server_notice.notice_type": "通知类型", + "room_screen.server_notice.limit_type": "限制类型", + "room_screen.server_notice.admin_contact": "管理员联系方式", + "room_screen.server_notice.username": "服务器通知", + "room_screen.verification.sent_prefix": "已发送", + "room_screen.verification.request": "验证请求", + "room_screen.verification.sent_to_suffix": " 给 {user_id}。", + "room_screen.verification.supported_methods": "支持的方法", + "room_screen.image.unsupported_type": "{body}\n\n不支持的类型:{mime}", + "room_screen.image.failed_to_display": "{body}\n\n显示图片失败:{error}", + "room_screen.image.failed_to_fetch": "{body}\n\n从 {mxc_uri} 获取图片失败", + "room_screen.image.encrypted_todo": "{body}\n\n[TODO] 获取加密图片:{url}", + "room_screen.image.no_source_url": "{body}\n\n图片消息缺少来源 URL。", + "room_screen.file.preview_html": "{filename}{size}{caption}
暂不支持文件下载。", + "room_screen.audio.preview_html": "音频:{filename}{mime}{duration}{size}{caption}
暂不支持音频播放。", + "room_screen.video.preview_html": "视频:{filename}{mime}{duration}{size}{dimensions}{caption}
暂不支持视频播放。", + "room_screen.location.label": "位置:", + "room_screen.location.open_osm": "在 OpenStreetMap 中打开", + "room_screen.location.open_google_maps": "在 Google 地图中打开", + "room_screen.location.open_apple_maps": "在 Apple 地图中打开", + "room_screen.location.invalid_html": "[位置无效] {body}", + "room_screen.redacted.self_with_reason": "⛔ 删除了自己的消息。原因:“{reason}”。", + "room_screen.redacted.self": "⛔ 删除了自己的消息。", + "room_screen.redacted.other_with_reason": "⛔ {redactor} 删除了这条消息。原因:“{reason}”。", + "room_screen.redacted.other": "⛔ {redactor} 删除了这条消息。", + "room_screen.redacted.generic": "⛔ 消息已删除。", + "room_screen.reply_preview.error_username": "[获取用户名失败]", + "room_screen.reply_preview.error_event": "[获取被回复事件失败]", + "room_screen.reply_preview.loading_username": "[正在加载用户名...]", + "room_screen.reply_preview.loading_event": "[正在加载被回复消息...]", + "room_screen.thread_summary.loading_latest_reply": "正在加载最新回复...", + "room_screen.thread_summary.error_latest_reply": "无法加载最新回复", + "room_screen.thread_summary.one_reply": "1 条回复", + "room_screen.thread_summary.n_replies": "{n} 条回复", + "room_screen.small_state.invite_to_room": "邀请加入房间", + "room_screen.app_service.sender_name": "BotFather", + "room_screen.app_service.sender_tag": "机器人", + "room_screen.app_service.title": "App Service 操作", + "room_screen.app_service.subtitle": "通过 BotFather 创建机器人。Robrix 只会发送对应的斜杠命令。", + "room_screen.app_service.timestamp_now": "刚刚", + "room_screen.app_service.button.create_bot": "创建机器人", + "room_screen.app_service.button.list_bots": "列出机器人", + "room_screen.app_service.button.delete_bot": "删除机器人", + "room_screen.app_service.button.bot_help": "Bot 帮助", + "room_screen.app_service.button.unbind": "解绑", + + "spaces_bar.tooltip.unknown_space_name": "未知空间名称", + "spaces_bar.status.none_matching": "未找到\n匹配的空间。", + "spaces_bar.status.none_joined": "未找到\n已加入的空间。", + "spaces_bar.status.one_matching": "找到 1 个\n匹配的空间。", + "spaces_bar.status.one_joined": "找到 1 个\n已加入的空间。", + "spaces_bar.status.n_matching": "找到 {count} 个\n匹配的空间。", + "spaces_bar.status.n_joined": "找到 {count} 个\n已加入的空间。", + "spaces_bar.status.many_matching": "找到 99+ 个\n匹配的空间。", + "spaces_bar.status.many_joined": "找到 99+ 个\n已加入的空间。", + + "tsp.settings.title": "TSP 钱包设置", + "tsp.settings.section.active_identity": "当前活跃身份:", + "tsp.settings.section.wallets": "你的钱包:", + "tsp.settings.wallet.none": "未找到钱包。请创建或导入钱包。", + "tsp.settings.identity.none_set": "尚未设置默认身份。", + "tsp.settings.button.republish_identity": "重新发布当前身份到 DID 服务器", + "tsp.settings.button.republishing_now": "正在重新发布 DID...", + "tsp.settings.button.create_identity": "创建新身份(DID)", + "tsp.settings.button.create_wallet": "创建新钱包", + "tsp.settings.button.import_wallet": "导入已有钱包", + "tsp.settings.popup.wallet.removed": "已移除钱包“{wallet_name}”。", + "tsp.settings.popup.wallet.default_removed_warning": "默认钱包已被移除。\n\n在重新设置默认钱包前,TSP 功能将无法正常工作。", + "tsp.settings.popup.wallet.set_default_failed": "设置默认钱包失败,找不到或无法打开所选钱包。", + "tsp.settings.popup.wallet.open_failed": "打开钱包失败:{error}", + "tsp.settings.popup.wallet.import_not_implemented": "导入已有钱包功能暂未实现。", + "tsp.settings.popup.wallet.none_found": "未找到 TSP 钱包。\n\n请创建或导入钱包。", + "tsp.settings.popup.wallet.no_default": "尚未设置默认 TSP 钱包。\n\n请选择或创建一个默认钱包。", + "tsp.settings.popup.identity.republish_success": "已成功将身份“{did}”重新发布到 DID 服务器。", + "tsp.settings.popup.identity.republish_failed": "重新发布身份到 DID 服务器失败:{error}", + "tsp.settings.popup.identity.copied": "已将默认 TSP 身份复制到剪贴板。", + "tsp.settings.popup.identity.none_set": "尚未设置默认 TSP 身份。", + "tsp.settings.popup.identity.must_set_default": "必须先设置默认 TSP 身份,才能重新发布。", + "tsp_dummy.message.disabled": "当前构建未包含 TSP 功能。\n如需使用 TSP,请使用启用 'tsp' feature 的方式构建 Robrix。", + "tsp.wallet_entry.default_label": "✅ 默认", + "tsp.wallet_entry.not_found": "未找到钱包!", + "tsp.wallet_entry.button.set_default": "设为默认", + "tsp.wallet_entry.button.remove": "从列表移除", + "tsp.wallet_entry.button.delete": "删除钱包", + "tsp.wallet_entry.modal.remove.title": "移除钱包", + "tsp.wallet_entry.modal.remove.body": "确认要将钱包“{wallet_name}”从列表中移除吗?\n\n这不会删除实际的钱包文件。", + "tsp.wallet_entry.modal.remove.accept": "移除", + "tsp.wallet_entry.popup.delete_not_implemented": "删除钱包功能暂未实现。", + + "app.room_filter.search_results_title": "搜索结果", + "app.room_filter.empty_hint": "输入关键词以搜索房间和空间...", + "app.room_filter.no_local_results": "本地未找到“{keywords}”相关结果。请选择下方类型以搜索服务器。", + "app.room_filter.searching_remote": "正在服务器上搜索{kind}...", + "app.room_filter.remote.people": "联系人", + "app.room_filter.remote.rooms": "房间", + "app.room_filter.remote.spaces": "空间", + "app.room_filter.remote.kind.people": "联系人", + "app.room_filter.remote.kind.rooms": "房间", + "app.room_filter.remote.kind.spaces": "空间", + + "rooms_list.category.invites": "邀请", + "rooms_list.category.favorites": "收藏", + "rooms_list.category.rooms": "房间", + "rooms_list.category.people": "联系人", + "rooms_list.category.low_priority": "低优先级", + "rooms_list.category.left_rooms": "已离开房间", + + "space_lobby.entry.explore_space": "探索此空间", + "space_lobby.header.welcome": "欢迎来到此空间:", + "space_lobby.header.public_space": "🌐 公开空间", + "space_lobby.header.private_space": "🔒 私有空间", + "space_lobby.header.member_one": "1 位成员", + "space_lobby.header.member_n": "{count} 位成员", + "space_lobby.header.button.new_room": "新建房间", + "space_lobby.header.button.invite": "邀请", + "space_lobby.status.loading_rooms_spaces": "正在加载房间和空间...", + "space_lobby.status.no_rooms_spaces": "未找到房间或空间。", + "space_lobby.status.loading": "加载中...", + "space_lobby.item.button.join": "加入", + "space_lobby.item.button.view": "查看", + "space_lobby.item.button.leave": "离开", + "space_lobby.item.state.joined": "✅ 已加入", + "space_lobby.item.state.left": "已离开", + "space_lobby.item.state.invited": "已邀请", + "space_lobby.item.state.knocked": "已敲门", + "space_lobby.item.state.banned": "已封禁", + "space_lobby.item.member_one": "1 位成员", + "space_lobby.item.member_n": "{count} 位成员", + "space_lobby.item.child_room_one": "~{count} 个房间", + "space_lobby.item.child_room_n": "~{count} 个房间" +} diff --git a/src/app.rs b/src/app.rs index e2e72e28c..5e0f17913 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,7 +12,7 @@ use crate::{ avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt}, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef - }, join_leave_room_modal::{ + }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, @@ -186,7 +186,7 @@ script_mod! { width: Fill, height: Fit, margin: Inset{left: 4, top: 2} - text: "Search Results" + text: "" draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 10} @@ -208,7 +208,7 @@ script_mod! { width: Fill, height: Fit, flow: Flow.Right{wrap: true}, - text: "Type to search rooms and spaces..." + text: "" draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} @@ -225,15 +225,15 @@ script_mod! { remote_search_people_button := RobrixNeutralIconButton { width: Fit, - text: "People" + text: "" } remote_search_rooms_button := RobrixNeutralIconButton { width: Fit, - text: "Rooms" + text: "" } remote_search_spaces_button := RobrixNeutralIconButton { width: Fit, - text: "Spaces" + text: "" } } @@ -626,6 +626,7 @@ impl MatchEvent for App { } self.update_login_visibility(cx); + self.sync_app_language(cx); log!("App::Startup: starting matrix sdk loop"); let _tokio_rt_handle = crate::sliding_sync::start_matrix_tokio().unwrap(); @@ -650,6 +651,8 @@ impl MatchEvent for App { } fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { + self.sync_app_language(cx); + let invite_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); if let Some(_accepted) = invite_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(invite_confirmation_modal)).close(cx); @@ -707,13 +710,14 @@ impl MatchEvent for App { let query = room_filter_input.text().trim().to_owned(); if !query.is_empty() { let kind_text = match &kind { - RemoteDirectorySearchKind::People => "people", - RemoteDirectorySearchKind::Rooms => "rooms", - RemoteDirectorySearchKind::Spaces => "spaces", + RemoteDirectorySearchKind::People => tr_key(self.app_state.app_language, "app.room_filter.remote.kind.people"), + RemoteDirectorySearchKind::Rooms => tr_key(self.app_state.app_language, "app.room_filter.remote.kind.rooms"), + RemoteDirectorySearchKind::Spaces => tr_key(self.app_state.app_language, "app.room_filter.remote.kind.spaces"), }; + let searching_text = tr_fmt(self.app_state.app_language, "app.room_filter.searching_remote", &[("kind", kind_text)]); self.set_room_filter_modal_empty_state( cx, - &format!("Searching {} on server...", kind_text), + &searching_text, false, ); submit_async_request(MatrixRequest::SearchDirectory { @@ -881,7 +885,7 @@ impl MatchEvent for App { if let RoomsListAction::OpenRoomContextMenu { details, pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); - let expected_dimensions = room_context_menu.show(cx, details); + let expected_dimensions = room_context_menu.show(cx, details, self.app_state.app_language); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(pos.x, rect.size.x - expected_dimensions.x); @@ -1165,7 +1169,7 @@ impl MatchEvent for App { // Handle InviteModalAction to open/close the invite modal. match action.downcast_ref() { Some(InviteModalAction::Open(room_name_id)) => { - self.ui.invite_modal(cx, ids!(invite_modal_inner)).show(cx, room_name_id.clone()); + self.ui.invite_modal(cx, ids!(invite_modal_inner)).show(cx, room_name_id.clone(), self.app_state.app_language); self.ui.modal(cx, ids!(invite_modal)).open(cx); continue; } @@ -1383,6 +1387,20 @@ impl App { live_id!(result_item_6), live_id!(result_item_7), ]; + fn sync_app_language(&self, cx: &mut Cx) { + let app_language = self.app_state.app_language; + self.ui.label(cx, ids!(room_filter_modal_inner.search_results_title)) + .set_text(cx, tr_key(app_language, "app.room_filter.search_results_title")); + self.ui.label(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_empty)) + .set_text(cx, tr_key(app_language, "app.room_filter.empty_hint")); + self.ui.button(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options.remote_search_people_button)) + .set_text(cx, tr_key(app_language, "app.room_filter.remote.people")); + self.ui.button(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options.remote_search_rooms_button)) + .set_text(cx, tr_key(app_language, "app.room_filter.remote.rooms")); + self.ui.button(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options.remote_search_spaces_button)) + .set_text(cx, tr_key(app_language, "app.room_filter.remote.spaces")); + } + fn open_join_from_search_result( &mut self, cx: &mut Cx, @@ -1597,13 +1615,17 @@ impl App { if keywords.is_empty() { self.set_room_filter_modal_empty_state( cx, - "Type to search rooms and spaces...", + tr_key(self.app_state.app_language, "app.room_filter.empty_hint"), false, ); } else if self.room_filter_modal_results.is_empty() { self.set_room_filter_modal_empty_state( cx, - &format!("No local results for \"{}\". Choose a type below to search server.", keywords), + &tr_fmt( + self.app_state.app_language, + "app.room_filter.no_local_results", + &[("keywords", keywords)], + ), true, ); } else { @@ -1788,6 +1810,7 @@ impl App { /// App-wide state that is stored persistently across multiple app runs /// and shared/updated across various parts of the app. #[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[serde(default)] pub struct AppState { /// The currently-selected room, which is highlighted (selected) in the RoomsList /// and considered "active" in the main rooms screen. @@ -1808,6 +1831,8 @@ pub struct AppState { pub saved_dock_state_per_space: HashMap, /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, + /// The preferred app language. + pub app_language: AppLanguage, /// Local configuration and UI state for bot-assisted room binding. pub bot_settings: BotSettingsState, } diff --git a/src/home/add_room.rs b/src/home/add_room.rs index a6a32edc7..bac89be74 100644 --- a/src/home/add_room.rs +++ b/src/home/add_room.rs @@ -6,8 +6,9 @@ use matrix_sdk::RoomState; use ruma::{IdParseError, MatrixToUri, MatrixUri, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, matrix_uri::MatrixId, room::{JoinRuleSummary, RoomType}}; use crate::{ - app::AppStateAction, + app::{AppState, AppStateAction}, home::{invite_screen::JoinRoomResultAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef}, + i18n::{AppLanguage, tr_fmt, tr_key}, profile::user_profile::UserProfile, room::{BasicRoomDetails, FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{ @@ -158,7 +159,7 @@ script_mod! { LineH { padding: 10, margin: Inset{top: 10, right: 2} } - SubsectionLabel { + create_new_room_label := SubsectionLabel { margin: Inset{top: 8} text: "Create a new room:" } @@ -167,7 +168,7 @@ script_mod! { LineH { padding: 10, margin: Inset{right: 2} } - SubsectionLabel { + add_friend_label := SubsectionLabel { margin: Inset{top: 4} text: "Add a friend:" } @@ -210,7 +211,7 @@ script_mod! { LineH { padding: 10, margin: Inset{right: 2} } - SubsectionLabel { + join_existing_label := SubsectionLabel { text: "Join an existing room or space:" } @@ -513,6 +514,7 @@ pub struct AddRoomScreen { /// The function to perform when the user clicks the `join_room_button`. #[rust(JoinButtonFunction::None)] join_function: JoinButtonFunction, #[rust(false)] adding_friend: bool, + #[rust] app_language: AppLanguage, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -615,10 +617,17 @@ pub struct CreateRoomForm { #[rust(Vec::new())] creatable_spaces: Vec, #[rust(None)] preferred_parent_space_id: Option, #[rust(None)] fixed_parent_space_id: Option, + #[rust] app_language: AppLanguage, } impl Widget for CreateRoomForm { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } @@ -641,6 +650,7 @@ impl Widget for CreateRoomForm { &create_room_space_hint, &self.creatable_spaces, selected_space_id.as_ref(), + self.app_language, ); self.sync_mode_views(cx); @@ -671,6 +681,7 @@ impl WidgetMatchEvent for CreateRoomForm { &create_room_space_hint, &self.creatable_spaces, self.preferred_parent_space_id.as_ref(), + self.app_language, ); self.view.redraw(cx); } @@ -695,11 +706,14 @@ impl WidgetMatchEvent for CreateRoomForm { refresh_space_children(cx, space_id); } - let mut popup_message = format!("Successfully created room \"{}\".", room_name_id); + let room_name_text = room_name_id.to_string(); + let mut popup_message = tr_fmt(self.app_language, "add_room.popup.created_room_success", &[ + ("room_name", room_name_text.as_str()), + ]); let popup_kind = if let Some(link_error) = space_link_error { - popup_message.push_str(&format!( - "\n\nThe room was created, but it could not be linked into the selected space.\nError: {link_error}" - )); + popup_message.push_str(&tr_fmt(self.app_language, "add_room.popup.created_room_space_link_suffix", &[ + ("error", link_error.as_str()), + ])); PopupKind::Warning } else { PopupKind::Success @@ -720,9 +734,9 @@ impl WidgetMatchEvent for CreateRoomForm { } else { self.pending_created_room = Some(room_name_id.clone()); let feedback_text = match (parent_space_id.as_ref(), space_link_error.as_ref()) { - (Some(_), None) => "Room created. Syncing it into the space...", - (Some(_), Some(_)) => "Room created, but linking it into the space failed. Opening the room...", - (None, _) => "Room created. Opening the room...", + (Some(_), None) => tr_key(self.app_language, "add_room.feedback.room_created_syncing"), + (Some(_), Some(_)) => tr_key(self.app_language, "add_room.feedback.room_created_link_failed_opening"), + (None, _) => tr_key(self.app_language, "add_room.feedback.room_created_opening"), }; self.set_feedback(cx, feedback_text, true, false); } @@ -736,12 +750,23 @@ impl WidgetMatchEvent for CreateRoomForm { create_room_button.set_enabled(cx, !create_room_name_input.text().trim().is_empty()); self.set_feedback( cx, - &format!("Failed to create room: {error}"), + &{ + let error_text = error.to_string(); + tr_fmt(self.app_language, "add_room.feedback.create_room_failed", &[ + ("error", error_text.as_str()), + ]) + }, false, true, ); enqueue_popup_notification( - format!("Failed to create room \"{room_name}\".\n\nError: {error}"), + { + let error_text = error.to_string(); + tr_fmt(self.app_language, "add_room.popup.create_room_failed", &[ + ("room_name", room_name.as_str()), + ("error", error_text.as_str()), + ]) + }, PopupKind::Error, None, ); @@ -759,6 +784,7 @@ impl WidgetMatchEvent for CreateRoomForm { &create_room_space_hint, &self.creatable_spaces, self.preferred_parent_space_id.as_ref(), + self.app_language, ); self.sync_mode_views(cx); self.view.redraw(cx); @@ -795,6 +821,15 @@ impl CreateRoomForm { self.creating_room || self.pending_created_room.is_some() } + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view.text_input(cx, ids!(create_room_name_input)) + .set_empty_text(cx, tr_key(self.app_language, "add_room.create_room.input.placeholder").to_string()); + self.view.button(cx, ids!(create_room_button)) + .set_text(cx, tr_key(self.app_language, "add_room.create_room.button.create")); + self.sync_mode_views(cx); + } + fn set_feedback(&mut self, cx: &mut Cx, text: &str, show_spinner: bool, is_error: bool) { self.view.view(cx, ids!(create_room_feedback)).set_visible(cx, true); self.view.view(cx, ids!(create_room_feedback_spinner_wrap)) @@ -831,7 +866,7 @@ impl CreateRoomForm { ); self.creating_room = true; - self.set_feedback(cx, "Creating room...", true, false); + self.set_feedback(cx, tr_key(self.app_language, "add_room.feedback.creating_room"), true, false); submit_async_request(MatrixRequest::CreateRoom { room_name: room_name.to_owned(), parent_space_id, @@ -866,7 +901,7 @@ impl CreateRoomForm { } self.clear_feedback(cx); create_room_button.set_enabled(cx, !create_room_name_input.text().trim().is_empty()); - create_room_button.set_text(cx, "Create room"); + create_room_button.set_text(cx, tr_key(self.app_language, "add_room.create_room.button.create")); create_room_button.reset_hover(cx); sync_space_dropdown( @@ -875,6 +910,7 @@ impl CreateRoomForm { &create_room_space_hint, &self.creatable_spaces, self.preferred_parent_space_id.as_ref(), + self.app_language, ); self.sync_mode_views(cx); @@ -900,15 +936,20 @@ impl CreateRoomForm { self.view.view(cx, ids!(create_room_button_row)).set_visible(cx, !show_fixed_parent); let help_text = if show_fixed_parent { - "Enter a room name. It will be created directly in this space." + tr_key(self.app_language, "add_room.create_room.help.fixed_parent") } else { - "Create a standalone room, or attach it under a space where you can create child rooms." + tr_key(self.app_language, "add_room.create_room.help.default") }; self.view.label(cx, ids!(create_room_help)).set_text(cx, help_text); } } impl CreateRoomFormRef { + pub fn set_app_language(&self, cx: &mut Cx, app_language: AppLanguage) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_app_language(cx, app_language); + } + pub fn can_submit(&self, cx: &mut Cx) -> bool { self.borrow().is_some_and(|inner| inner.can_submit(cx)) } @@ -941,21 +982,38 @@ impl CreateRoomFormRef { #[derive(Script, ScriptHook, Widget)] pub struct CreateRoomModal { #[deref] view: View, + #[rust] app_language: AppLanguage, } impl Widget for CreateRoomModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } let create_room_form = self.view.create_room_form(cx, ids!(create_room_form)); let is_busy = create_room_form.is_busy(); let create_button = self.view.button(cx, ids!(create_button)); let can_submit = create_room_form.can_submit(cx); create_button.set_enabled(cx, can_submit); - create_button.set_text(cx, if is_busy { "Syncing..." } else { "Create room" }); + create_button.set_text(cx, if is_busy { + tr_key(self.app_language, "add_room.create_room.button.syncing") + } else { + tr_key(self.app_language, "add_room.create_room.button.create") + }); self.view.button(cx, ids!(cancel_button)).set_enabled(cx, !is_busy); self.view.draw_walk(cx, scope, walk) } @@ -981,14 +1039,32 @@ impl WidgetMatchEvent for CreateRoomModal { } impl CreateRoomModal { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view.label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "add_room.create_room.modal.title")); + self.view.label(cx, ids!(subtitle)) + .set_text(cx, tr_key(self.app_language, "add_room.create_room.modal.subtitle")); + self.view.button(cx, ids!(create_button)) + .set_text(cx, tr_key(self.app_language, "add_room.create_room.button.create")); + self.view.button(cx, ids!(cancel_button)) + .set_text(cx, tr_key(self.app_language, "add_room.button.cancel")); + self.view.create_room_form(cx, ids!(create_room_form)) + .set_app_language(cx, app_language); + self.view.redraw(cx); + } + pub fn show(&mut self, cx: &mut Cx, preferred_parent_space_id: Option) { + self.view.create_room_form(cx, ids!(create_room_form)) + .set_app_language(cx, self.app_language); self.view.create_room_form(cx, ids!(create_room_form)).prepare( cx, preferred_parent_space_id, CreateRoomContext::SpaceLobbyModal, true, ); - self.view.button(cx, ids!(create_button)).set_text(cx, "Create room"); + self.view.button(cx, ids!(create_button)) + .set_text(cx, tr_key(self.app_language, "add_room.create_room.button.create")); self.view.button(cx, ids!(create_button)).reset_hover(cx); self.view.button(cx, ids!(cancel_button)).reset_hover(cx); self.view.redraw(cx); @@ -1002,8 +1078,45 @@ impl CreateRoomModalRef { } } +impl AddRoomScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view.label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "add_room.title")); + self.view.label(cx, ids!(create_new_room_label)) + .set_text(cx, tr_key(self.app_language, "add_room.section.create_new_room")); + self.view.label(cx, ids!(add_friend_label)) + .set_text(cx, tr_key(self.app_language, "add_room.section.add_friend")); + self.view.label(cx, ids!(join_existing_label)) + .set_text(cx, tr_key(self.app_language, "add_room.section.join_existing")); + self.view.label(cx, ids!(add_friend_help)) + .set_text(cx, tr_key(self.app_language, "add_room.add_friend.help")); + self.view.html(cx, ids!(help_info)) + .set_text(cx, tr_key(self.app_language, "add_room.join.help_html")); + self.view.text_input(cx, ids!(friend_user_id_input)) + .set_empty_text(cx, tr_key(self.app_language, "add_room.add_friend.input.placeholder").to_string()); + self.view.text_input(cx, ids!(room_alias_id_input)) + .set_empty_text(cx, tr_key(self.app_language, "add_room.join.input.placeholder").to_string()); + self.view.button(cx, ids!(add_friend_button)) + .set_text(cx, tr_key(self.app_language, "add_room.add_friend.button")); + self.view.button(cx, ids!(search_for_room_button)) + .set_text(cx, tr_key(self.app_language, "add_room.join.button.go")); + self.view.button(cx, ids!(fetched_room_summary.buttons_view.cancel_button)) + .set_text(cx, tr_key(self.app_language, "add_room.button.cancel")); + self.view.create_room_form(cx, ids!(create_room_form)) + .set_app_language(cx, app_language); + self.view.redraw(cx); + } +} + impl Widget for AddRoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); if let Event::Actions(actions) = event { @@ -1032,7 +1145,7 @@ impl Widget for AddRoomScreen { Ok(user_id) => { if current_user_id().as_ref().is_some_and(|current| current == &user_id) { enqueue_popup_notification( - "You cannot add yourself as a friend.".to_string(), + tr_key(self.app_language, "add_room.popup.cannot_add_self").to_string(), PopupKind::Warning, Some(4.0), ); @@ -1050,8 +1163,11 @@ impl Widget for AddRoomScreen { } } Err(e) => { + let error_text = e.to_string(); enqueue_popup_notification( - format!("Invalid Matrix user ID.\n\nError: {e}"), + tr_fmt(self.app_language, "add_room.popup.invalid_user_id", &[ + ("error", error_text.as_str()), + ]), PopupKind::Error, None, ); @@ -1112,7 +1228,10 @@ impl Widget for AddRoomScreen { submit_async_request(MatrixRequest::GetRoomPreview { room_or_alias_id, via }); } Err(e) => { - let err_str = format!("Could not parse the text as a valid room address.\nError: {e}."); + let error_text = e.to_string(); + let err_str = tr_fmt(self.app_language, "add_room.popup.parse_error", &[ + ("error", error_text.as_str()), + ]); enqueue_popup_notification( err_str.clone(), PopupKind::Error, @@ -1145,7 +1264,10 @@ impl Widget for AddRoomScreen { break; } Some(RoomPreviewAction::Fetched(Err(e))) => { - let err_str = format!("Failed to fetch room info.\n\nError: {e}."); + let error_text = e.to_string(); + let err_str = tr_fmt(self.app_language, "add_room.popup.fetch_error", &[ + ("error", error_text.as_str()), + ]); enqueue_popup_notification( err_str.clone(), PopupKind::Error, @@ -1170,11 +1292,15 @@ impl Widget for AddRoomScreen { match action.downcast_ref() { Some(KnockResultAction::Knocked { room, .. }) if room.room_id() == frp.room_name_id.room_id() => { let room_type = match room.room_type() { - Some(RoomType::Space) => "space", - _ => "room", + Some(RoomType::Space) => tr_key(self.app_language, "add_room.word.space_lc"), + _ => tr_key(self.app_language, "add_room.word.room_lc"), }; + let room_name_text = frp.room_name_id.to_string(); enqueue_popup_notification( - format!("Successfully knocked on {room_type} {}.", frp.room_name_id), + tr_fmt(self.app_language, "add_room.popup.knock_success", &[ + ("room_type", room_type), + ("room_name", room_name_text.as_str()), + ]), PopupKind::Success, Some(4.0), ); @@ -1182,8 +1308,11 @@ impl Widget for AddRoomScreen { break; } Some(KnockResultAction::Failed { error, room_or_alias_id: roai }) if room_or_alias_id == roai => { + let error_text = error.to_string(); enqueue_popup_notification( - format!("Failed to knock on room.\n\nError: {error}."), + tr_fmt(self.app_language, "add_room.popup.knock_failed", &[ + ("error", error_text.as_str()), + ]), PopupKind::Error, None, ); @@ -1195,11 +1324,15 @@ impl Widget for AddRoomScreen { match action.downcast_ref() { Some(JoinRoomResultAction::Joined { room_id }) if room_id == frp.room_name_id.room_id() => { let room_type = match &frp.room_type { - Some(RoomType::Space) => "space", - _ => "room", + Some(RoomType::Space) => tr_key(self.app_language, "add_room.word.space_lc"), + _ => tr_key(self.app_language, "add_room.word.room_lc"), }; + let room_name_text = frp.room_name_id.to_string(); enqueue_popup_notification( - format!("Successfully joined {room_type} {}.", frp.room_name_id), + tr_fmt(self.app_language, "add_room.popup.join_success", &[ + ("room_type", room_type), + ("room_name", room_name_text.as_str()), + ]), PopupKind::Success, Some(4.0), ); @@ -1207,8 +1340,11 @@ impl Widget for AddRoomScreen { break; } Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == frp.room_name_id.room_id() => { + let error_text = error.to_string(); enqueue_popup_notification( - format!("Failed to join room.\n\nError: {error}."), + tr_fmt(self.app_language, "add_room.popup.join_failed", &[ + ("error", error_text.as_str()), + ]), PopupKind::Error, None, ); @@ -1261,6 +1397,13 @@ impl Widget for AddRoomScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } + let add_friend_text_is_empty = self.view .text_input(cx, ids!(friend_user_id_input)) .text() @@ -1289,7 +1432,9 @@ impl Widget for AddRoomScreen { loading_room_view.set_visible(cx, true); loading_room_view.label(cx, ids!(loading_text)).set_text( cx, - &format!("Fetching {room_or_alias_id}..."), + &tr_fmt(self.app_language, "add_room.loading.fetching", &[ + ("target", room_or_alias_id.as_str()), + ]), ); fetched_room_summary.set_visible(cx, false); error_view.set_visible(cx, false); @@ -1326,81 +1471,113 @@ impl Widget for AddRoomScreen { } let (room_or_space_lc, room_or_space_uc) = match &frp.room_type { - Some(RoomType::Space) => ("space", "Space"), - _ => ("room", "Room"), + Some(RoomType::Space) => ( + tr_key(self.app_language, "add_room.word.space_lc"), + tr_key(self.app_language, "add_room.word.space_uc"), + ), + _ => ( + tr_key(self.app_language, "add_room.word.room_lc"), + tr_key(self.app_language, "add_room.word.room_uc"), + ), }; let room_name = fetched_room_summary.label(cx, ids!(room_name)); match frp.room_name_id.name_for_avatar() { Some(n) => room_name.set_text(cx, n), - _ => room_name.set_text(cx, &format!("Unnamed {room_or_space_uc}, ID: {}", frp.room_name_id.room_id())), + _ => room_name.set_text(cx, &tr_fmt(self.app_language, "add_room.fetched.room_name.unnamed", &[ + ("room_or_space_uc", room_or_space_uc), + ("room_id", frp.room_name_id.room_id().as_str()), + ])), } fetched_room_summary.label(cx, ids!(subsection_alias_id)).set_text( cx, - &format!("Main {room_or_space_uc} Alias and ID"), + &tr_fmt(self.app_language, "add_room.fetched.main_alias_and_id", &[ + ("room_or_space_uc", room_or_space_uc), + ]), ); fetched_room_summary.label(cx, ids!(room_alias)).set_text( cx, - &format!("Alias: {}", frp.canonical_alias.as_ref().map_or("not set", |a| a.as_str())), + &tr_fmt(self.app_language, "add_room.fetched.alias", &[ + ("alias", frp.canonical_alias.as_ref().map_or( + tr_key(self.app_language, "add_room.fetched.alias.not_set"), + |a| a.as_str() + )), + ]), ); fetched_room_summary.label(cx, ids!(room_id)).set_text( cx, - &format!("ID: {}", frp.room_name_id.room_id().as_str()), + &tr_fmt(self.app_language, "add_room.fetched.id", &[ + ("room_id", frp.room_name_id.room_id().as_str()), + ]), ); fetched_room_summary.label(cx, ids!(subsection_topic)).set_text( cx, - &format!("{room_or_space_uc} Topic"), + &tr_fmt(self.app_language, "add_room.fetched.topic_title", &[ + ("room_or_space_uc", room_or_space_uc), + ]), ); fetched_room_summary.html(cx, ids!(room_topic)).set_text( cx, - frp.topic.as_deref().unwrap_or("No topic set"), + frp.topic.as_deref().unwrap_or(tr_key(self.app_language, "add_room.fetched.topic.not_set_html")), ); let room_summary = fetched_room_summary.label(cx, ids!(room_summary)); let join_room_button = fetched_room_summary.button(cx, ids!(join_room_button)); let join_function = match (&frp.state, &frp.join_rule) { (Some(RoomState::Joined), _) => { - room_summary.set_text(cx, &format!("You have already joined this {room_or_space_lc}.")); - join_room_button.set_text(cx, &format!("Go to {room_or_space_lc}")); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.already_joined", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, &tr_fmt(self.app_language, "add_room.button.go_to", &[ + ("room_or_space_lc", room_or_space_lc), + ])); JoinButtonFunction::NavigateOrJoin } (Some(RoomState::Banned), _) => { - room_summary.set_text(cx, &format!("You have been banned from this {room_or_space_lc}.")); - join_room_button.set_text(cx, "Cannot join until un-banned"); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.banned", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, tr_key(self.app_language, "add_room.button.cannot_join_until_unbanned")); JoinButtonFunction::None } (Some(RoomState::Invited), _) => { - room_summary.set_text(cx, &format!("You have already been invited to this {room_or_space_lc}.")); - join_room_button.set_text(cx, "Go to invitation"); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.already_invited", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, tr_key(self.app_language, "add_room.button.go_to_invitation")); JoinButtonFunction::NavigateOrJoin } (Some(RoomState::Knocked), _) => { - room_summary.set_text(cx, &format!("You have already knocked on this {room_or_space_lc}.")); - join_room_button.set_text(cx, "Knock again (be nice!)"); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.already_knocked", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, tr_key(self.app_language, "add_room.button.knock_again")); JoinButtonFunction::Knock } (Some(RoomState::Left), join_rule) => { - room_summary.set_text(cx, &format!("You previously left this {room_or_space_lc}.")); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.previously_left", &[ + ("room_or_space_lc", room_or_space_lc), + ])); let (join_room_text, join_function) = match join_rule { Some(JoinRuleSummary::Public) => ( - format!("Re-join this {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.rejoin", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::NavigateOrJoin, ), Some(JoinRuleSummary::Invite) => ( - format!("Re-joining {room_or_space_lc} requires an invite"), + tr_fmt(self.app_language, "add_room.button.rejoin_requires_invite", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), Some(JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)) => ( - format!("Knock to re-join {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.knock_to_rejoin", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::Knock, ), // TODO: handle this after we update matrix-sdk to the new `JoinRule` enum. Some(JoinRuleSummary::Restricted(_)) => ( - format!("Re-joining {room_or_space_lc} requires an invite or other room membership"), + tr_fmt(self.app_language, "add_room.button.rejoin_requires_other_membership", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), _ => ( - format!("Not allowed to re-join this {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.not_allowed_to_rejoin", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), }; @@ -1409,36 +1586,43 @@ impl Widget for AddRoomScreen { } // This room is not yet known to the user. (None, join_rule) => { - let direct = if frp.is_direct == Some(true) { "direct" } else { "regular" }; - room_summary.set_text(cx, &format!( - "This is a {direct} {room_or_space_lc} with {} {}.", - frp.num_joined_members, - match frp.num_joined_members { - 1 => "member", - _ => "members", - }, - )); + let directness = if frp.is_direct == Some(true) { + tr_key(self.app_language, "add_room.word.direct") + } else { + tr_key(self.app_language, "add_room.word.regular") + }; + let num_members = frp.num_joined_members.to_string(); + let member_word = match frp.num_joined_members { + 1 => tr_key(self.app_language, "add_room.word.member"), + _ => tr_key(self.app_language, "add_room.word.members"), + }; + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.member_count", &[ + ("directness", directness), + ("room_or_space_lc", room_or_space_lc), + ("num_members", num_members.as_str()), + ("member_word", member_word), + ])); let (join_room_text, join_function) = match join_rule { Some(JoinRuleSummary::Public) => ( - format!("Join this {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.join", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::NavigateOrJoin, ), Some(JoinRuleSummary::Invite) => ( - format!("Joining {room_or_space_lc} requires an invite"), + tr_fmt(self.app_language, "add_room.button.join_requires_invite", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), Some(JoinRuleSummary::Knock | JoinRuleSummary::KnockRestricted(_)) => ( - format!("Knock to join {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.knock_to_join", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::Knock, ), // TODO: handle this after we update matrix-sdk to the new `JoinRule` enum. Some(JoinRuleSummary::Restricted(_)) => ( - format!("Joining {room_or_space_lc} requires an invite or other room membership"), + tr_fmt(self.app_language, "add_room.button.join_requires_other_membership", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), _ => ( - format!("Not allowed to join this {room_or_space_lc}"), + tr_fmt(self.app_language, "add_room.button.not_allowed_to_join", &[("room_or_space_lc", room_or_space_lc)]), JoinButtonFunction::None, ), }; @@ -1453,20 +1637,38 @@ impl Widget for AddRoomScreen { self.join_function = join_function; } AddRoomState::Knocked { .. } => { - room_summary.set_text(cx, &format!("You have knocked on this {room_or_space_lc} and must now wait for someone to invite you in.")); - join_room_button.set_text(cx, "Successfully knocked!"); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.knocked_waiting", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, tr_key(self.app_language, "add_room.button.successfully_knocked")); join_room_button.set_enabled(cx, false); } AddRoomState::Joined { .. } => { - room_summary.set_text(cx, &format!("You have joined this {room_or_space_lc}. It is now being loaded from the homeserver; please wait...")); - join_room_button.set_text(cx, "Successfully joined!"); + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.joined_loading", &[ + ("room_or_space_lc", room_or_space_lc), + ])); + join_room_button.set_text(cx, tr_key(self.app_language, "add_room.button.successfully_joined")); join_room_button.set_enabled(cx, false); } AddRoomState::Loaded { is_invite, .. } => { - let verb = if *is_invite { "been invited to" } else { "fully joined" }; - room_summary.set_text(cx, &format!("You have {verb} this {room_or_space_lc}.")); - let adj = if *is_invite { "invited" } else { "joined" }; - join_room_button.set_text(cx, &format!("Go to {adj} {room_or_space_lc}")); + let verb = if *is_invite { + tr_key(self.app_language, "add_room.word.verb.invited") + } else { + tr_key(self.app_language, "add_room.word.verb.joined") + }; + room_summary.set_text(cx, &tr_fmt(self.app_language, "add_room.summary.loaded", &[ + ("verb", verb), + ("room_or_space_lc", room_or_space_lc), + ])); + let adj = if *is_invite { + tr_key(self.app_language, "add_room.word.adj.invited") + } else { + tr_key(self.app_language, "add_room.word.adj.joined") + }; + join_room_button.set_text(cx, &tr_fmt(self.app_language, "add_room.button.go_to_loaded", &[ + ("adj", adj), + ("room_or_space_lc", room_or_space_lc), + ])); join_room_button.set_enabled(cx, true); self.join_function = JoinButtonFunction::NavigateOrJoin; } @@ -1508,9 +1710,9 @@ fn refresh_space_children(cx: &mut Cx, space_id: &OwnedRoomId) { } } -fn creatable_space_labels(creatable_spaces: &[RoomNameId]) -> Vec { +fn creatable_space_labels(creatable_spaces: &[RoomNameId], app_language: AppLanguage) -> Vec { let mut labels = Vec::with_capacity(creatable_spaces.len() + 1); - labels.push("Create without a space".to_string()); + labels.push(tr_key(app_language, "add_room.create_room.dropdown.no_space").to_string()); labels.extend(creatable_spaces.iter().map(ToString::to_string)); labels } @@ -1541,18 +1743,21 @@ fn update_space_hint( hint_label: &LabelRef, creatable_spaces: &[RoomNameId], selected_space_id: Option<&OwnedRoomId>, + app_language: AppLanguage, ) { if creatable_spaces.is_empty() { - hint_label.set_text(cx, "No joined space currently allows you to create child rooms."); + hint_label.set_text(cx, tr_key(app_language, "add_room.create_room.dropdown.hint.no_creatable_spaces")); } else if let Some(space_id) = selected_space_id { let selected_name = creatable_spaces .iter() .find(|space| space.room_id() == space_id) .map(ToString::to_string) .unwrap_or_else(|| space_id.to_string()); - hint_label.set_text(cx, &format!("New room will be added under: {selected_name}")); + hint_label.set_text(cx, &tr_fmt(app_language, "add_room.create_room.dropdown.hint.new_room_under", &[ + ("selected_name", selected_name.as_str()), + ])); } else { - hint_label.set_text(cx, "Create a standalone room, or choose a space from the dropdown."); + hint_label.set_text(cx, tr_key(app_language, "add_room.create_room.dropdown.hint.default")); } } @@ -1562,11 +1767,12 @@ fn sync_space_dropdown( hint_label: &LabelRef, creatable_spaces: &[RoomNameId], preferred_parent_space_id: Option<&OwnedRoomId>, + app_language: AppLanguage, ) { - dropdown.set_labels(cx, creatable_space_labels(creatable_spaces)); + dropdown.set_labels(cx, creatable_space_labels(creatable_spaces, app_language)); apply_space_dropdown_selection(cx, dropdown, creatable_spaces, preferred_parent_space_id); let selected_space_id = selected_creatable_space(creatable_spaces, dropdown.selected_item()); - update_space_hint(cx, hint_label, creatable_spaces, selected_space_id.as_ref()); + update_space_hint(cx, hint_label, creatable_spaces, selected_space_id.as_ref(), app_language); } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index de033c820..b13c2f0c3 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -431,7 +431,7 @@ impl Widget for HomeScreen { if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { settings_page .settings_screen(cx, ids!(settings_screen)) - .populate(cx, None, &app_state.bot_settings); + .populate(cx, None, &app_state.bot_settings, app_state.app_language); self.view.redraw(cx); } else { error!("BUG: failed to set active page to show settings screen."); diff --git a/src/home/invite_modal.rs b/src/home/invite_modal.rs index d644bf542..2bcd32850 100644 --- a/src/home/invite_modal.rs +++ b/src/home/invite_modal.rs @@ -3,6 +3,8 @@ use makepad_widgets::*; use ruma::OwnedUserId; +use crate::app::AppState; +use crate::i18n::{AppLanguage, tr_fmt, tr_key}; use crate::home::room_screen::InviteResultAction; use crate::sliding_sync::{MatrixRequest, submit_async_request}; use crate::utils::RoomNameId; @@ -45,7 +47,7 @@ script_mod! { text_style: TITLE_TEXT {font_size: 13}, color: #000 } - text: "Invite to Room" + text: "" } } @@ -54,7 +56,7 @@ script_mod! { text_style: REGULAR_TEXT {font_size: 11}, color: #000 } - empty_text: "@user:example.org", + empty_text: "", } View { @@ -70,7 +72,7 @@ script_mod! { padding: 12, draw_icon.svg: (ICON_FORBIDDEN) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } - text: "Cancel" + text: "" } confirm_button := RobrixPositiveIconButton { @@ -79,7 +81,7 @@ script_mod! { padding: 12, draw_icon.svg: (ICON_ADD_USER) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } - text: "Invite" + text: "" } okay_button := RobrixIconButton { @@ -89,7 +91,7 @@ script_mod! { padding: 12, draw_icon.svg: (ICON_CHECKMARK) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } - text: "Okay" + text: "" } } @@ -144,10 +146,20 @@ pub struct InviteModal { #[deref] view: View, #[rust] state: InviteModalState, #[rust] room_name_id: Option, + #[rust] app_language: AppLanguage, } impl Widget for InviteModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Some(app_state) = scope.data.get::() + && self.app_language != app_state.app_language + { + self.app_language = app_state.app_language; + self.update_static_texts(cx); + if let Some(room_name_id) = self.room_name_id.clone() { + self.set_invite_title(cx, &room_name_id); + } + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } @@ -195,7 +207,7 @@ impl WidgetMatchEvent for InviteModal { // Validate the user ID if user_id_str.is_empty() { script_apply_eval!(cx, status_label, { - text: "Please enter a user ID.", + text: #(tr_key(self.app_language, "invite_modal.status.enter_user_id")), draw_text +: { color: mod.widgets.COLOR_FG_DANGER_RED, }, @@ -215,7 +227,7 @@ impl WidgetMatchEvent for InviteModal { }); self.state = InviteModalState::WaitingForInvite(user_id.to_owned()); script_apply_eval!(cx, status_label, { - text: "Sending invite...", + text: #(tr_key(self.app_language, "invite_modal.status.sending")), draw_text +: { color: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER, }, @@ -227,7 +239,7 @@ impl WidgetMatchEvent for InviteModal { } Err(_) => { script_apply_eval!(cx, status_label, { - text: "Invalid User ID. Expected format: @user:server.xyz", + text: #(tr_key(self.app_language, "invite_modal.status.invalid_user_id")), draw_text +: { color: mod.widgets.COLOR_FG_DANGER_RED, }, @@ -247,7 +259,11 @@ impl WidgetMatchEvent for InviteModal { if self.room_name_id.as_ref().is_some_and(|rni| rni.room_id() == room_id) && invited_user_id == user_id => { - let status = format!("Successfully invited {user_id}!"); + let status = tr_fmt( + self.app_language, + "invite_modal.status.success_invited", + &[("user_id", user_id.as_str())], + ); script_apply_eval!(cx, status_label, { text: #(status), draw_text +: { @@ -264,7 +280,12 @@ impl WidgetMatchEvent for InviteModal { if self.room_name_id.as_ref().is_some_and(|rni| rni.room_id() == room_id) && invited_user_id == user_id => { - let status = format!("Failed to send invite: {error}"); + let error_text = error.to_string(); + let status = tr_fmt( + self.app_language, + "invite_modal.status.send_failed", + &[("error", error_text.as_str())], + ); script_apply_eval!(cx, status_label, { text: #(status), draw_text +: { @@ -290,11 +311,31 @@ impl WidgetMatchEvent for InviteModal { } impl InviteModal { - pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { - self.view.label(cx, ids!(title)).set_text( - cx, - &format!("Invite to {room_name_id}"), + fn set_invite_title(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { + let room_name = room_name_id.to_string(); + let title = tr_fmt( + self.app_language, + "invite_modal.title.invite_to_room_name", + &[("room_name", room_name.as_str())], ); + self.view.label(cx, ids!(title)).set_text(cx, &title); + } + + fn update_static_texts(&mut self, cx: &mut Cx) { + self.view.button(cx, ids!(cancel_button)) + .set_text(cx, tr_key(self.app_language, "invite_modal.button.cancel")); + self.view.button(cx, ids!(confirm_button)) + .set_text(cx, tr_key(self.app_language, "invite_modal.button.invite")); + self.view.button(cx, ids!(okay_button)) + .set_text(cx, tr_key(self.app_language, "invite_modal.button.okay")); + self.view.text_input(cx, ids!(user_id_input)) + .set_empty_text(cx, tr_key(self.app_language, "invite_modal.input.placeholder").to_string()); + } + + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId, app_language: AppLanguage) { + self.app_language = app_language; + self.set_invite_title(cx, &room_name_id); + self.update_static_texts(cx); self.state = InviteModalState::WaitingForUserInput; self.room_name_id = Some(room_name_id); @@ -321,8 +362,8 @@ impl InviteModal { } impl InviteModalRef { - pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { + pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId, app_language: AppLanguage) { let Some(mut inner) = self.borrow_mut() else { return }; - inner.show(cx, room_name_id); + inner.show(cx, room_name_id, app_language); } } diff --git a/src/home/invite_screen.rs b/src/home/invite_screen.rs index 672b6d1ba..0bcd46acc 100644 --- a/src/home/invite_screen.rs +++ b/src/home/invite_screen.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{app::AppStateAction, home::rooms_list::RoomsListRef, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, popup_list::{enqueue_popup_notification, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt}, sliding_sync::{submit_async_request, MatrixRequest}, utils::{self, RoomNameId}}; +use crate::{app::{AppState, AppStateAction}, home::rooms_list::RoomsListRef, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, popup_list::{enqueue_popup_notification, PopupKind}, restore_status_view::RestoreStatusViewWidgetExt}, sliding_sync::{submit_async_request, MatrixRequest}, utils::{self, RoomNameId}}; use super::rooms_list::{InviteState, InviterInfo}; @@ -251,10 +251,16 @@ pub struct InviteScreen { #[rust] room_name_id: Option, #[rust] is_loaded: bool, #[rust] all_rooms_loaded: bool, + #[rust] app_language: AppLanguage, } impl Widget for InviteScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.app_language = app_language; + // Currently, a Signal event is only used to tell this widget // to check if the room has been loaded from the homeserver yet. if let Event::Signal = event { @@ -324,7 +330,11 @@ impl Widget for InviteScreen { Some(JoinRoomResultAction::Joined { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingForJoinedRoom; if !self.has_shown_confirmation { - enqueue_popup_notification("Successfully joined room.", PopupKind::Success, Some(5.0)); + enqueue_popup_notification( + tr_key(self.app_language, "invite_screen.popup.joined_success"), + PopupKind::Success, + Some(5.0), + ); } continue; } @@ -343,14 +353,23 @@ impl Widget for InviteScreen { Some(LeaveRoomResultAction::Left { room_id }) if room_id == info.room_id() => { self.invite_state = InviteState::RoomLeft; if !self.has_shown_confirmation { - enqueue_popup_notification("Successfully rejected invite.", PopupKind::Success, Some(5.0)); + enqueue_popup_notification( + tr_key(self.app_language, "invite_screen.popup.rejected_success"), + PopupKind::Success, + Some(5.0), + ); } continue; } Some(LeaveRoomResultAction::Failed { room_id, error }) if room_id == info.room_id() => { self.invite_state = InviteState::WaitingOnUserInput; if !self.has_shown_confirmation { - enqueue_popup_notification(format!("Failed to reject invite: {error}"), PopupKind::Error, None); + let error_text = error.to_string(); + enqueue_popup_notification( + tr_fmt(self.app_language, "invite_screen.popup.reject_failed", &[("error", error_text.as_str())]), + PopupKind::Error, + None, + ); } continue; } @@ -375,6 +394,11 @@ impl Widget for InviteScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.app_language = app_language; + if !self.is_loaded { let mut restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); if let Some(room_name) = &self.room_name_id { @@ -421,10 +445,10 @@ impl Widget for InviteScreen { inviter_name.set_text(cx, inviter.user_id.as_str()); inviter_user_id.set_visible(cx, false); } - (true, "has invited you to join:") + (true, tr_key(self.app_language, "invite_screen.message.invited_by")) } else { - (false, "You have been invited to join:") + (false, tr_key(self.app_language, "invite_screen.message.invited_generic")) }; inviter_view.set_visible(cx, is_visible); self.view.label(cx, ids!(invite_message)).set_text(cx, invite_text); @@ -459,33 +483,33 @@ impl Widget for InviteScreen { InviteState::WaitingOnUserInput => { cancel_button.set_enabled(cx, true); accept_button.set_enabled(cx, true); - cancel_button.set_text(cx, "Reject Invite"); - accept_button.set_text(cx, "Join Room"); + cancel_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.reject")); + accept_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.join")); } InviteState::WaitingForJoinResult => { cancel_button.set_enabled(cx, false); accept_button.set_enabled(cx, false); - cancel_button.set_text(cx, "Reject Invite"); - accept_button.set_text(cx, "Joining..."); + cancel_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.reject")); + accept_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.joining")); } InviteState::WaitingForLeaveResult => { cancel_button.set_enabled(cx, false); accept_button.set_enabled(cx, false); - cancel_button.set_text(cx, "Rejecting..."); - accept_button.set_text(cx, "Join Room"); + cancel_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.rejecting")); + accept_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.join")); } InviteState::WaitingForJoinedRoom => { cancel_button.set_enabled(cx, false); accept_button.set_enabled(cx, false); - cancel_button.set_text(cx, "Reject Invite"); - accept_button.set_text(cx, "Joined!"); + cancel_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.reject")); + accept_button.set_text(cx, tr_key(self.app_language, "invite_screen.button.joined")); } InviteState::RoomLeft => { cancel_button.set_visible(cx, false); accept_button.set_visible(cx, false); self.view.label(cx, ids!(completion_label)).set_text( cx, - "Invite successfully rejected. You may close this invite.", + tr_key(self.app_language, "invite_screen.completion.rejected"), ); } } diff --git a/src/home/loading_pane.rs b/src/home/loading_pane.rs index baa975a3d..5e6901572 100644 --- a/src/home/loading_pane.rs +++ b/src/home/loading_pane.rs @@ -1,7 +1,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedEventId; -use crate::sliding_sync::TimelineRequestSender; +use crate::{app::AppState, i18n::{AppLanguage, tr_fmt, tr_key}, sliding_sync::TimelineRequestSender}; script_mod! { @@ -115,6 +115,7 @@ pub enum LoadingPaneState { pub struct LoadingPane { #[deref] view: View, #[rust] state: LoadingPaneState, + #[rust] app_language: AppLanguage, } impl Drop for LoadingPane { fn drop(&mut self) { @@ -134,6 +135,12 @@ impl Drop for LoadingPane { impl Widget for LoadingPane { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.visible = true; if matches!(self.state, LoadingPaneState::None) { self.visible = false; @@ -144,6 +151,12 @@ impl Widget for LoadingPane { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } if !self.visible { return; } self.view.handle_event(cx, event, scope); @@ -196,6 +209,49 @@ impl Widget for LoadingPane { impl LoadingPane { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.sync_state_text(cx); + self.view.redraw(cx); + } + + fn sync_state_text(&mut self, cx: &mut Cx) { + let (title, status, cancel_text) = match &self.state { + LoadingPaneState::BackwardsPaginateUntilEvent { + target_event_id, + events_paginated, + .. + } => { + let events_paginated_str = events_paginated.to_string(); + ( + tr_key(self.app_language, "loading_pane.title.searching_older").to_string(), + Some(tr_fmt(self.app_language, "loading_pane.status.searching_event", &[ + ("target_event_id", target_event_id.as_str()), + ("events_paginated", events_paginated_str.as_str()), + ])), + tr_key(self.app_language, "loading_pane.button.cancel").to_string(), + ) + } + LoadingPaneState::Error(error_message) => ( + tr_key(self.app_language, "loading_pane.title.error").to_string(), + Some(error_message.clone()), + tr_key(self.app_language, "loading_pane.button.okay").to_string(), + ), + LoadingPaneState::None => ( + tr_key(self.app_language, "loading_pane.title.default").to_string(), + None, + tr_key(self.app_language, "loading_pane.button.cancel").to_string(), + ), + }; + + self.set_title(cx, &title); + if let Some(status) = status { + self.set_status(cx, &status); + } + let cancel_button = self.button(cx, ids!(cancel_button)); + cancel_button.set_text(cx, &cancel_text); + } + /// Returns `true` if this pane is currently being shown. pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { self.visible @@ -208,29 +264,8 @@ impl LoadingPane { } pub fn set_state(&mut self, cx: &mut Cx, state: LoadingPaneState) { - let cancel_button = self.button(cx, ids!(cancel_button)); - match &state { - LoadingPaneState::BackwardsPaginateUntilEvent { - target_event_id, - events_paginated, - .. - } => { - self.set_title(cx, "Searching older messages..."); - self.set_status(cx, &format!( - "Looking for event {target_event_id}\n\n\ - Fetched {events_paginated} messages so far...", - )); - cancel_button.set_text(cx, "Cancel"); - } - LoadingPaneState::Error(error_message) => { - self.set_title(cx, "Error loading content"); - self.set_status(cx, error_message); - cancel_button.set_text(cx, "Okay"); - } - LoadingPaneState::None => { } - } - self.state = state; + self.sync_state_text(cx); self.redraw(cx); } diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 796a43a86..06d2abcc8 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,7 +3,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{app::AppState, home::invite_modal::InviteModalAction, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId}; +use crate::{app::AppState, home::invite_modal::InviteModalAction, i18n::{AppLanguage, tr_fmt, tr_key}, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId}; const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; @@ -147,6 +147,7 @@ pub struct RoomContextMenu { #[deref] view: View, #[source] source: ScriptObjectRef, #[rust] details: Option, + #[rust] app_language: AppLanguage, } impl Widget for RoomContextMenu { @@ -159,6 +160,14 @@ impl Widget for RoomContextMenu { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if !self.visible { return; } + if let Some(app_state) = scope.data.get::() + && self.app_language != app_state.app_language + { + self.app_language = app_state.app_language; + if let Some(details) = self.details.clone() { + self.update_buttons(cx, &details); + } + } self.view.handle_event(cx, event, scope); // Close logic similar to NewMessageContextMenu @@ -219,10 +228,10 @@ impl WidgetMatchEvent for RoomContextMenu { }); close_menu = true; } - else if self.button(cx, ids!(room_settings_button)).clicked(actions) { + else if self.button(cx, ids!(room_settings_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( - "The room settings page is not yet implemented.", + tr_key(self.app_language, "room_context_menu.popup.settings_not_implemented"), PopupKind::Warning, Some(5.0), ); @@ -231,7 +240,7 @@ impl WidgetMatchEvent for RoomContextMenu { else if self.button(cx, ids!(notifications_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( - "The room notifications page is not yet implemented.", + tr_key(self.app_language, "room_context_menu.popup.notifications_not_implemented"), PopupKind::Warning, Some(5.0), ); @@ -256,7 +265,9 @@ impl WidgetMatchEvent for RoomContextMenu { bot_user_id: bot_user_id.clone(), }); enqueue_popup_notification( - format!("Removing BotFather {bot_user_id} from this room..."), + tr_fmt(self.app_language, "room_context_menu.popup.removing_botfather", &[ + ("bot_user_id", bot_user_id.as_str()), + ]), PopupKind::Info, Some(4.0), ); @@ -267,7 +278,9 @@ impl WidgetMatchEvent for RoomContextMenu { bot_user_id: bot_user_id.clone(), }); enqueue_popup_notification( - format!("Inviting BotFather {bot_user_id} into this room..."), + tr_fmt(self.app_language, "room_context_menu.popup.inviting_botfather", &[ + ("bot_user_id", bot_user_id.as_str()), + ]), PopupKind::Info, Some(5.0), ); @@ -279,7 +292,7 @@ impl WidgetMatchEvent for RoomContextMenu { } } else { enqueue_popup_notification( - "Bot settings are unavailable right now.", + tr_key(self.app_language, "room_context_menu.popup.bot_settings_unavailable"), PopupKind::Error, Some(5.0), ); @@ -308,7 +321,8 @@ impl RoomContextMenu { self.visible } - pub fn show(&mut self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 { + pub fn show(&mut self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage) -> DVec2 { + self.app_language = app_language; let height = self.update_buttons(cx, &details); self.details = Some(details); self.visible = true; @@ -319,31 +333,42 @@ impl RoomContextMenu { fn update_buttons(&mut self, cx: &mut Cx, details: &RoomContextMenuDetails) -> f64 { let mark_unread_button = self.button(cx, ids!(mark_unread_button)); if details.is_marked_unread { - mark_unread_button.set_text(cx, "Mark as Read"); + mark_unread_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.mark_read")); } else { - mark_unread_button.set_text(cx, "Mark as Unread"); + mark_unread_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.mark_unread")); } let favorite_button = self.button(cx, ids!(favorite_button)); if details.is_favorite { - favorite_button.set_text(cx, "Un-favorite"); + favorite_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.unfavorite")); } else { - favorite_button.set_text(cx, "Favorite"); + favorite_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.favorite")); } let priority_button = self.button(cx, ids!(priority_button)); if details.is_low_priority { - priority_button.set_text(cx, "Un-set Low Priority"); + priority_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.unset_low_priority")); } else { - priority_button.set_text(cx, "Set Low Priority"); + priority_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.set_low_priority")); } + self.button(cx, ids!(copy_link_button)) + .set_text(cx, tr_key(self.app_language, "room_context_menu.button.copy_link_to_room")); + self.button(cx, ids!(room_settings_button)) + .set_text(cx, tr_key(self.app_language, "room_context_menu.button.settings")); + self.button(cx, ids!(notifications_button)) + .set_text(cx, tr_key(self.app_language, "room_context_menu.button.notifications")); + self.button(cx, ids!(invite_button)) + .set_text(cx, tr_key(self.app_language, "room_context_menu.button.invite")); + self.button(cx, ids!(leave_button)) + .set_text(cx, tr_key(self.app_language, "room_context_menu.button.leave_room")); + let bot_binding_button = self.button(cx, ids!(bot_binding_button)); bot_binding_button.set_visible(cx, details.app_service_enabled); if details.is_bot_bound { - bot_binding_button.set_text(cx, "Unbind BotFather"); + bot_binding_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.unbind_botfather")); } else { - bot_binding_button.set_text(cx, "Bind BotFather"); + bot_binding_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.bind_botfather")); } // Reset hover states @@ -378,8 +403,8 @@ impl RoomContextMenuRef { inner.is_currently_shown(cx) } - pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 { + pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; - inner.show(cx, details) + inner.show(cx, details, app_language) } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 349d23326..e6d91406a 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{ use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, i18n::{AppLanguage, tr_fmt, tr_key}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, @@ -59,7 +59,6 @@ const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; /// otherwise many short messages can trigger a long chain of tiny paginations. const VIEWPORT_FILL_PAGINATION_SIZE: u16 = 150; -static UNNAMED_ROOM: &str = "Unnamed Room"; /// #FFF4E5 const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); @@ -220,10 +219,11 @@ fn format_delete_bot_command(matrix_user_id: &UserId) -> String { fn resolve_delete_bot_user_id( user_id_or_localpart: &str, current_user_id: Option<&UserId>, + app_language: AppLanguage, ) -> Result { let raw = user_id_or_localpart.trim(); if raw.is_empty() { - return Err("Please enter the bot Matrix user ID to delete.".into()); + return Err(tr_key(app_language, "room_screen.bot.delete.error.empty_user_id").into()); } if raw.starts_with('@') || raw.contains(':') { @@ -234,19 +234,23 @@ fn resolve_delete_bot_user_id( }; return UserId::parse(&full_user_id) .map(|user_id| user_id.to_owned()) - .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")); + .map_err(|_| tr_fmt(app_language, "room_screen.bot.delete.error.invalid_user_id", &[ + ("full_user_id", full_user_id.as_str()), + ])); } let Some(current_user_id) = current_user_id else { return Err( - "Current user ID is unavailable, so the bot homeserver cannot be resolved.".into(), + tr_key(app_language, "room_screen.bot.delete.error.current_user_unavailable").into(), ); }; let full_user_id = format!("@{raw}:{}", current_user_id.server_name()); UserId::parse(&full_user_id) .map(|user_id| user_id.to_owned()) - .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")) + .map_err(|_| tr_fmt(app_language, "room_screen.bot.delete.error.invalid_user_id", &[ + ("full_user_id", full_user_id.as_str()), + ])) } fn detected_bot_binding_for_members( @@ -464,7 +468,7 @@ script_mod! { text_style: USERNAME_TEXT_STYLE {}, color: (USERNAME_TEXT_COLOR) } - text: "" + text: "" } } @@ -624,7 +628,7 @@ script_mod! { draw_icon.svg: (ICON_ADD_USER) draw_text.text_style: SMALL_STATE_TEXT_STYLE {} icon_walk: Walk{width: 15, height: Fit, margin: Inset{right: -4}} - text: "Invite to Room" + text: "" } content := Label { @@ -664,7 +668,7 @@ script_mod! { text_style: TEXT_SUB {}, color: (COLOR_DIVIDER_DARK) } - text: "" + text: "" } right_line := LineH { } @@ -679,7 +683,7 @@ script_mod! { date := Label { draw_text.color: (mod.widgets.COLOR_READ_MARKER) - text: "New Messages" + text: "" } right_line := LineH { @@ -708,7 +712,7 @@ script_mod! { text_style: MESSAGE_TEXT_STYLE { font_size: 10 }, color: (TIMESTAMP_TEXT_COLOR) } - text: "Loading earlier messages..." + text: "" } } @@ -733,7 +737,7 @@ script_mod! { text_style: USERNAME_TEXT_STYLE { font_size: 10.8 } color: (COLOR_ACTIVE_PRIMARY) } - text: "BotFather" + text: "" } sender_tag := Label { @@ -743,7 +747,7 @@ script_mod! { text_style: REGULAR_TEXT { font_size: 9.5 } color: #8A8A8A } - text: "bot" + text: "" } } @@ -775,7 +779,7 @@ script_mod! { text_style: USERNAME_TEXT_STYLE { font_size: 11.2 } color: #1F1F1F } - text: "App Service Actions" + text: "" } spacer := View { @@ -802,7 +806,7 @@ script_mod! { text_style: REGULAR_TEXT { font_size: 10.5 } color: (COLOR_TEXT) } - text: "Create a bot through BotFather. Robrix only sends the matching slash command." + text: "" } footer := View { @@ -818,7 +822,7 @@ script_mod! { text_style: REGULAR_TEXT { font_size: 8.8 } color: #9A9A9A } - text: "now" + text: "" } } } @@ -841,7 +845,7 @@ script_mod! { padding: 10 draw_icon.svg: (ICON_CHECKMARK) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} - text: "Create Bot" + text: "" } list_button := RobrixNeutralIconButton { @@ -850,7 +854,7 @@ script_mod! { padding: 10 draw_icon.svg: (ICON_SEARCH) icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} - text: "List Bots" + text: "" } } @@ -866,7 +870,7 @@ script_mod! { padding: 10 draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} - text: "Delete Bot" + text: "" } help_button := RobrixNeutralIconButton { @@ -875,7 +879,7 @@ script_mod! { padding: 10 draw_icon.svg: (ICON_INFO) icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} - text: "Bot Help" + text: "" } } @@ -891,7 +895,7 @@ script_mod! { padding: 10 draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} - text: "Unbind" + text: "" } } } @@ -1038,6 +1042,8 @@ pub struct RoomScreen { streaming_timeout_timer: Timer, /// Whether the in-room app service quick actions card is currently visible. #[rust] show_app_service_actions: bool, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, } impl Drop for RoomScreen { @@ -1067,6 +1073,12 @@ impl ScriptHook for RoomScreen { impl Widget for RoomScreen { // Handle events and actions for the RoomScreen widget and its inner Timeline view. fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } let room_screen_widget_uid = self.widget_uid(); let portal_list = self.portal_list(cx, ids!(timeline.list)); let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); @@ -1193,7 +1205,9 @@ impl Widget for RoomScreen { .collect(); let mut tooltip_text = utils::human_readable_list(&tooltip_text_arr, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT); - tooltip_text.push_str(&format!(" reacted with: {}", reaction_data.reaction)); + tooltip_text.push_str(&tr_fmt(self.app_language, "room_screen.tooltip.reacted_with_suffix", &[ + ("reaction", reaction_data.reaction.as_str()), + ])); cx.widget_action( room_screen_widget_uid, TooltipAction::HoverIn { @@ -1262,10 +1276,11 @@ impl Widget for RoomScreen { user_id.as_str() }; let room_id = tl.kind.room_id().clone(); + let app_language = self.app_language; let content = ConfirmationModalContent { - title_text: "Send Invitation".into(), - body_text: format!("Are you sure you want to invite {username} to this room?").into(), - accept_button_text: Some("Invite".into()), + title_text: tr_key(app_language, "room_screen.modal.invite.title").into(), + body_text: tr_fmt(app_language, "room_screen.modal.invite.body", &[("username", username)]).into(), + accept_button_text: Some(tr_key(app_language, "room_screen.modal.invite.accept").into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::InviteUser { room_id, user_id }); })), @@ -1296,7 +1311,7 @@ impl Widget for RoomScreen { // Only handle if this is for the current room. if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { enqueue_popup_notification( - "Sent invite successfully.", + tr_key(self.app_language, "room_screen.popup.invite.sent_success"), PopupKind::Success, Some(4.0), ); @@ -1305,8 +1320,11 @@ impl Widget for RoomScreen { if let Some(InviteResultAction::Failed { room_id, error, .. }) = action.downcast_ref() { // Only handle if this is for the current room. if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + let error_text = error.to_string(); enqueue_popup_notification( - format!("Failed to send invite.\n\nError: {error}"), + tr_fmt(self.app_language, "room_screen.popup.invite.failed", &[ + ("error", error_text.as_str()), + ]), PopupKind::Error, None, ); @@ -1500,14 +1518,14 @@ impl Widget for RoomScreen { if let Some(app_state) = scope.data.get::() { if !app_state.bot_settings.enabled { enqueue_popup_notification( - "Enable App Service before creating bots in a room.", + tr_key(self.app_language, "room_screen.popup.app_service.enable_before_create"), PopupKind::Warning, Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else if !room_props.app_service_room_bound { enqueue_popup_notification( - "Bind BotFather to this room before creating a bot.", + tr_key(self.app_language, "room_screen.popup.app_service.bind_before_create"), PopupKind::Warning, Some(4.0), ); @@ -1517,7 +1535,7 @@ impl Widget for RoomScreen { } } else { enqueue_popup_notification( - "App state is unavailable, so bot creation is temporarily unavailable.", + tr_key(self.app_language, "room_screen.popup.app_service.state_unavailable_create"), PopupKind::Error, Some(4.0), ); @@ -1529,14 +1547,14 @@ impl Widget for RoomScreen { if let Some(app_state) = scope.data.get::() { if !app_state.bot_settings.enabled { enqueue_popup_notification( - "Enable App Service before deleting bots in a room.", + tr_key(self.app_language, "room_screen.popup.app_service.enable_before_delete"), PopupKind::Warning, Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else if !room_props.app_service_room_bound { enqueue_popup_notification( - "Bind BotFather to this room before deleting a bot.", + tr_key(self.app_language, "room_screen.popup.app_service.bind_before_delete"), PopupKind::Warning, Some(4.0), ); @@ -1546,7 +1564,7 @@ impl Widget for RoomScreen { } } else { enqueue_popup_notification( - "App state is unavailable, so bot deletion is temporarily unavailable.", + tr_key(self.app_language, "room_screen.popup.app_service.state_unavailable_delete"), PopupKind::Error, Some(4.0), ); @@ -1560,7 +1578,7 @@ impl Widget for RoomScreen { cx, app_state, "/listbots", - "Sent `/listbots` to BotFather.", + tr_key(self.app_language, "room_screen.popup.bot.sent_listbots").to_string(), ); } return false; @@ -1571,7 +1589,7 @@ impl Widget for RoomScreen { cx, app_state, "/bothelp", - "Sent `/bothelp` to BotFather.", + tr_key(self.app_language, "room_screen.popup.bot.sent_bothelp").to_string(), ); } return false; @@ -1580,7 +1598,7 @@ impl Widget for RoomScreen { if let Some(app_state) = scope.data.get::() { if !room_props.app_service_room_bound { enqueue_popup_notification( - "This room is not currently bound to BotFather.", + tr_key(self.app_language, "room_screen.popup.app_service.room_not_bound"), PopupKind::Warning, Some(4.0), ); @@ -1599,9 +1617,9 @@ impl Widget for RoomScreen { bot_user_id: bot_user_id.clone(), }); enqueue_popup_notification( - format!( - "Removing BotFather {bot_user_id} from this room..." - ), + tr_fmt(self.app_language, "room_screen.popup.app_service.removing_botfather", &[ + ("bot_user_id", bot_user_id.as_str()), + ]), PopupKind::Info, Some(4.0), ); @@ -1617,7 +1635,7 @@ impl Widget for RoomScreen { } } else { enqueue_popup_notification( - "App state is unavailable, so BotFather could not be removed from this room.", + tr_key(self.app_language, "room_screen.popup.app_service.state_unavailable_unbind"), PopupKind::Error, Some(4.0), ); @@ -1636,7 +1654,7 @@ impl Widget for RoomScreen { Some(CreateBotModalAction::Submit(request)) => { let Some(app_state) = scope.data.get::() else { enqueue_popup_notification( - "App state is unavailable, so the create-bot command was not sent.", + tr_key(self.app_language, "room_screen.popup.bot.state_unavailable_create_command"), PopupKind::Error, Some(4.0), ); @@ -1663,7 +1681,7 @@ impl Widget for RoomScreen { Some(DeleteBotModalAction::Submit(request)) => { let Some(app_state) = scope.data.get::() else { enqueue_popup_notification( - "App state is unavailable, so the delete-bot command was not sent.", + tr_key(self.app_language, "room_screen.popup.bot.state_unavailable_delete_command"), PopupKind::Error, Some(4.0), ); @@ -1683,19 +1701,19 @@ impl Widget for RoomScreen { { if room_props.timeline_kind.thread_root_event_id().is_some() { enqueue_popup_notification( - "Bot commands are only supported in the main room timeline.", + tr_key(self.app_language, "room_screen.popup.bot.main_timeline_only"), PopupKind::Warning, Some(4.0), ); } else if !room_props.app_service_enabled { enqueue_popup_notification( - "Enable App Service in Settings before using /bot.", + tr_key(self.app_language, "room_screen.popup.bot.enable_in_settings_before_bot"), PopupKind::Warning, Some(4.0), ); } else if !room_props.app_service_room_bound { enqueue_popup_notification( - "Bind BotFather to this room before using /bot.", + tr_key(self.app_language, "room_screen.popup.bot.bind_before_bot"), PopupKind::Warning, Some(4.0), ); @@ -1713,7 +1731,7 @@ impl Widget for RoomScreen { UserProfilePaneInfo { profile_and_room_id, room_name: self.room_name_id.as_ref().map_or_else( - || UNNAMED_ROOM.to_string(), + || tr_key(self.app_language, "room_screen.fallback.unnamed_room").to_string(), |r| r.to_string(), ), room_member: None, @@ -1768,6 +1786,12 @@ impl Widget for RoomScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } // If the room isn't loaded yet, we show the restore status label only. if !self.is_loaded { let Some(room_name) = &self.room_name_id else { @@ -1840,6 +1864,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, msg_like_content, prev_event, @@ -1860,6 +1885,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, poll_state, item_drawn_status, @@ -1869,6 +1895,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, utd, item_drawn_status, @@ -1878,6 +1905,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, other, item_drawn_status, @@ -1890,6 +1918,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, membership_change, item_drawn_status, @@ -1899,6 +1928,7 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, profile_change, item_drawn_status, @@ -1908,13 +1938,17 @@ impl Widget for RoomScreen { list, item_id, &tl_state.kind, + self.app_language, event_tl_item, other, item_drawn_status, ), unhandled => { let item = list.item(cx, item_id, id!(SmallStateEvent)); - item.label(cx, ids!(content)).set_text(cx, &format!("[Unsupported] {:?}", unhandled)); + item.label(cx, ids!(content)).set_text( + cx, + &format!("{} {:?}", tr_key(self.app_language, "room_screen.unsupported.prefix"), unhandled), + ); (item, ItemDrawnStatus::both_drawn()) } } @@ -1929,6 +1963,10 @@ impl Widget for RoomScreen { } TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { let item = list.item(cx, item_id, id!(ReadMarker)); + item.label(cx, ids!(date)).set_text( + cx, + tr_key(self.app_language, "room_screen.read_marker.new_messages"), + ); (item, ItemDrawnStatus::both_drawn()) } TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { @@ -1970,6 +2008,19 @@ impl Widget for RoomScreen { } impl RoomScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(top_space.label)) + .set_text(cx, tr_key(self.app_language, "room_screen.top_space.loading_earlier")); + self.view.redraw(cx); + } + fn room_id(&self) -> Option<&OwnedRoomId> { self.room_name_id.as_ref().map(|r| r.room_id()) } @@ -2047,14 +2098,14 @@ impl RoomScreen { cx: &mut Cx, app_state: &AppState, command: &str, - success_message: &str, + success_message: String, ) -> bool { let Some(timeline_kind) = self.timeline_kind.clone() else { return false; }; if timeline_kind.thread_root_event_id().is_some() { enqueue_popup_notification( - "Bot commands are only supported in the main room timeline.", + tr_key(self.app_language, "room_screen.popup.bot.main_timeline_only"), PopupKind::Warning, Some(4.0), ); @@ -2066,7 +2117,7 @@ impl RoomScreen { }; if !app_state.bot_settings.enabled { enqueue_popup_notification( - "Enable App Service before using BotFather commands in a room.", + tr_key(self.app_language, "room_screen.popup.bot.enable_before_commands"), PopupKind::Warning, Some(4.0), ); @@ -2074,7 +2125,7 @@ impl RoomScreen { } if !self.is_app_service_room_bound(app_state, &room_id) { enqueue_popup_notification( - "Bind BotFather to this room before using BotFather commands.", + tr_key(self.app_language, "room_screen.popup.bot.bind_before_commands"), PopupKind::Warning, Some(4.0), ); @@ -2089,7 +2140,7 @@ impl RoomScreen { sign_with_tsp: false, }); - enqueue_popup_notification(success_message.to_string(), PopupKind::Info, Some(4.0)); + enqueue_popup_notification(success_message, PopupKind::Info, Some(4.0)); self.set_app_service_actions_visible(cx, false); true } @@ -2107,7 +2158,7 @@ impl RoomScreen { }; if timeline_kind.thread_root_event_id().is_some() { enqueue_popup_notification( - "Bot creation commands are only supported in the main room timeline.", + tr_key(self.app_language, "room_screen.popup.bot.creation_main_timeline_only"), PopupKind::Warning, Some(4.0), ); @@ -2119,7 +2170,7 @@ impl RoomScreen { }; if !app_state.bot_settings.enabled { enqueue_popup_notification( - "Enable App Service before creating bots in a room.", + tr_key(self.app_language, "room_screen.popup.app_service.enable_before_create"), PopupKind::Warning, Some(4.0), ); @@ -2127,7 +2178,7 @@ impl RoomScreen { } if !self.is_app_service_room_bound(app_state, &room_id) { enqueue_popup_notification( - "Bind BotFather to this room before creating a bot.", + tr_key(self.app_language, "room_screen.popup.app_service.bind_before_create"), PopupKind::Warning, Some(4.0), ); @@ -2139,7 +2190,7 @@ impl RoomScreen { cx, app_state, &command, - &format!("Sent `/createbot` for `{username}` to BotFather."), + tr_fmt(self.app_language, "room_screen.popup.bot.sent_createbot", &[("username", username)]), ) { self.close_create_bot_modal(cx); } @@ -2152,7 +2203,7 @@ impl RoomScreen { user_id_or_localpart: &str, ) { let matrix_user_id = - match resolve_delete_bot_user_id(user_id_or_localpart, current_user_id().as_deref()) { + match resolve_delete_bot_user_id(user_id_or_localpart, current_user_id().as_deref(), self.app_language) { Ok(user_id) => user_id, Err(error) => { enqueue_popup_notification(error, PopupKind::Error, Some(4.0)); @@ -2165,7 +2216,7 @@ impl RoomScreen { cx, app_state, &command, - &format!("Sent `/deletebot` for {matrix_user_id} to BotFather."), + tr_fmt(self.app_language, "room_screen.popup.bot.sent_deletebot", &[("matrix_user_id", matrix_user_id.as_str())]), ) { self.close_delete_bot_modal(cx); } @@ -2424,7 +2475,7 @@ impl RoomScreen { if is_valid { // We successfully found the target event, so we can close the loading pane, // reset the loading panestate to `None`, and stop issuing backwards pagination requests. - loading_pane.set_status(cx, "Successfully found replied-to message!"); + loading_pane.set_status(cx, tr_key(self.app_language, "room_screen.loading.found_related_message")); loading_pane.set_state(cx, LoadingPaneState::None); // NOTE: this code was copied from the `MessageAction::JumpToRelated` handler; @@ -2447,7 +2498,7 @@ impl RoomScreen { error!("Target event index {index} of {} is out of bounds for room {}", tl.items.len(), tl.kind.room_id()); // Show this error in the loading pane, which should already be open. loading_pane.set_state(cx, LoadingPaneState::Error( - String::from("Unable to find related message; it may have been deleted.") + tr_key(self.app_language, "room_screen.loading.related_message_not_found").to_string() )); } @@ -2472,7 +2523,12 @@ impl RoomScreen { error!("Pagination error ({direction}) in {:?}: {error:?}", self.room_name_id); let room_name = self.room_name_id.as_ref().map(|r| r.to_string()); enqueue_popup_notification( - utils::stringify_pagination_error(&error, room_name.as_deref().unwrap_or(UNNAMED_ROOM)), + utils::stringify_pagination_error( + &error, + room_name + .as_deref() + .unwrap_or(tr_key(self.app_language, "room_screen.fallback.unnamed_room")), + ), PopupKind::Error, Some(10.0), ); @@ -2562,17 +2618,29 @@ impl RoomScreen { TimelineUpdate::PinResult { result, pin, .. } => { let (message, auto_dismissal_duration, kind) = match &result { Ok(true) => ( - format!("Successfully {} event.", if pin { "pinned" } else { "unpinned" }), + if pin { + tr_key(self.app_language, "room_screen.popup.pin.pinned_success").to_string() + } else { + tr_key(self.app_language, "room_screen.popup.pin.unpinned_success").to_string() + }, Some(4.0), PopupKind::Success ), Ok(false) => ( - format!("Message was already {}.", if pin { "pinned" } else { "unpinned" }), + if pin { + tr_key(self.app_language, "room_screen.popup.pin.already_pinned").to_string() + } else { + tr_key(self.app_language, "room_screen.popup.pin.already_unpinned").to_string() + }, Some(4.0), PopupKind::Info ), Err(e) => ( - format!("Failed to {} event. Error: {e}", if pin { "pin" } else { "unpin" }), + tr_fmt(self.app_language, if pin { + "room_screen.popup.pin.pin_failed" + } else { + "room_screen.popup.pin.unpin_failed" + }, &[("error", &e.to_string())]), None, PopupKind::Error ), @@ -2695,7 +2763,7 @@ impl RoomScreen { MatrixId::Room(room_id) => { if self.room_name_id.as_ref().is_some_and(|r| r.room_id() == room_id) { enqueue_popup_notification( - "You are already viewing that room.", + tr_key(self.app_language, "room_screen.popup.already_viewing_room"), PopupKind::Info, Some(4.0), ); @@ -2745,7 +2813,7 @@ impl RoomScreen { if let Err(e) = robius_open::Uri::new(&url).open() { error!("Failed to open URL {:?}. Error: {:?}", url, e); enqueue_popup_notification( - format!("Could not open URL: {url}"), + tr_fmt(self.app_language, "room_screen.popup.open_url_failed", &[("url", url.as_str())]), PopupKind::Error, Some(10.0), ); @@ -2760,7 +2828,7 @@ impl RoomScreen { if let Err(e) = robius_open::Uri::new(&url).open() { error!("Failed to open URL {:?}. Error: {:?}", url, e); enqueue_popup_notification( - format!("Could not open URL: {url}"), + tr_fmt(self.app_language, "room_screen.popup.open_url_failed", &[("url", url.as_str())]), PopupKind::Error, Some(10.0), ); @@ -2860,7 +2928,7 @@ impl RoomScreen { } else { enqueue_popup_notification( - "Could not find message in timeline to reply to. Please try again.", + tr_key(self.app_language, "room_screen.popup.message.reply_not_found"), PopupKind::Error, Some(5.0), ); @@ -2883,7 +2951,7 @@ impl RoomScreen { } else { enqueue_popup_notification( - "Could not find message in timeline to edit. Please try again.", + tr_key(self.app_language, "room_screen.popup.message.edit_not_found"), PopupKind::Error, Some(5.0), ); @@ -2911,7 +2979,7 @@ impl RoomScreen { } else { enqueue_popup_notification( - "No recent message available to edit. Please manually select a message to edit.", + tr_key(self.app_language, "room_screen.popup.message.no_recent_editable"), PopupKind::Warning, Some(5.0), ); @@ -2927,7 +2995,7 @@ impl RoomScreen { }); } else { enqueue_popup_notification( - "This event cannot be pinned.", + tr_key(self.app_language, "room_screen.popup.message.cannot_pin"), PopupKind::Error, Some(5.0), ); @@ -2943,7 +3011,7 @@ impl RoomScreen { }); } else { enqueue_popup_notification( - "This event cannot be unpinned.", + tr_key(self.app_language, "room_screen.popup.message.cannot_unpin"), PopupKind::Error, Some(5.0), ); @@ -2956,7 +3024,7 @@ impl RoomScreen { } else { enqueue_popup_notification( - "Could not find message in timeline to copy text from. Please try again.", + tr_key(self.app_language, "room_screen.popup.message.copy_text_not_found"), PopupKind::Error, Some(5.0), ); @@ -2993,7 +3061,7 @@ impl RoomScreen { } if !success { enqueue_popup_notification( - "Could not find message in timeline to copy HTML from. Please try again.", + tr_key(self.app_language, "room_screen.popup.message.copy_html_not_found"), PopupKind::Error, Some(5.0), ); @@ -3011,7 +3079,7 @@ impl RoomScreen { cx.copy_to_clipboard(&matrix_to_uri.to_string()); } else { enqueue_popup_notification( - "Couldn't create permalink to message. Please try again.", + tr_key(self.app_language, "room_screen.popup.message.copy_link_failed"), PopupKind::Error, Some(5.0), ); @@ -3026,7 +3094,7 @@ impl RoomScreen { let Some(tl) = self.tl_state.as_ref() else { continue }; let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) else { enqueue_popup_notification( - "Could not find message in timeline to view source.", + tr_key(self.app_language, "room_screen.popup.message.view_source_not_found"), PopupKind::Error, Some(5.0), ); @@ -3050,7 +3118,7 @@ impl RoomScreen { let Some(related_event_id) = details.related_event_id.as_ref() else { error!("BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}"); enqueue_popup_notification( - "Could not find related message or event in timeline.", + tr_key(self.app_language, "room_screen.popup.message.related_not_found"), PopupKind::Error, Some(5.0), ); @@ -3091,10 +3159,11 @@ impl RoomScreen { let timeline_event_id = details.timeline_event_id.clone(); let timeline_kind = tl.kind.clone(); let reason = reason.clone(); + let app_language = self.app_language; let content = ConfirmationModalContent { - title_text: "Delete Message".into(), - body_text: "Are you sure you want to delete this message? This cannot be undone.".into(), - accept_button_text: Some("Delete".into()), + title_text: tr_key(app_language, "room_screen.modal.delete_message.title").into(), + body_text: tr_key(app_language, "room_screen.modal.delete_message.body").into(), + accept_button_text: Some(tr_key(app_language, "room_screen.modal.delete_message.accept").into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::RedactMessage { timeline_kind, @@ -4049,6 +4118,7 @@ fn populate_message_view( list: &mut PortalList, item_id: usize, timeline_kind: &TimelineKind, + app_language: AppLanguage, event_tl_item: &EventTimelineItem, msg_like_content: &MsgLikeContent, prev_event: Option<&Arc>, @@ -4122,6 +4192,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_text_message_content( cx, &html_or_plaintext_ref, + app_language, body, formatted.as_ref(), Some(&mut link_preview_ref), @@ -4160,6 +4231,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_text_message_content( cx, &html_or_plaintext_ref, + app_language, body, formatted.as_ref(), Some(&mut link_preview_ref), @@ -4188,14 +4260,16 @@ fn populate_message_view( } }); let formatted = format!( - "Server notice: {}\n\nNotice type:: {}{}{}", + "{} {}\n\n{}: {}{}{}", + tr_key(app_language, "room_screen.server_notice.title"), sn.body, + tr_key(app_language, "room_screen.server_notice.notice_type"), sn.server_notice_type.as_str(), sn.limit_type.as_ref() - .map(|l| format!("\nLimit type: {}", l.as_str())) + .map(|l| format!("\n{} {}", tr_key(app_language, "room_screen.server_notice.limit_type"), l.as_str())) .unwrap_or_default(), sn.admin_contact.as_ref() - .map(|c| format!("\nAdmin contact: {}", c)) + .map(|c| format!("\n{} {}", tr_key(app_language, "room_screen.server_notice.admin_contact"), c)) .unwrap_or_default(), ); let mut link_preview_ref = @@ -4203,6 +4277,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_text_message_content( cx, &html_or_plaintext_ref, + app_language, &sn.body, Some(&FormattedBody { format: MessageFormat::Html, @@ -4257,6 +4332,7 @@ fn populate_message_view( let link_previews_drawn = populate_text_message_content( cx, &html_or_plaintext_ref, + app_language, &body, formatted.as_ref(), Some(&mut link_preview_ref), @@ -4285,6 +4361,7 @@ fn populate_message_view( let is_image_fully_drawn = populate_image_message_content( cx, &text_or_image_ref, + app_language, image_info, image.source.clone(), msg.body(), @@ -4310,6 +4387,7 @@ fn populate_message_view( let is_location_fully_drawn = populate_location_message_content( cx, &html_or_plaintext_ref, + app_language, location, ); new_drawn_status.content_drawn = is_location_fully_drawn; @@ -4332,6 +4410,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_file_message_content( cx, &html_or_plaintext_ref, + app_language, file_content, ); (item, false) @@ -4353,6 +4432,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_audio_message_content( cx, &html_or_plaintext_ref, + app_language, audio, ); (item, false) @@ -4374,6 +4454,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_video_message_content( cx, &html_or_plaintext_ref, + app_language, video, ); (item, false) @@ -4390,8 +4471,11 @@ fn populate_message_view( let formatted = FormattedBody { format: MessageFormat::Html, body: format!( - "Sent a verification request to {}.
(Supported methods: {})
", - verification.to, + "{}{}{}
({}: {})
", + tr_key(app_language, "room_screen.verification.sent_prefix"), + tr_key(app_language, "room_screen.verification.request"), + tr_fmt(app_language, "room_screen.verification.sent_to_suffix", &[("user_id", verification.to.as_str())]), + tr_key(app_language, "room_screen.verification.supported_methods"), verification.methods .iter() .map(|m| m.as_str()) @@ -4407,6 +4491,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_text_message_content( cx, &html_or_plaintext_ref, + app_language, &verification.body, Some(&formatted), Some(&mut link_preview_ref), @@ -4424,7 +4509,7 @@ fn populate_message_view( } else { item.label(cx, ids!(content.message)).set_text( cx, - &format!("[Unsupported {:?}]", msg_like_content.kind), + &format!("{} {:?}", tr_key(app_language, "room_screen.unsupported.prefix"), msg_like_content.kind), ); new_drawn_status.content_drawn = true; (item, false) @@ -4453,6 +4538,7 @@ fn populate_message_view( let is_image_fully_drawn = populate_image_message_content( cx, &text_or_image_ref, + app_language, Some(Box::new(image_info.clone())), MediaSource::Plain(owned_mxc_url.clone()), body, @@ -4491,6 +4577,7 @@ fn populate_message_view( new_drawn_status.content_drawn = populate_redacted_message_content( cx, &html_or_plaintext_ref, + app_language, event_tl_item, timeline_kind.room_id(), ); @@ -4505,7 +4592,7 @@ fn populate_message_view( } else { item.label(cx, ids!(content.message)).set_text( cx, - &format!("[Unsupported {:?}] ", other), + &format!("{} {:?} ", tr_key(app_language, "room_screen.unsupported.prefix"), other), ); new_drawn_status.content_drawn = true; (item, false) @@ -4530,6 +4617,7 @@ fn populate_message_view( cx, &item.view(cx, ids!(replied_to_message)), timeline_kind, + app_language, msg_like_content.in_reply_to.as_ref(), event_tl_item.event_id(), ); @@ -4538,6 +4626,7 @@ fn populate_message_view( &item, item_id, timeline_kind, + app_language, msg_like_content, event_tl_item, fetched_thread_summaries, @@ -4611,7 +4700,7 @@ fn populate_message_view( // Server notices are drawn with a red color avatar background and username. let avatar = item.avatar(cx, ids!(profile.avatar)); avatar.show_text(cx, Some(COLOR_FG_DANGER_RED), None, "⚠"); - username_label.set_text(cx, "Server notice"); + username_label.set_text(cx, tr_key(app_language, "room_screen.server_notice.username")); script_apply_eval!(cx, username_label, { draw_text +: { color: (mod.widgets.COLOR_FG_DANGER_RED) @@ -4681,6 +4770,7 @@ fn populate_message_view( fn populate_text_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, body: &str, formatted_body: Option<&FormattedBody>, link_preview_ref: Option<&mut LinkPreviewRef>, @@ -4717,7 +4807,17 @@ fn populate_text_message_content( &links, media_cache, link_preview_cache, - &populate_image_message_content, + &|cx, text_or_image_ref, image_info_source, original_source, body, media_cache| { + populate_image_message_content( + cx, + text_or_image_ref, + app_language, + image_info_source, + original_source, + body, + media_cache, + ) + }, ) } else { true @@ -4730,6 +4830,7 @@ fn populate_text_message_content( fn populate_image_message_content( cx: &mut Cx, text_or_image_ref: &TextOrImageRef, + app_language: AppLanguage, image_info_source: Option>, original_source: MediaSource, body: &str, @@ -4747,7 +4848,7 @@ fn populate_image_message_content( if ImageFormat::from_mimetype(mime).is_none() { text_or_image_ref.show_text( cx, - format!("{body}\n\nUnsupported type {mime:?}"), + tr_fmt(app_language, "room_screen.image.unsupported_type", &[("body", body), ("mime", mime)]), ); return true; // consider this as fully drawn } @@ -4765,7 +4866,7 @@ fn populate_image_message_content( .map(|()| img.size_in_pixels(cx).unwrap_or_default()) }); if let Err(e) = show_image_result { - let err_str = format!("{body}\n\nFailed to display image: {e:?}"); + let err_str = tr_fmt(app_language, "room_screen.image.failed_to_display", &[("body", body), ("error", &format!("{e:?}"))]); error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } @@ -4813,7 +4914,7 @@ fn populate_image_message_content( } }); if let Err(e) = show_image_result { - let err_str = format!("{body}\n\nFailed to display image: {e:?}"); + let err_str = tr_fmt(app_language, "room_screen.image.failed_to_display", &[("body", body), ("error", &format!("{e:?}"))]); error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } @@ -4826,7 +4927,7 @@ fn populate_image_message_content( return; } text_or_image_ref - .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri)); + .show_text(cx, tr_fmt(app_language, "room_screen.image.failed_to_fetch", &[("body", body), ("mxc_uri", &format!("{mxc_uri:?}"))])); // For now, we consider this as being "complete". In the future, we could support // retrying to fetch thumbnail of the image on a user click/tap. fully_drawn = true; @@ -4840,7 +4941,7 @@ fn populate_image_message_content( // We consider this as "fully drawn" since we don't yet support encryption. text_or_image_ref.show_text( cx, - format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) + tr_fmt(app_language, "room_screen.image.encrypted_todo", &[("body", body), ("url", &format!("{:?}", encrypted.url))]) ); }, MediaSource::Plain(mxc_uri) => { @@ -4857,7 +4958,7 @@ fn populate_image_message_content( fetch_and_show_media_source(cx, media_source, image_info); } None => { - text_or_image_ref.show_text(cx, "{body}\n\nImage message had no source URL."); + text_or_image_ref.show_text(cx, tr_fmt(app_language, "room_screen.image.no_source_url", &[("body", body)])); fully_drawn = true; } } @@ -4872,6 +4973,7 @@ fn populate_image_message_content( fn populate_file_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, file_content: &FileMessageEventContent, ) -> bool { // Display the file name, human-readable size, caption, and a button to download it. @@ -4891,7 +4993,11 @@ fn populate_file_message_content( message_content_widget.show_html( cx, - format!("{filename}{size}{caption}
File download not yet supported."), + tr_fmt(app_language, "room_screen.file.preview_html", &[ + ("filename", &filename), + ("size", size.as_str()), + ("caption", caption.as_str()), + ]), ); true } @@ -4902,6 +5008,7 @@ fn populate_file_message_content( fn populate_audio_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, audio: &AudioMessageEventContent, ) -> bool { // Display the file name, human-readable size, caption, and a button to download it. @@ -4931,7 +5038,13 @@ fn populate_audio_message_content( message_content_widget.show_html( cx, - format!("Audio: {filename}{mime}{duration}{size}{caption}
Audio playback not yet supported."), + tr_fmt(app_language, "room_screen.audio.preview_html", &[ + ("filename", &filename), + ("mime", mime.as_str()), + ("duration", duration.as_str()), + ("size", size.as_str()), + ("caption", caption.as_str()), + ]), ); true } @@ -4943,6 +5056,7 @@ fn populate_audio_message_content( fn populate_video_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, video: &VideoMessageEventContent, ) -> bool { // Display the file name, human-readable size, caption, and a button to download it. @@ -4975,7 +5089,14 @@ fn populate_video_message_content( message_content_widget.show_html( cx, - format!("Video: {filename}{mime}{duration}{size}{dimensions}{caption}
Video playback not yet supported."), + tr_fmt(app_language, "room_screen.video.preview_html", &[ + ("filename", &filename), + ("mime", mime.as_str()), + ("duration", duration.as_str()), + ("size", size.as_str()), + ("dimensions", dimensions.as_str()), + ("caption", caption.as_str()), + ]), ); true } @@ -4988,6 +5109,7 @@ fn populate_video_message_content( fn populate_location_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, location: &LocationMessageEventContent, ) -> bool { let coords = location.geo_uri @@ -5009,19 +5131,26 @@ fn populate_location_message_content( let safe_short_lat = htmlize::escape_text(short_lat); let safe_short_long = htmlize::escape_text(short_long); let html_body = format!( - "Location: {safe_short_lat},{safe_short_long}
\ + "{} {safe_short_lat},{safe_short_long}
\ ", + tr_key(app_language, "room_screen.location.label"), safe_geo_uri, + tr_key(app_language, "room_screen.location.open_osm"), + tr_key(app_language, "room_screen.location.open_google_maps"), + tr_key(app_language, "room_screen.location.open_apple_maps"), ); message_content_widget.show_html(cx, html_body); } else { + let escaped_body = htmlize::escape_text(&location.body); message_content_widget.show_html( cx, - format!("[Location invalid] {}", htmlize::escape_text(&location.body)) + tr_fmt(app_language, "room_screen.location.invalid_html", &[ + ("body", &escaped_body), + ]) ); } @@ -5038,6 +5167,7 @@ fn populate_location_message_content( fn populate_redacted_message_content( cx: &mut Cx, message_content_widget: &HtmlOrPlaintextRef, + app_language: AppLanguage, event_tl_item: &EventTimelineItem, room_id: &OwnedRoomId, ) -> bool { @@ -5062,8 +5192,13 @@ fn populate_redacted_message_content( if redactor == event_tl_item.sender() { fully_drawn = true; match reason { - Some(r) => format!("⛔ Deleted their own message. Reason: \"{}\".", htmlize::escape_text(r)), - None => String::from("⛔ Deleted their own message."), + Some(r) => { + let escaped_reason = htmlize::escape_text(r); + tr_fmt(app_language, "room_screen.redacted.self_with_reason", &[ + ("reason", &escaped_reason), + ]) + } + None => tr_key(app_language, "room_screen.redacted.self").to_string(), } } else { // Try to get the displayable name of the user who redacted this message. @@ -5076,16 +5211,21 @@ fn populate_redacted_message_content( fully_drawn = redactor_name.was_found(); let redactor_name_esc = htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); match reason { - Some(r) => format!("⛔ {} deleted this message. Reason: \"{}\".", - redactor_name_esc, - htmlize::escape_text(r), - ), - None => format!("⛔ {} deleted this message.", redactor_name_esc), + Some(r) => { + let escaped_reason = htmlize::escape_text(r); + tr_fmt(app_language, "room_screen.redacted.other_with_reason", &[ + ("redactor", &redactor_name_esc), + ("reason", &escaped_reason), + ]) + } + None => tr_fmt(app_language, "room_screen.redacted.other", &[ + ("redactor", &redactor_name_esc), + ]), } } } else { fully_drawn = true; - String::from("⛔ Message deleted.") + tr_key(app_language, "room_screen.redacted.generic").to_string() }; message_content_widget.show_html(cx, html); fully_drawn @@ -5108,6 +5248,7 @@ fn draw_replied_to_message( cx: &mut Cx2d, replied_to_message_view: &ViewRef, timeline_kind: &TimelineKind, + app_language: AppLanguage, in_reply_to: Option<&InReplyToDetails>, message_event_id: Option<&EventId>, ) -> bool { @@ -5139,6 +5280,7 @@ fn draw_replied_to_message( populate_preview_of_timeline_item( cx, &msg_body, + app_language, &replied_to_event.content, &replied_to_event.sender, &in_reply_to_username, @@ -5148,26 +5290,26 @@ fn draw_replied_to_message( fully_drawn = true; replied_to_message_view .label(cx, ids!(replied_to_message_content.reply_preview_username)) - .set_text(cx, "[Error fetching username]"); + .set_text(cx, tr_key(app_language, "room_screen.reply_preview.error_username")); replied_to_message_view .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) .show_text(cx, None, None, "?"); replied_to_message_view .html_or_plaintext(cx, ids!(replied_to_message_content.reply_preview_body)) - .show_plaintext(cx, "[Error fetching replied-to event]"); + .show_plaintext(cx, tr_key(app_language, "room_screen.reply_preview.error_event")); } td @ TimelineDetails::Pending | td @ TimelineDetails::Unavailable => { // We don't have the replied-to message yet, so we can't fully draw the preview. fully_drawn = false; replied_to_message_view .label(cx, ids!(replied_to_message_content.reply_preview_username)) - .set_text(cx, "[Loading username...]"); + .set_text(cx, tr_key(app_language, "room_screen.reply_preview.loading_username")); replied_to_message_view .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) .show_text(cx, None, None, "?"); replied_to_message_view .html_or_plaintext(cx, ids!(replied_to_message_content.reply_preview_body)) - .show_plaintext(cx, "[Loading replied-to message...]"); + .show_plaintext(cx, tr_key(app_language, "room_screen.reply_preview.loading_event")); // Confusingly, we need to fetch the details of the `message` (the event that is the reply), // not the details of the original event that this `message` is replying to. @@ -5200,6 +5342,7 @@ fn populate_thread_root_summary( item: &WidgetRef, timeline_item_index: usize, timeline_kind: &TimelineKind, + app_language: AppLanguage, msg_like_content: &MsgLikeContent, event_tl_item: &EventTimelineItem, fetched_thread_summaries: &HashMap, @@ -5269,18 +5412,18 @@ fn populate_thread_root_summary( } } fetched_summary.and_then(|fs| fs.latest_reply_preview_text.as_deref()) - .unwrap_or("Loading latest reply...") + .unwrap_or(tr_key(app_language, "room_screen.thread_summary.loading_latest_reply")) .into() } TimelineDetails::Error(_) => { fully_drawn = true; // consider this fully drawn since there's no point retrying. - "Unable to load latest reply".into() + tr_key(app_language, "room_screen.thread_summary.error_latest_reply").into() } }; let replies_count_text = match replies_count { - 1 => Cow::Borrowed("1 reply"), - n => Cow::Owned(format!("{n} replies")) + 1 => Cow::Borrowed(tr_key(app_language, "room_screen.thread_summary.one_reply")), + n => Cow::Owned(tr_fmt(app_language, "room_screen.thread_summary.n_replies", &[("n", &n.to_string())])) }; item.label(cx, ids!(thread_summary_count)) .set_text(cx, &replies_count_text); @@ -5294,6 +5437,7 @@ fn populate_thread_root_summary( pub fn populate_preview_of_timeline_item( cx: &mut Cx, widget_out: &HtmlOrPlaintextRef, + app_language: AppLanguage, timeline_item_content: &TimelineItemContent, sender_user_id: &UserId, sender_username: &str, @@ -5302,7 +5446,7 @@ pub fn populate_preview_of_timeline_item( match m.msgtype() { MessageType::Text(TextMessageEventContent { body, formatted, .. }) | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { - let _ = populate_text_message_content(cx, widget_out, body, formatted.as_ref(), None, None, None); + let _ = populate_text_message_content(cx, widget_out, app_language, body, formatted.as_ref(), None, None, None); return; } _ => { } // fall through to the general case for all timeline items below. @@ -5506,6 +5650,7 @@ fn populate_small_state_event( list: &mut PortalList, item_id: usize, timeline_kind: &TimelineKind, + app_language: AppLanguage, event_tl_item: &EventTimelineItem, event_content: &impl SmallStateEventContent, item_drawn_status: ItemDrawnStatus, @@ -5548,7 +5693,7 @@ fn populate_small_state_event( }); // Proceed to draw the actual event content. - event_content.populate_item_content( + let (item, new_drawn_status) = event_content.populate_item_content( cx, list, item_id, @@ -5557,7 +5702,12 @@ fn populate_small_state_event( &username, item_drawn_status, new_drawn_status, - ) + ); + + item.button(cx, ids!(invite_user_button)) + .set_text(cx, tr_key(app_language, "room_screen.small_state.invite_to_room")); + + (item, new_drawn_status) } @@ -5705,10 +5855,18 @@ impl ActionDefaultRef for AppServicePanelAction { #[derive(Script, ScriptHook, Widget)] pub struct AppServicePanel { #[deref] view: View, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, } impl Widget for AppServicePanel { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); let room_screen_props = scope @@ -5786,10 +5944,54 @@ impl Widget for AppServicePanel { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } +impl AppServicePanel { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.view + .label(cx, ids!(sender_row.sender_name)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.sender_name")); + self.view + .label(cx, ids!(sender_row.sender_tag)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.sender_tag")); + self.view + .label(cx, ids!(bubble.header.title)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.title")); + self.view + .label(cx, ids!(bubble.subtitle)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.subtitle")); + self.view + .label(cx, ids!(bubble.footer.timestamp)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.timestamp_now")); + self.view + .button(cx, ids!(keyboard.first_row.create_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.create_bot")); + self.view + .button(cx, ids!(keyboard.first_row.list_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.list_bots")); + self.view + .button(cx, ids!(keyboard.second_row.delete_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.delete_bot")); + self.view + .button(cx, ids!(keyboard.second_row.help_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.bot_help")); + self.view + .button(cx, ids!(keyboard.third_row.unbind_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.unbind")); + self.view.redraw(cx); + } +} + /// A widget representing a single message of any kind within a room timeline. #[derive(Script, ScriptHook, Widget, Animator)] pub struct Message { diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 83e706223..fd58b7134 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -1490,9 +1490,10 @@ impl Widget for RoomsList { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - let app_state = scope.data.get_mut::().unwrap(); + let app_state = scope.data.get::().unwrap(); // Update the currently-selected room from the AppState data. self.current_active_room = app_state.selected_room.clone(); + let mut app_state_for_item_scope = app_state.clone(); // Based on the various displayed room lists and is_expanded state of each room header, // calculate the indexes in the PortalList where the headers and rooms should be drawn. @@ -1541,8 +1542,7 @@ impl Widget for RoomsList { list.set_item_range(cx, 0, total_count); while let Some(portal_list_index) = list.next_visible_item(cx) { - let mut scope = Scope::empty(); - + let mut item_scope = Scope::with_data(&mut app_state_for_item_scope); if self.invited_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( @@ -1551,7 +1551,7 @@ impl Widget for RoomsList { HeaderCategory::Invites, self.displayed_invited_rooms.len() as u64, ); - item.draw_all(cx, &mut scope); + item.draw_all(cx, &mut item_scope); } else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { let mut invited_rooms_mut = self.invited_rooms.borrow_mut(); @@ -1560,11 +1560,12 @@ impl Widget for RoomsList { invited_room.is_selected = self.current_active_room.as_ref() .is_some_and(|sel_room| sel_room.room_id() == invited_room_id); // Pass the room info down to the RoomsListEntry widget via Scope. - scope = Scope::with_props(&*invited_room); - item.draw_all(cx, &mut scope); + item_scope.override_props(&*invited_room, |scope| { + item.draw_all(cx, scope); + }); } else { list.item(cx, portal_list_index, id!(empty)) - .draw_all(cx, &mut scope); + .draw_all(cx, &mut item_scope); } } else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { @@ -1577,7 +1578,7 @@ impl Widget for RoomsList { // TODO: sum up all the unread mentions in rooms // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); - item.draw_all(cx, &mut scope); + item.draw_all(cx, &mut item_scope); } else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { if let Some(direct_room) = self.all_joined_rooms.get_mut(direct_room_id) { @@ -1597,11 +1598,12 @@ impl Widget for RoomsList { }); } // Pass the room info down to the RoomsListEntry widget via Scope. - scope = Scope::with_props(&*direct_room); - item.draw_all(cx, &mut scope); + item_scope.override_props(&*direct_room, |scope| { + item.draw_all(cx, scope); + }); } else { list.item(cx, portal_list_index, id!(empty)) - .draw_all(cx, &mut scope); + .draw_all(cx, &mut item_scope); } } else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { @@ -1614,7 +1616,7 @@ impl Widget for RoomsList { // TODO: sum up all the unread mentions in rooms. // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); - item.draw_all(cx, &mut scope); + item.draw_all(cx, &mut item_scope); } else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { if let Some(regular_room) = self.all_joined_rooms.get_mut(regular_room_id) { @@ -1634,22 +1636,23 @@ impl Widget for RoomsList { }); } // Pass the room info down to the RoomsListEntry widget via Scope. - scope = Scope::with_props(&*regular_room); - item.draw_all(cx, &mut scope); + item_scope.override_props(&*regular_room, |scope| { + item.draw_all(cx, scope); + }); } else { - list.item(cx, portal_list_index, id!(empty)).draw_all(cx, &mut scope); + list.item(cx, portal_list_index, id!(empty)).draw_all(cx, &mut item_scope); } } // Draw the status label as the bottom entry. else if portal_list_index == status_label_id { let item = list.item(cx, portal_list_index, id!(status_label)); item.label(cx, ids!(label)).set_text(cx, &self.status); - item.draw_all(cx, &mut scope); + item.draw_all(cx, &mut item_scope); } // Draw a filler entry to take up space at the bottom of the portal list. else { list.item(cx, portal_list_index, id!(bottom_filler)) - .draw_all(cx, &mut scope); + .draw_all(cx, &mut item_scope); } } } diff --git a/src/home/rooms_list_entry.rs b/src/home/rooms_list_entry.rs index 13494c590..5870fc08a 100644 --- a/src/home/rooms_list_entry.rs +++ b/src/home/rooms_list_entry.rs @@ -2,6 +2,8 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use crate::{ + app::AppState, + i18n::{AppLanguage, tr_fmt, tr_key}, room::FetchedRoomAvatar, shared::{ avatar::AvatarWidgetExt, html_or_plaintext::HtmlOrPlaintextWidgetExt, unread_badge::UnreadBadgeWidgetExt as _, @@ -302,10 +304,13 @@ impl Widget for RoomsListEntryContent { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); if let Some(joined_room_info) = scope.props.get::() { self.draw_joined_room(cx, joined_room_info); } else if let Some(invited_room_info) = scope.props.get::() { - self.draw_invited_room(cx, invited_room_info); + self.draw_invited_room(cx, invited_room_info, app_language); } self.view.draw_walk(cx, scope, walk) @@ -346,14 +351,30 @@ impl RoomsListEntryContent { &mut self, cx: &mut Cx, room_info: &InvitedRoomInfo, + app_language: AppLanguage, ) { self.view.label(cx, ids!(room_name)).set_text(cx, &room_info.room_name_id.to_string()); // Hide the timestamp field, and use the latest message field to show the inviter. self.view.label(cx, ids!(timestamp)).set_text(cx, ""); let inviter_string = match &room_info.inviter_info { - Some(InviterInfo { user_id, display_name: Some(dn), .. }) => format!("Invited by {} ({})", htmlize::escape_text(dn), htmlize::escape_text(user_id.as_str())), - Some(InviterInfo { user_id, .. }) => format!("Invited by {}", htmlize::escape_text(user_id.as_str())), - None => String::from("You were invited"), + Some(InviterInfo { user_id, display_name: Some(dn), .. }) => { + let display_name = htmlize::escape_text(dn); + let user_id = htmlize::escape_text(user_id.as_str()); + tr_fmt( + app_language, + "rooms_list_entry.invited.by_name_and_user", + &[("display_name", display_name.as_ref()), ("user_id", user_id.as_ref())], + ) + } + Some(InviterInfo { user_id, .. }) => { + let user_id = htmlize::escape_text(user_id.as_str()); + tr_fmt( + app_language, + "rooms_list_entry.invited.by_user", + &[("user_id", user_id.as_ref())], + ) + } + None => tr_key(app_language, "rooms_list_entry.invited.generic").to_string(), }; self.view.html_or_plaintext(cx, ids!(latest_message)).show_html(cx, &inviter_string); diff --git a/src/home/rooms_list_header.rs b/src/home/rooms_list_header.rs index d4aaa3a8e..085031c55 100644 --- a/src/home/rooms_list_header.rs +++ b/src/home/rooms_list_header.rs @@ -9,7 +9,9 @@ use makepad_widgets::*; use matrix_sdk_ui::sync_service::State; use crate::{ + app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, + i18n::{AppLanguage, tr_key}, shared::{ image_viewer::{ImageViewerAction, ImageViewerError, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, @@ -130,10 +132,18 @@ pub struct RoomsListHeader { #[deref] view: View, #[rust(State::Idle)] sync_state: State, + #[rust] app_language: AppLanguage, + #[rust] showing_space_title: bool, } impl Widget for RoomsListHeader { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } if let Event::Actions(actions) = event { if self.view.button(cx, ids!(open_room_filter_modal_button.click_area)).clicked(actions) { cx.action(RoomsListHeaderAction::OpenRoomFilterModal); @@ -162,7 +172,7 @@ impl Widget for RoomsListHeader { self.view.view(cx, ids!(synced_icon)).set_visible(cx, false); self.view.view(cx, ids!(offline_icon)).set_visible(cx, true); enqueue_popup_notification( - "Cannot reach the Matrix homeserver. Please check your connection.", + tr_key(self.app_language, "rooms_list_header.popup.offline"), PopupKind::Error, None, ); @@ -181,8 +191,12 @@ impl Widget for RoomsListHeader { match tab { SelectedTab::Space { space_name_id } => { header_title.set_text(cx, &space_name_id.to_string()); + self.showing_space_title = true; + } + _ => { + header_title.set_text(cx, tr_key(self.app_language, "rooms_list_header.title.all_rooms")); + self.showing_space_title = false; } - _ => header_title.set_text(cx, "All Rooms"), } continue; } @@ -191,9 +205,9 @@ impl Widget for RoomsListHeader { // Show tooltips for the sync status icons. for (view, text, bg_color) in [ - (self.view.view(cx, ids!(loading_spinner)), "Syncing...", vec4(0.059, 0.533, 0.996, 1.0)), // COLOR_ACTIVE_PRIMARY #0f88fe - (self.view.view(cx, ids!(offline_icon)), "Offline", vec4(0.863, 0.0, 0.020, 1.0)), // COLOR_FG_DANGER_RED #DC0005 - (self.view.view(cx, ids!(synced_icon)), "Fully synced", vec4(0.075, 0.533, 0.031, 1.0)), // COLOR_FG_ACCEPT_GREEN #138808 + (self.view.view(cx, ids!(loading_spinner)), tr_key(self.app_language, "rooms_list_header.tooltip.syncing"), vec4(0.059, 0.533, 0.996, 1.0)), // COLOR_ACTIVE_PRIMARY #0f88fe + (self.view.view(cx, ids!(offline_icon)), tr_key(self.app_language, "rooms_list_header.tooltip.offline"), vec4(0.863, 0.0, 0.020, 1.0)), // COLOR_FG_DANGER_RED #DC0005 + (self.view.view(cx, ids!(synced_icon)), tr_key(self.app_language, "rooms_list_header.tooltip.synced"), vec4(0.075, 0.533, 0.031, 1.0)), // COLOR_FG_ACCEPT_GREEN #138808 ] { if !view.visible() { continue; @@ -225,10 +239,28 @@ impl Widget for RoomsListHeader { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } +impl RoomsListHeader { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + if !self.showing_space_title { + self.view + .label(cx, ids!(header_title)) + .set_text(cx, tr_key(self.app_language, "rooms_list_header.title.all_rooms")); + } + self.view.redraw(cx); + } +} + /// Actions that can be handled by the `RoomsListHeader`. #[derive(Debug)] pub enum RoomsListHeaderAction { diff --git a/src/home/search_messages.rs b/src/home/search_messages.rs index 5228ca129..75ac7afa9 100644 --- a/src/home/search_messages.rs +++ b/src/home/search_messages.rs @@ -2,6 +2,7 @@ //! UI widgets for searching messages in one or more rooms. use makepad_widgets::*; +use crate::{app::AppState, i18n::{AppLanguage, tr_key}}; script_mod! { use mod.prelude.widgets.* @@ -47,10 +48,17 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct SearchMessagesButton { #[deref] button: Button, + #[rust] app_language: AppLanguage, } impl Widget for SearchMessagesButton { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.button.handle_event(cx, event, scope); if let Event::Actions(actions) = event { @@ -61,10 +69,23 @@ impl Widget for SearchMessagesButton { } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.button.draw_walk(cx, scope, walk) } } +impl SearchMessagesButton { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.button.set_text(cx, tr_key(self.app_language, "search_messages.button.todo")); + } +} + #[derive(Debug)] pub enum AddRoomAction { SearchMessagesButtonClicked, diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index eb9b8277c..487c8be52 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -19,13 +19,14 @@ use crate::shared::avatar::AvatarState; use crate::shared::expand_arrow::ExpandArrow; use crate::utils::replace_linebreaks_separators; use crate::{ - app::AppStateAction, + app::{AppState, AppStateAction}, avatar_cache::{self, AvatarCacheEntry}, home::{ add_room::{CreateRoomAction, CreateRoomModalAction}, invite_modal::InviteModalAction, rooms_list::RoomsListRef, }, + i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::BasicRoomDetails, shared::avatar::{AvatarWidgetExt, AvatarWidgetRefExt}, @@ -152,7 +153,7 @@ script_mod! { ) } } - text: "Explore this Space" + text: "" } animator: Animator{ @@ -323,7 +324,7 @@ script_mod! { spacing: 0 icon_walk: Walk{width: 0, height: 0} draw_text.text_style: REGULAR_TEXT {font_size: 9.5} - text: "Join" + text: "" } view_button := RobrixIconButton { @@ -332,7 +333,7 @@ script_mod! { spacing: 0 icon_walk: Walk{width: 0, height: 0} draw_text.text_style: REGULAR_TEXT {font_size: 9.5} - text: "View" + text: "" } leave_button := RobrixNegativeIconButton { @@ -341,7 +342,7 @@ script_mod! { spacing: 0 icon_walk: Walk{width: 0, height: 0} draw_text.text_style: REGULAR_TEXT {font_size: 9.5} - text: "Leave" + text: "" } } @@ -389,7 +390,7 @@ script_mod! { color: #737373, text_style: REGULAR_TEXT {font_size: 10} } - text: "Loading rooms and spaces..." + text: "" } } @@ -420,7 +421,7 @@ script_mod! { text_style: REGULAR_TEXT {font_size: 9}, color: #888, } - text: "Loading..." + text: "" } } @@ -455,7 +456,7 @@ script_mod! { text_style: REGULAR_TEXT {font_size: 10}, color: #737373, } - text: "Welcome to the space:" + text: "" } parent_space_row := View { @@ -490,7 +491,7 @@ script_mod! { padding: 12, draw_icon.svg: (ICON_ADD) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } - text: "New Room" + text: "" } invite_button := RobrixPositiveIconButton { @@ -500,7 +501,7 @@ script_mod! { padding: 12, draw_icon.svg: (ICON_ADD_USER) icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } - text: "Invite" + text: "" } } } @@ -584,6 +585,11 @@ impl Widget for SpaceLobbyEntry { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.view.label(cx, ids!(space_lobby_label)) + .set_text(cx, tr_key(app_language, "space_lobby.entry.explore_space")); self.view.draw_walk(cx, scope, walk) } } @@ -877,10 +883,21 @@ pub struct SpaceLobbyScreen { /// Whether we are currently loading the initial data. #[rust] is_loading: bool, + #[rust] top_level_join_rule: Option, + #[rust] top_level_member_count: Option, + #[rust] app_language: AppLanguage, } impl Widget for SpaceLobbyScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.app_language = app_language; + self.update_space_info_label(cx, app_language); + self.redraw(cx); + } self.view.handle_event(cx, event, scope); // Handle Signal events for avatar cache updates @@ -902,15 +919,9 @@ impl Widget for SpaceLobbyScreen { if self.space_name_id.as_ref().is_some_and(|sni| sni.room_id() == &sr.room_id) { self.space_avatar_state = AvatarState::Known(sr.avatar_url.clone()); self.space_avatar_state.update_from_cache(cx); // prefetch the avatar image - self.view.label(cx, ids!(header.space_info_label)).set_text(cx, &format!( - "{} · {} {}", - match sr.join_rule { - Some(JoinRuleSummary::Public) => "🌐 Public space", - _ => "🔒 Private space", - }, - sr.num_joined_members, - if sr.num_joined_members == 1 { "member" } else { "members" } - )); + self.top_level_join_rule = sr.join_rule.clone(); + self.top_level_member_count = Some(sr.num_joined_members); + self.update_space_info_label(cx, app_language); self.redraw(cx); } } @@ -1006,6 +1017,11 @@ impl Widget for SpaceLobbyScreen { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.app_language = app_language; + // Draw parent avatar from the SpaceRoom's avatar URL, or show initials. let parent_avatar_ref = self.view.avatar(cx, ids!(parent_avatar)); if self.space_avatar_state.update_from_cache(cx).is_none_or(|data| { @@ -1019,6 +1035,12 @@ impl Widget for SpaceLobbyScreen { .and_then(|name| utils::user_name_first_letter(name)); parent_avatar_ref.show_text(cx, None, None, first_char.unwrap_or("S")); } + + self.update_space_info_label(cx, app_language); + self.view.button(cx, ids!(header.parent_space_row.create_room_button)) + .set_text(cx, tr_key(app_language, "space_lobby.header.button.new_room")); + self.view.button(cx, ids!(header.parent_space_row.invite_button)) + .set_text(cx, tr_key(app_language, "space_lobby.header.button.invite")); while let Some(widget_to_draw) = self.view.draw_walk(cx, scope, walk).step() { let portal_list_ref = widget_to_draw.as_portal_list(); @@ -1041,13 +1063,20 @@ impl Widget for SpaceLobbyScreen { // Draw loading indicator let item = if self.is_loading && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); - item.child_by_path(ids!(label)).as_label().set_text(cx, "Loading rooms and spaces..."); + item.child_by_path(ids!(label)).as_label().set_text( + cx, + tr_key(app_language, "space_lobby.status.loading_rooms_spaces"), + ); + item.child_by_path(ids!(loading_spinner)).set_visible(cx, true); item } // No entries found else if entry_count == 0 && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); - item.child_by_path(ids!(label)).as_label().set_text(cx, "No rooms or spaces found."); + item.child_by_path(ids!(label)).as_label().set_text( + cx, + tr_key(app_language, "space_lobby.status.no_rooms_spaces"), + ); item.child_by_path(ids!(loading_spinner)).set_visible(cx, false); item } @@ -1102,6 +1131,18 @@ impl Widget for SpaceLobbyScreen { item.child_by_path(ids!(buttons_view.join_button)).set_visible(cx, show_join_button); item.child_by_path(ids!(buttons_view.leave_button)).set_visible(cx, show_leave_button); item.child_by_path(ids!(buttons_view.view_button)).set_visible(cx, show_view_button); + item.child_by_path(ids!(buttons_view.join_button)).as_button().set_text( + cx, + tr_key(app_language, "space_lobby.item.button.join"), + ); + item.child_by_path(ids!(buttons_view.leave_button)).as_button().set_text( + cx, + tr_key(app_language, "space_lobby.item.button.leave"), + ); + item.child_by_path(ids!(buttons_view.view_button)).as_button().set_text( + cx, + tr_key(app_language, "space_lobby.item.button.view"), + ); // Below, draw things that are common to child rooms and subspaces. item.child_by_path(ids!(content.name_label)).as_label().set_text(cx, &info.name); @@ -1157,29 +1198,31 @@ impl Widget for SpaceLobbyScreen { // Add join status for rooms we haven't joined if let Some(state) = &info.state { match state { - RoomState::Joined => info_parts.push("✅ Joined".to_string()), - RoomState::Left => info_parts.push("Left".to_string()), - RoomState::Invited => info_parts.push("Invited".to_string()), - RoomState::Knocked => info_parts.push("Knocked".to_string()), - RoomState::Banned => info_parts.push("Banned".to_string()), + RoomState::Joined => info_parts.push(tr_key(app_language, "space_lobby.item.state.joined").to_string()), + RoomState::Left => info_parts.push(tr_key(app_language, "space_lobby.item.state.left").to_string()), + RoomState::Invited => info_parts.push(tr_key(app_language, "space_lobby.item.state.invited").to_string()), + RoomState::Knocked => info_parts.push(tr_key(app_language, "space_lobby.item.state.knocked").to_string()), + RoomState::Banned => info_parts.push(tr_key(app_language, "space_lobby.item.state.banned").to_string()), } } // Add member count - info_parts.push(format!( - "{} {}", - info.num_joined_members, - if info.num_joined_members == 1 { "member" } else { "members" } - )); + let member_count = info.num_joined_members.to_string(); + info_parts.push(if info.num_joined_members == 1 { + tr_key(app_language, "space_lobby.item.member_one").to_string() + } else { + tr_fmt(app_language, "space_lobby.item.member_n", &[("count", member_count.as_str())]) + }); // Add children count for spaces if let Some(c) = info.children_count { if c > 0 { - info_parts.push(format!( - "~{} {}", - c, - if c == 1 { "room" } else { "rooms" } - )); + let child_count = c.to_string(); + info_parts.push(if c == 1 { + tr_fmt(app_language, "space_lobby.item.child_room_one", &[("count", child_count.as_str())]) + } else { + tr_fmt(app_language, "space_lobby.item.child_room_n", &[("count", child_count.as_str())]) + }); } } @@ -1195,6 +1238,10 @@ impl Widget for SpaceLobbyScreen { TreeEntry::Loading { level, parent_mask } => { // Draw loading indicator for subspace let item = list.item(cx, item_id, id!(subspace_loading)); + item.child_by_path(ids!(label)).as_label().set_text( + cx, + tr_key(app_language, "space_lobby.status.loading"), + ); // Configure tree lines if let Some(mut lines) = item.child_by_path(ids!(tree_lines)).borrow_mut::() { lines.draw_bg.level = *level as f32; @@ -1229,6 +1276,29 @@ impl SpaceLobbyScreen { BasicRoomDetails::Name(room_name_id) } + fn update_space_info_label(&mut self, cx: &mut Cx, app_language: AppLanguage) { + let text = if self.is_loading { + tr_key(app_language, "space_lobby.header.welcome").to_string() + } else if let Some(member_count) = self.top_level_member_count { + let member_count_str = member_count.to_string(); + format!( + "{} · {}", + match self.top_level_join_rule.as_ref() { + Some(JoinRuleSummary::Public) => tr_key(app_language, "space_lobby.header.public_space"), + _ => tr_key(app_language, "space_lobby.header.private_space"), + }, + if member_count == 1 { + tr_key(app_language, "space_lobby.header.member_one").to_string() + } else { + tr_fmt(app_language, "space_lobby.header.member_n", &[("count", member_count_str.as_str())]) + } + ) + } else { + String::new() + }; + self.view.label(cx, ids!(header.space_info_label)).set_text(cx, &text); + } + fn insert_created_room_placeholder(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { let Some(space_id) = self.space_name_id.as_ref().map(|space| space.room_id().clone()) else { return; @@ -1450,6 +1520,8 @@ impl SpaceLobbyScreen { // Clear the main content until we receive the async space info responses. self.tree_entries.clear(); + self.top_level_join_rule = None; + self.top_level_member_count = None; self.view.label(cx, ids!(header.space_info_label)).set_text(cx, ""); self.is_loading = true; diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 75b03765d..f071cfa77 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -13,7 +13,7 @@ use matrix_sdk::{RoomDisplayName, RoomState}; use ruma::{OwnedRoomAliasId, OwnedRoomId, room::JoinRuleSummary}; use crate::{ - home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, utils::{self, RoomNameId} + app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, i18n::{AppLanguage, tr_fmt, tr_key}, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, utils::{self, RoomNameId} }; script_mod! { @@ -257,6 +257,7 @@ pub struct SpacesBarEntry { #[apply_default] animator: Animator, #[rust] space_name_id: Option, + #[rust] app_language: AppLanguage, } impl Widget for SpacesBarEntry { @@ -273,7 +274,7 @@ impl Widget for SpacesBarEntry { TooltipAction::HoverIn { widget_rect: area.rect(cx), text: this.space_name_id.as_ref().map_or( - String::from("Unknown Space Name"), + String::from(tr_key(this.app_language, "spaces_bar.tooltip.unknown_space_name")), |sni| sni.to_string(), ), options: CalloutTooltipOptions { @@ -343,15 +344,16 @@ impl Widget for SpacesBarEntry { } impl SpacesBarEntry { - fn set_metadata(&mut self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool) { + fn set_metadata(&mut self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool, app_language: AppLanguage) { self.space_name_id = Some(space_name_id); + self.app_language = app_language; self.animator_toggle(cx, is_selected, Animate::No, ids!(active.on), ids!(active.off)); } } impl SpacesBarEntryRef { - pub fn set_metadata(&self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool) { + pub fn set_metadata(&self, cx: &mut Cx, space_name_id: RoomNameId, is_selected: bool, app_language: AppLanguage) { let Some(mut inner) = self.borrow_mut() else { return }; - inner.set_metadata(cx, space_name_id, is_selected); + inner.set_metadata(cx, space_name_id, is_selected, app_language); } } @@ -554,6 +556,10 @@ impl Widget for SpacesBar { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + while let Some(widget_to_draw) = self.view.draw_walk(cx, scope, walk).step() { // We only care about drawing the portal list. let portal_list_ref = widget_to_draw.as_portal_list(); @@ -580,9 +586,9 @@ impl Widget for SpacesBar { item.label(cx, ids!(label)).set_text( cx, if self.is_filtered { - "Found no\nmatching spaces." + tr_key(app_language, "spaces_bar.status.none_matching") } else { - "Found no\njoined spaces." + tr_key(app_language, "spaces_bar.status.none_joined") } ); item @@ -628,17 +634,41 @@ impl Widget for SpacesBar { cx, space.space_name_id.clone(), self.selected_space.as_ref().is_some_and(|id| id == space.space_name_id.room_id()), + app_language, ); item } else if portal_list_index == len { let item = list.item(cx, portal_list_index, id!(StatusLabel)); - let descriptor = if self.is_filtered { "matching" } else { "joined" }; let text = match len { - 0 => format!("Found no\n{descriptor} spaces."), - 1 => format!("Found 1\n{descriptor} space."), - 2..100 => format!("Found {len}\n{descriptor} spaces."), - 100.. => format!("Found 99+\n{descriptor} spaces."), + 0 => { + if self.is_filtered { + tr_key(app_language, "spaces_bar.status.none_matching").to_string() + } else { + tr_key(app_language, "spaces_bar.status.none_joined").to_string() + } + } + 1 => { + if self.is_filtered { + tr_key(app_language, "spaces_bar.status.one_matching").to_string() + } else { + tr_key(app_language, "spaces_bar.status.one_joined").to_string() + } + } + 2..100 => { + if self.is_filtered { + tr_fmt(app_language, "spaces_bar.status.n_matching", &[("count", &len.to_string())]) + } else { + tr_fmt(app_language, "spaces_bar.status.n_joined", &[("count", &len.to_string())]) + } + } + 100.. => { + if self.is_filtered { + tr_key(app_language, "spaces_bar.status.many_matching").to_string() + } else { + tr_key(app_language, "spaces_bar.status.many_joined").to_string() + } + } }; item.label(cx, ids!(label)).set_text(cx, &text); item diff --git a/src/home/welcome_screen.rs b/src/home/welcome_screen.rs index 5e08674dc..892c16ed6 100644 --- a/src/home/welcome_screen.rs +++ b/src/home/welcome_screen.rs @@ -1,4 +1,5 @@ use makepad_widgets::*; +use crate::{app::AppState, i18n::{AppLanguage, tr_key}}; script_mod! { use mod.prelude.widgets.* @@ -7,9 +8,9 @@ script_mod! { mod.widgets.WELCOME_TEXT_COLOR = #x4 - mod.widgets.WelcomeScreen = SolidView { + mod.widgets.WelcomeScreen = #(WelcomeScreen::register_widget(vm)) { width: Fill, height: Fill - align: Align{x: 0.0, y: 0.5} + align: Align{x: 0.5, y: 0.5} show_bg: true, draw_bg.color: (COLOR_PRIMARY) @@ -22,13 +23,15 @@ script_mod! { welcome_message := RoundedView { padding: 40. - width: Fill, height: Fit + width: Fill, height: Fill flow: Down, spacing: 20 + align: Align{x: 0.5, y: 0.5} draw_bg.color: (COLOR_PRIMARY) title := Label { - text: "Welcome to Robrix!", + text: "" + align: Align{x: 0.5, y: 0.5} draw_text +: { color: (mod.widgets.WELCOME_TEXT_COLOR), text_style: theme.font_bold { @@ -38,7 +41,7 @@ script_mod! { } // Using the HTML widget to taking advantage of embedding a link within text with proper vertical alignment - MessageHtml { + body := MessageHtml { padding: Inset{top: 12, left: 0.} font_size: 14. font_color: (mod.widgets.WELCOME_TEXT_COLOR) @@ -52,14 +55,51 @@ script_mod! { // color_hover: #0f0, // } } - body:"

Our Matrix client is under heavy development. Currently, you can access the rooms and spaces that you've joined in other clients.

-


-

But don't worry, we're constantly expanding the featureset of Robrix!

-


-

Look for the latest announcements in our Matrix channel:

-

#robrix:matrix.org

- " + body:"" } } } } + +#[derive(Script, ScriptHook, Widget)] +pub struct WelcomeScreen { + #[deref] view: View, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, +} + +impl Widget for WelcomeScreen { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + self.view.draw_walk(cx, scope, walk) + } +} + +impl WelcomeScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.view + .label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "welcome_screen.title")); + self.view + .html(cx, ids!(body)) + .set_text(cx, tr_key(self.app_language, "welcome_screen.body_html")); + self.view.redraw(cx); + } +} diff --git a/src/i18n.rs b/src/i18n.rs new file mode 100644 index 000000000..2f19806e4 --- /dev/null +++ b/src/i18n.rs @@ -0,0 +1,115 @@ +use std::{collections::HashMap, sync::OnceLock}; + +use serde::{Deserialize, Serialize}; + +/// App UI language preference stored in persisted app state. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AppLanguage { + #[serde(rename = "en", alias = "English")] + #[default] + English, + #[serde(rename = "zh-CN", alias = "ChineseSimplified")] + ChineseSimplified, +} + +impl AppLanguage { + pub const ALL: [Self; 2] = [ + Self::English, + Self::ChineseSimplified, + ]; + + pub fn code(self) -> &'static str { + match self { + Self::English => "en", + Self::ChineseSimplified => "zh-CN", + } + } + + pub fn from_dropdown_index(index: usize) -> Self { + Self::ALL + .get(index) + .copied() + .unwrap_or(Self::English) + } + + pub fn dropdown_index(self) -> usize { + Self::ALL + .iter() + .position(|lang| *lang == self) + .unwrap_or(0) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum I18nKey { + AllSettingsTitle, + SettingsCategoryAccount, + SettingsCategoryPreferences, + SettingsCategoryLabs, + LanguageTitle, + ApplicationLanguageLabel, + LanguageReloadHint, + LanguageOptionEnglish, + LanguageOptionChineseSimplified, +} + +impl I18nKey { + fn as_str(self) -> &'static str { + match self { + I18nKey::AllSettingsTitle => "settings.all_settings_title", + I18nKey::SettingsCategoryAccount => "settings.category.account", + I18nKey::SettingsCategoryPreferences => "settings.category.preferences", + I18nKey::SettingsCategoryLabs => "settings.category.labs", + I18nKey::LanguageTitle => "settings.preferences.language.title", + I18nKey::ApplicationLanguageLabel => "settings.preferences.language.application_label", + I18nKey::LanguageReloadHint => "settings.preferences.language.reload_hint", + I18nKey::LanguageOptionEnglish => "language.option.english", + I18nKey::LanguageOptionChineseSimplified => "language.option.chinese_simplified", + } + } +} + +fn load_dictionary(language: AppLanguage) -> HashMap { + let json = match language { + AppLanguage::English => include_str!("../resources/i18n/en.json"), + AppLanguage::ChineseSimplified => include_str!("../resources/i18n/zh-CN.json"), + }; + serde_json::from_str(json).unwrap_or_default() +} + +fn dictionary(language: AppLanguage) -> &'static HashMap { + static EN_DICTIONARY: OnceLock> = OnceLock::new(); + static ZH_CN_DICTIONARY: OnceLock> = OnceLock::new(); + + match language { + AppLanguage::English => EN_DICTIONARY.get_or_init(|| load_dictionary(AppLanguage::English)), + AppLanguage::ChineseSimplified => ZH_CN_DICTIONARY.get_or_init(|| load_dictionary(AppLanguage::ChineseSimplified)), + } +} + +pub fn tr_key<'a>(language: AppLanguage, key: &'a str) -> &'a str { + dictionary(language) + .get(key) + .map(String::as_str) + .or_else(|| dictionary(AppLanguage::English).get(key).map(String::as_str)) + .unwrap_or(key) +} + +pub fn tr_fmt(language: AppLanguage, key: &str, vars: &[(&str, &str)]) -> String { + let mut output = tr_key(language, key).to_string(); + for (name, value) in vars { + output = output.replace(&format!("{{{name}}}"), value); + } + output +} + +pub fn tr(language: AppLanguage, key: I18nKey) -> &'static str { + tr_key(language, key.as_str()) +} + +pub fn language_dropdown_labels(language: AppLanguage) -> Vec { + vec![ + tr(language, I18nKey::LanguageOptionEnglish).to_string(), + tr(language, I18nKey::LanguageOptionChineseSimplified).to_string(), + ] +} diff --git a/src/lib.rs b/src/lib.rs index 346c0314b..584bc9c4e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,8 @@ pub mod app; pub mod persistence; /// The settings screen and settings-related content/widgets. pub mod settings; +/// App-localized text and language preference definitions. +pub mod i18n; /// Login screen pub mod login; diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index bf4ec59c2..246308ad8 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; +use crate::{app::AppState, i18n::{AppLanguage, tr_fmt, tr_key}, sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -154,7 +154,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - Label { + homeserver_hint_label := Label { width: Fit, height: Fit padding: 0 draw_text +: { @@ -190,7 +190,7 @@ script_mod! { draw_bg.color: #C8C8C8 } - Label { + sso_prompt_label := Label { width: Fit, height: Fit padding: 0, draw_text +: { @@ -296,25 +296,66 @@ pub struct LoginScreen { #[rust] sso_redirect_url: Option, /// The most recent login failure message shown to the user. #[rust] last_failure_message_shown: Option, + #[rust] app_language: AppLanguage, } impl LoginScreen { - fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { - self.signup_mode = signup_mode; - self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); - self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); + fn sync_mode_texts(&mut self, cx: &mut Cx) { self.view.label(cx, ids!(title)).set_text(cx, - if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } + if self.signup_mode { + tr_key(self.app_language, "login.title.create_account") + } else { + tr_key(self.app_language, "login.title.login_to_robrix") + } ); self.view.button(cx, ids!(login_button)).set_text(cx, - if signup_mode { "Create account" } else { "Login" } + if self.signup_mode { + tr_key(self.app_language, "login.button.create_account") + } else { + tr_key(self.app_language, "login.button.login") + } ); self.view.label(cx, ids!(account_prompt_label)).set_text(cx, - if signup_mode { "Already have an account?" } else { "Don't have an account?" } + if self.signup_mode { + tr_key(self.app_language, "login.account_prompt.already_have") + } else { + tr_key(self.app_language, "login.account_prompt.no_account") + } ); self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, - if signup_mode { "Back to login" } else { "Sign up here" } + if self.signup_mode { + tr_key(self.app_language, "login.mode_toggle.back_to_login") + } else { + tr_key(self.app_language, "login.mode_toggle.sign_up_here") + } ); + } + + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view.text_input(cx, ids!(user_id_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.input.user_id").to_string()); + self.view.text_input(cx, ids!(password_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.input.password").to_string()); + self.view.text_input(cx, ids!(confirm_password_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.input.confirm_password").to_string()); + self.view.text_input(cx, ids!(homeserver_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.input.homeserver").to_string()); + self.view.label(cx, ids!(homeserver_hint_label)) + .set_text(cx, tr_key(self.app_language, "login.label.homeserver_optional")); + self.view.label(cx, ids!(sso_prompt_label)) + .set_text(cx, tr_key(self.app_language, "login.sso.prompt")); + let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login_status_modal.title")); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login_status_modal.button.cancel")); + self.sync_mode_texts(cx); + } + + fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { + self.signup_mode = signup_mode; + self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); + self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); + self.sync_mode_texts(cx); if !signup_mode { self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); @@ -327,17 +368,29 @@ impl LoginScreen { impl Widget for LoginScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); - self.match_event(cx, event); + self.widget_match_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } -impl MatchEvent for LoginScreen { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { +impl WidgetMatchEvent for LoginScreen { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { let login_button = self.view.button(cx, ids!(login_button)); let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); @@ -363,33 +416,33 @@ impl MatchEvent for LoginScreen { let confirm_password = confirm_password_input.text(); let homeserver = homeserver_input.text().trim().to_owned(); if user_id.is_empty() { - login_status_modal_inner.set_title(cx, "Missing User ID"); - login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.missing_user_id.title")); + login_status_modal_inner.set_status(cx, tr_key(self.app_language, "login.status.missing_user_id.body")); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); } else if password.is_empty() { - login_status_modal_inner.set_title(cx, "Missing Password"); - login_status_modal_inner.set_status(cx, "Please enter a valid password."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.missing_password.title")); + login_status_modal_inner.set_status(cx, tr_key(self.app_language, "login.status.missing_password.body")); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); } else if self.signup_mode && password != confirm_password { - login_status_modal_inner.set_title(cx, "Passwords do not match"); - login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.password_mismatch.title")); + login_status_modal_inner.set_status(cx, tr_key(self.app_language, "login.status.password_mismatch.body")); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); } else { self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, if self.signup_mode { - "Creating account..." + tr_key(self.app_language, "login.status.creating_account.title") } else { - "Logging in..." + tr_key(self.app_language, "login.status.logging_in.title") }); login_status_modal_inner.set_status( cx, if self.signup_mode { - "Waiting for the homeserver to create your account..." + tr_key(self.app_language, "login.status.creating_account.body") } else { - "Waiting for a login response..." + tr_key(self.app_language, "login.status.logging_in.body") }, ); - login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.cancel")); submit_async_request(MatrixRequest::Login(if self.signup_mode { LoginRequest::Register(RegisterAccount { user_id, @@ -429,13 +482,15 @@ impl MatchEvent for LoginScreen { user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); - login_status_modal_inner.set_title(cx, "Logging in via CLI..."); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.logging_in_cli.title")); login_status_modal_inner.set_status( cx, - &format!("Auto-logging in as user {user_id}...") + &tr_fmt(self.app_language, "login.status.auto_logging_in_as_user", &[ + ("user_id", user_id.as_str()), + ]) ); let login_status_modal_button = login_status_modal_inner.button_ref(cx); - login_status_modal_button.set_text(cx, "Cancel"); + login_status_modal_button.set_text(cx, tr_key(self.app_language, "login.status.cancel")); login_status_modal_button.set_enabled(cx, false); // Login cancel not yet supported login_status_modal.open(cx); } @@ -444,7 +499,7 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); - login_status_modal_button.set_text(cx, "Cancel"); + login_status_modal_button.set_text(cx, tr_key(self.app_language, "login.status.cancel")); login_status_modal_button.set_enabled(cx, true); login_status_modal.open(cx); self.redraw(cx); @@ -467,13 +522,13 @@ impl MatchEvent for LoginScreen { } self.last_failure_message_shown = Some(error.clone()); login_status_modal_inner.set_title(cx, if self.signup_mode { - "Account Creation Failed." + tr_key(self.app_language, "login.status.account_creation_failed") } else { - "Login Failed." + tr_key(self.app_language, "login.status.login_failed") }); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); - login_status_modal_button.set_text(cx, "Okay"); + login_status_modal_button.set_text(cx, tr_key(self.app_language, "login.status.okay")); login_status_modal_button.set_enabled(cx, true); login_status_modal.open(cx); self.redraw(cx); diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index bf4563d65..102cde38f 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}}, i18n::AppLanguage, 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}; script_mod! { use mod.prelude.widgets.* @@ -409,6 +409,7 @@ impl RoomInputBar { populate_preview_of_timeline_item( cx, &replying_preview.html_or_plaintext(cx, ids!(reply_preview_content.reply_preview_body)), + AppLanguage::default(), replying_to.0.content(), replying_to.0.sender(), &replying_preview_username, diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 4669039d4..3f5844d36 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; -use crate::{app::ConfirmDeleteAction, avatar_cache::{self}, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, utils}; +use crate::{app::{AppState, ConfirmDeleteAction}, avatar_cache::{self}, i18n::{AppLanguage, tr_key}, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, utils}; script_mod! { use mod.prelude.widgets.* @@ -14,11 +14,11 @@ script_mod! { width: Fill, height: Fit flow: Down - TitleLabel { + account_settings_title := TitleLabel { text: "Account Settings" } - SubsectionLabel { + avatar_section_label := SubsectionLabel { text: "Your Avatar:" } @@ -95,7 +95,7 @@ script_mod! { } } - SubsectionLabel { + display_name_section_label := SubsectionLabel { text: "Your Display Name:" } @@ -144,7 +144,7 @@ script_mod! { } } - SubsectionLabel { + user_id_section_label := SubsectionLabel { text: "Your User ID:" } @@ -174,7 +174,7 @@ script_mod! { } } - SubsectionLabel { + other_actions_section_label := SubsectionLabel { text: "Other actions:" } @@ -210,10 +210,17 @@ pub struct AccountSettings { #[deref] view: View, #[rust] own_profile: Option, + #[rust] app_language: AppLanguage, } impl Widget for AccountSettings { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.match_event(cx, event); let copy_user_id_button = self.view.button(cx, ids!(copy_user_id_button)); @@ -223,7 +230,7 @@ impl Widget for AccountSettings { cx.widget_action( copy_user_id_button.widget_uid(), TooltipAction::HoverIn { - text: "Copy User ID".to_string(), + text: tr_key(self.app_language, "settings.account.tooltip.copy_user_id").to_string(), widget_rect: copy_user_id_button_area.rect(cx), options: CalloutTooltipOptions { position: TooltipPosition::Top, @@ -245,6 +252,12 @@ impl Widget for AccountSettings { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } @@ -272,7 +285,11 @@ impl MatchEvent for AccountSettings { // Handle LogoutAction::InProgress to update button state if let Some(LogoutAction::InProgress(is_in_progress)) = action.downcast_ref() { let logout_button = self.view.button(cx, ids!(logout_button)); - logout_button.set_text(cx, if *is_in_progress { "Logging out..." } else { "Log out" }); + logout_button.set_text(cx, if *is_in_progress { + tr_key(self.app_language, "settings.account.button.logging_out") + } else { + tr_key(self.app_language, "settings.account.button.log_out") + }); logout_button.set_enabled(cx, !*is_in_progress); logout_button.reset_hover(cx); continue; @@ -291,7 +308,11 @@ impl MatchEvent for AccountSettings { profile.avatar_state.update_from_cache(cx); self.populate_avatar_views(cx); enqueue_popup_notification( - format!("Successfully {} avatar.", if new_avatar_url.is_some() { "updated" } else { "deleted" }), + if new_avatar_url.is_some() { + tr_key(self.app_language, "settings.account.popup.avatar_updated") + } else { + tr_key(self.app_language, "settings.account.popup.avatar_deleted") + }, PopupKind::Success, Some(4.0), ); @@ -329,7 +350,11 @@ impl MatchEvent for AccountSettings { display_name_input.set_disabled(cx, false); Self::enable_display_name_buttons(cx, false, &accept_display_name_button, &cancel_display_name_button); enqueue_popup_notification( - format!("Successfully {} display name.", if new_name.is_some() { "updated" } else { "removed" }), + if new_name.is_some() { + tr_key(self.app_language, "settings.account.popup.display_name_updated") + } else { + tr_key(self.app_language, "settings.account.popup.display_name_removed") + }, PopupKind::Success, Some(4.0), ); @@ -380,7 +405,7 @@ impl MatchEvent for AccountSettings { // Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); // Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); enqueue_popup_notification( - "Avatar uploading is not yet implemented.", + tr_key(self.app_language, "settings.account.popup.avatar_upload_not_implemented"), PopupKind::Warning, Some(4.0), ); @@ -390,15 +415,16 @@ impl MatchEvent for AccountSettings { // Don't immediately disable the buttons. Instead, we wait for the user // to confirm the action in the confirmation modal, // and then we disable the buttons in the AvatarDeleteStarted action handler. + let app_language = self.app_language; let content = ConfirmationModalContent { - title_text: "Delete Avatar".into(), - body_text: "Are you sure you want to delete your avatar?".into(), - accept_button_text: Some("Delete".into()), - on_accept_clicked: Some(Box::new(|cx| { + title_text: tr_key(app_language, "settings.account.modal.delete_avatar.title").into(), + body_text: tr_key(app_language, "settings.account.modal.delete_avatar.body").into(), + accept_button_text: Some(tr_key(app_language, "settings.account.modal.delete_avatar.accept").into()), + on_accept_clicked: Some(Box::new(move |cx| { submit_async_request(MatrixRequest::SetAvatar { avatar_url: None }); cx.action(AccountSettingsAction::AvatarDeleteStarted); enqueue_popup_notification( - "Deleting your avatar...", + tr_key(app_language, "settings.account.popup.deleting_avatar"), PopupKind::Info, Some(5.0), ); @@ -436,7 +462,7 @@ impl MatchEvent for AccountSettings { display_name_input.set_is_read_only(cx, true); Self::enable_display_name_buttons(cx, false, &accept_display_name_button, &cancel_display_name_button); enqueue_popup_notification( - "Uploading new display name...", + tr_key(self.app_language, "settings.account.popup.uploading_display_name"), PopupKind::Info, Some(5.0), ); @@ -445,7 +471,7 @@ impl MatchEvent for AccountSettings { if self.view.button(cx, ids!(copy_user_id_button)).clicked(actions) { cx.copy_to_clipboard(own_profile.user_id.as_str()); enqueue_popup_notification( - "Copied your User ID to the clipboard.", + tr_key(self.app_language, "settings.account.popup.copied_user_id"), PopupKind::Success, Some(3.0), ); @@ -455,7 +481,7 @@ impl MatchEvent for AccountSettings { // TODO: support opening the user's account management page in a browser, // or perhaps in an in-app pane if that's what is needed for regular UN+PW login. enqueue_popup_notification( - "Account management is not yet implemented.", + tr_key(self.app_language, "settings.account.popup.account_management_not_implemented"), PopupKind::Warning, Some(4.0), ); @@ -465,6 +491,56 @@ impl MatchEvent for AccountSettings { } impl AccountSettings { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(account_settings_title)) + .set_text(cx, tr_key(self.app_language, "settings.account.title")); + self.view + .label(cx, ids!(avatar_section_label)) + .set_text(cx, tr_key(self.app_language, "settings.account.section.your_avatar")); + self.view + .button(cx, ids!(upload_avatar_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.upload_avatar")); + self.view + .button(cx, ids!(delete_avatar_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.delete_avatar")); + self.view + .label(cx, ids!(display_name_section_label)) + .set_text(cx, tr_key(self.app_language, "settings.account.section.your_display_name")); + self.view + .text_input(cx, ids!(display_name_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.account.display_name.placeholder").to_string()); + self.view + .button(cx, ids!(cancel_display_name_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.cancel")); + self.view + .button(cx, ids!(accept_display_name_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.save_name")); + self.view + .label(cx, ids!(user_id_section_label)) + .set_text(cx, tr_key(self.app_language, "settings.account.section.your_user_id")); + if self.own_profile.is_none() { + self.view + .label(cx, ids!(user_id)) + .set_text(cx, tr_key(self.app_language, "settings.account.user_id.not_logged_in")); + } + self.view + .label(cx, ids!(other_actions_section_label)) + .set_text(cx, tr_key(self.app_language, "settings.account.section.other_actions")); + self.view + .button(cx, ids!(manage_account_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.manage_account")); + self.view + .button(cx, ids!(logout_button)) + .set_text(cx, tr_key(self.app_language, "settings.account.button.log_out")); + self.view.redraw(cx); + } + /// Populate avatar-related views with the user's profile data. /// /// This does nothing if `self.own_profile` is `None`. @@ -519,6 +595,7 @@ impl AccountSettings { self.own_profile = Some(own_profile); self.populate_avatar_views(cx); + self.sync_app_language(cx); self.view.button(cx, ids!(upload_avatar_button)).reset_hover(cx); self.view.button(cx, ids!(delete_avatar_button)).reset_hover(cx); @@ -639,6 +716,11 @@ impl AccountSettingsRef { let Some(mut inner) = self.borrow_mut() else { return }; inner.populate(cx, own_profile); } + + pub fn set_app_language(&self, cx: &mut Cx, app_language: AppLanguage) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_app_language(cx, app_language); + } } /// Actions that are handled by the AccountSettings widget. diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index 6e877a62c..200116764 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -2,6 +2,7 @@ use makepad_widgets::*; use crate::{ app::{AppState, BotSettingsState}, + i18n::{AppLanguage, tr_key}, persistence, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::current_user_id, @@ -28,7 +29,7 @@ script_mod! { flow: Down spacing: 10 - TitleLabel { + app_service_title := TitleLabel { text: "App Service" } @@ -68,7 +69,7 @@ script_mod! { height: Fit flow: Down - SubsectionLabel { + bot_user_id_label := SubsectionLabel { text: "BotFather User ID:" } @@ -103,15 +104,29 @@ script_mod! { pub struct BotSettings { #[deref] view: View, + #[rust] + app_language: AppLanguage, } impl Widget for BotSettings { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } @@ -140,7 +155,7 @@ impl WidgetMatchEvent for BotSettings { app_state.bot_settings.botfather_user_id = bot_user_id_input.text().trim().to_string(); persist_bot_settings(app_state); enqueue_popup_notification( - "Saved Matrix app service settings.", + tr_key(self.app_language, "settings.labs.app_service.popup.saved"), PopupKind::Success, Some(3.0), ); @@ -150,6 +165,33 @@ impl WidgetMatchEvent for BotSettings { } impl BotSettings { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(app_service_title)) + .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.title")); + self.view + .label(cx, ids!(description)) + .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.description")); + self.view + .label(cx, ids!(enable_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.enable_label")); + self.view + .label(cx, ids!(bot_user_id_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.botfather_user_id")); + self.view + .text_input(cx, ids!(bot_user_id_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.labs.app_service.botfather_placeholder").to_string()); + self.view + .button(cx, ids!(buttons.save_button)) + .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.button.save")); + self.view.redraw(cx); + } + fn sync_ui(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { self.view .view(cx, ids!(bot_details)) @@ -159,9 +201,9 @@ impl BotSettings { .set_text(cx, &bot_settings.botfather_user_id); let toggle_text = if bot_settings.enabled { - "Disable App Service" + tr_key(self.app_language, "settings.labs.app_service.button.disable") } else { - "Enable App Service" + tr_key(self.app_language, "settings.labs.app_service.button.enable") }; self.view .button(cx, ids!(toggle_button)) @@ -175,6 +217,7 @@ impl BotSettings { /// Populates the bot settings UI from the current persisted app state. pub fn populate(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { + self.sync_app_language(cx); self.sync_ui(cx, bot_settings); } } @@ -187,6 +230,13 @@ impl BotSettingsRef { }; inner.populate(cx, bot_settings); } + + pub fn set_app_language(&self, cx: &mut Cx, app_language: AppLanguage) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.set_app_language(cx, app_language); + } } fn persist_bot_settings(app_state: &AppState) { diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 5d24945bc..5633bbc6f 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,7 +1,7 @@ use makepad_widgets::*; -use crate::{app::BotSettingsState, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}}; +use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; script_mod! { use mod.prelude.widgets.* @@ -49,27 +49,99 @@ script_mod! { // Make sure the dividing line is aligned with the close_button LineH { padding: 10, margin: Inset{top: 10, right: 2} } + settings_category_cards := View { + width: Fill, height: Fit + flow: Flow.Right{wrap: true} + align: Align{y: 0.5} + spacing: 10 + margin: Inset{left: 5, right: 5, bottom: 8} + + category_account_button := RobrixNeutralIconButton { + width: Fit, height: Fit, + padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + text: "Account" + } + + category_preferences_button := RobrixNeutralIconButton { + width: Fit, height: Fit, + padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + text: "Preferences" + } + + category_labs_button := RobrixNeutralIconButton { + width: Fit, height: Fit, + padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + text: "Labs" + } + } + ScrollXYView { width: Fill, height: Fill flow: Down - // The account settings section. - account_settings := AccountSettings {} + settings_sections := View { + width: Fill, height: Fit + flow: Down - LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + // The account settings section. + account_settings_section := View { + width: Fill, height: Fit + flow: Down + account_settings := AccountSettings {} + } - bot_settings := BotSettings {} + preferences_settings_section := View { + visible: false + width: Fill, height: Fit + flow: Down + spacing: 8 - LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + preferences_language_title := TitleLabel { + text: "Language" + } - // The TSP wallet settings section. - tsp_settings_screen := TspSettingsScreen {} + preferences_application_language_label := SubsectionLabel { + text: "Application language" + } - LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + language_dropdown := DropDownFlat { + width: 165 + height: 40 + margin: Inset{left: 5, top: 2, bottom: 2} + labels: ["English", "Simplified Chinese"] + } - // Add other settings sections here as needed. - // Don't forget to add a `show()` fn to those settings sections - // and call them in `SettingsScreen::show()`. + preferences_language_hint_label := Label { + width: Fill + height: Fit + margin: Inset{left: 5, right: 8, top: 3, bottom: 4} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "The app will reload after selecting another language" + } + } + + labs_settings_section := View { + visible: false + width: Fill, height: Fit + flow: Down + + bot_settings := BotSettings {} + + LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + + // The TSP wallet settings section. + tsp_settings_screen := TspSettingsScreen {} + } + } } } @@ -89,14 +161,32 @@ script_mod! { } +/// The top-level widget showing all app and user settings/preferences. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +enum SettingsCategory { + #[default] + Account, + Preferences, + Labs, +} + /// The top-level widget showing all app and user settings/preferences. #[derive(Script, ScriptHook, Widget)] pub struct SettingsScreen { #[deref] view: View, + + #[rust] selected_category: SettingsCategory, + #[rust] app_language: AppLanguage, } impl Widget for SettingsScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); // Close the pane if: @@ -124,57 +214,176 @@ impl Widget for SettingsScreen { cx.action(NavigationBarAction::CloseSettings); } - #[cfg(feature = "tsp")] if let Event::Actions(actions) = event { - use crate::tsp::{ - create_did_modal::CreateDidModalAction, - create_wallet_modal::CreateWalletModalAction, - }; - - for action in actions { - // Handle the create wallet modal being opened or closed. - match action.downcast_ref() { - Some(CreateWalletModalAction::Open) => { - use crate::tsp::create_wallet_modal::CreateWalletModalWidgetExt; - self.view.create_wallet_modal(cx, ids!(create_wallet_modal_inner)).show(cx); - self.view.modal(cx, ids!(create_wallet_modal)).open(cx); - } - Some(CreateWalletModalAction::Close) => { - self.view.modal(cx, ids!(create_wallet_modal)).close(cx); + if self.view.drop_down(cx, ids!(language_dropdown)).changed(actions).is_some() { + let selected_language = AppLanguage::from_dropdown_index( + self.view.drop_down(cx, ids!(language_dropdown)).selected_item(), + ); + if self.app_language != selected_language { + self.set_app_language(cx, selected_language); + if let Some(app_state) = scope.data.get_mut::() { + if app_state.app_language != selected_language { + app_state.app_language = selected_language; + persist_app_state(app_state); + enqueue_popup_notification( + tr(selected_language, I18nKey::LanguageReloadHint), + PopupKind::Info, + Some(4.0), + ); + } } - None => { } } + } - // Handle the create DID modal being opened or closed. - match action.downcast_ref() { - Some(CreateDidModalAction::Open) => { - use crate::tsp::create_did_modal::CreateDidModalWidgetExt; - self.view.create_did_modal(cx, ids!(create_did_modal_inner)).show(cx); - self.view.modal(cx, ids!(create_did_modal)).open(cx); + if self.view.button(cx, ids!(category_account_button)).clicked(actions) { + self.set_selected_category(cx, SettingsCategory::Account); + } + else if self.view.button(cx, ids!(category_preferences_button)).clicked(actions) { + self.set_selected_category(cx, SettingsCategory::Preferences); + } + else if self.view.button(cx, ids!(category_labs_button)).clicked(actions) { + self.set_selected_category(cx, SettingsCategory::Labs); + } + + #[cfg(feature = "tsp")] + { + use crate::tsp::{ + create_did_modal::CreateDidModalAction, + create_wallet_modal::CreateWalletModalAction, + }; + + for action in actions { + // Handle the create wallet modal being opened or closed. + match action.downcast_ref() { + Some(CreateWalletModalAction::Open) => { + use crate::tsp::create_wallet_modal::CreateWalletModalWidgetExt; + self.view.create_wallet_modal(cx, ids!(create_wallet_modal_inner)).show(cx); + self.view.modal(cx, ids!(create_wallet_modal)).open(cx); + } + Some(CreateWalletModalAction::Close) => { + self.view.modal(cx, ids!(create_wallet_modal)).close(cx); + } + None => { } } - Some(CreateDidModalAction::Close) => { - self.view.modal(cx, ids!(create_did_modal)).close(cx); + + // Handle the create DID modal being opened or closed. + match action.downcast_ref() { + Some(CreateDidModalAction::Open) => { + use crate::tsp::create_did_modal::CreateDidModalWidgetExt; + self.view.create_did_modal(cx, ids!(create_did_modal_inner)).show(cx); + self.view.modal(cx, ids!(create_did_modal)).open(cx); + } + Some(CreateDidModalAction::Close) => { + self.view.modal(cx, ids!(create_did_modal)).close(cx); + } + None => { } } - None => { } } } } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } impl SettingsScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(settings_header_title)) + .set_text(cx, tr(self.app_language, I18nKey::AllSettingsTitle)); + self.view + .button(cx, ids!(category_account_button)) + .set_text(cx, tr(self.app_language, I18nKey::SettingsCategoryAccount)); + self.view + .button(cx, ids!(category_preferences_button)) + .set_text(cx, tr(self.app_language, I18nKey::SettingsCategoryPreferences)); + self.view + .button(cx, ids!(category_labs_button)) + .set_text(cx, tr(self.app_language, I18nKey::SettingsCategoryLabs)); + self.view + .label(cx, ids!(preferences_language_title)) + .set_text(cx, tr(self.app_language, I18nKey::LanguageTitle)); + self.view + .label(cx, ids!(preferences_application_language_label)) + .set_text(cx, tr(self.app_language, I18nKey::ApplicationLanguageLabel)); + self.view + .label(cx, ids!(preferences_language_hint_label)) + .set_text(cx, tr(self.app_language, I18nKey::LanguageReloadHint)); + let language_dropdown = self.view.drop_down(cx, ids!(language_dropdown)); + language_dropdown.set_labels(cx, language_dropdown_labels(self.app_language)); + language_dropdown.set_selected_item(cx, self.app_language.dropdown_index()); + self.view + .account_settings(cx, ids!(account_settings)) + .set_app_language(cx, self.app_language); + self.view + .bot_settings(cx, ids!(bot_settings)) + .set_app_language(cx, self.app_language); + self.view.redraw(cx); + } + + fn set_selected_category(&mut self, cx: &mut Cx, category: SettingsCategory) { + self.selected_category = category; + self.sync_selected_category(cx); + } + + fn sync_selected_category(&mut self, cx: &mut Cx) { + let show_account = self.selected_category == SettingsCategory::Account; + let show_preferences = self.selected_category == SettingsCategory::Preferences; + let show_labs = self.selected_category == SettingsCategory::Labs; + + self.view.view(cx, ids!(account_settings_section)).set_visible(cx, show_account); + self.view.view(cx, ids!(preferences_settings_section)).set_visible(cx, show_preferences); + self.view.view(cx, ids!(labs_settings_section)).set_visible(cx, show_labs); + + let mut category_account_button = self.view.button(cx, ids!(category_account_button)); + let mut category_preferences_button = self.view.button(cx, ids!(category_preferences_button)); + let mut category_labs_button = self.view.button(cx, ids!(category_labs_button)); + + if show_account { + apply_primary_button_style(cx, &mut category_account_button); + } else { + apply_neutral_button_style(cx, &mut category_account_button); + } + if show_preferences { + apply_primary_button_style(cx, &mut category_preferences_button); + } else { + apply_neutral_button_style(cx, &mut category_preferences_button); + } + if show_labs { + apply_primary_button_style(cx, &mut category_labs_button); + } else { + apply_neutral_button_style(cx, &mut category_labs_button); + } + + category_account_button.reset_hover(cx); + category_preferences_button.reset_hover(cx); + category_labs_button.reset_hover(cx); + self.view.redraw(cx); + } + /// Fetches the current user's profile and uses it to populate the settings screen. - pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState) { + pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, app_language: AppLanguage) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { error!("Failed to get own profile for settings screen."); return; }; self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); self.view.bot_settings(cx, ids!(bot_settings)).populate(cx, bot_settings); + self.set_app_language(cx, app_language); + self.set_selected_category(cx, SettingsCategory::Account); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); @@ -183,8 +392,16 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. - pub fn populate(&self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState) { + pub fn populate(&self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, app_language: AppLanguage) { let Some(mut inner) = self.borrow_mut() else { return; }; - inner.populate(cx, own_profile, bot_settings); + inner.populate(cx, own_profile, bot_settings, app_language); + } +} + +fn persist_app_state(app_state: &AppState) { + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(app_state.clone(), user_id) { + error!("Failed to persist app state after updating language setting. Error: {e}"); + } } } diff --git a/src/shared/collapsible_header.rs b/src/shared/collapsible_header.rs index e9ad7e337..5a15803ee 100644 --- a/src/shared/collapsible_header.rs +++ b/src/shared/collapsible_header.rs @@ -10,7 +10,7 @@ use makepad_widgets::*; use makepad_widgets::animator::Animate; -use crate::home::rooms_list::RoomsListScopeProps; +use crate::{app::AppState, home::rooms_list::RoomsListScopeProps, i18n::tr_key}; use super::expand_arrow::ExpandArrow; use super::unread_badge::UnreadBadgeWidgetRefExt as _; @@ -82,15 +82,15 @@ pub enum HeaderCategory { None, } impl HeaderCategory { - fn as_str(&self) -> &'static str { + fn i18n_key(&self) -> Option<&'static str> { match self { - HeaderCategory::Invites => "Invites", - HeaderCategory::Favorites => "Favorites", - HeaderCategory::RegularRooms => "Rooms", - HeaderCategory::DirectRooms => "People", - HeaderCategory::LowPriority => "Low Priority", - HeaderCategory::LeftRooms => "Left Rooms", - HeaderCategory::None => "", + HeaderCategory::Invites => Some("rooms_list.category.invites"), + HeaderCategory::Favorites => Some("rooms_list.category.favorites"), + HeaderCategory::RegularRooms => Some("rooms_list.category.rooms"), + HeaderCategory::DirectRooms => Some("rooms_list.category.people"), + HeaderCategory::LowPriority => Some("rooms_list.category.low_priority"), + HeaderCategory::LeftRooms => Some("rooms_list.category.left_rooms"), + HeaderCategory::None => None, } } } @@ -133,10 +133,18 @@ impl Widget for CollapsibleHeader { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // Set arrow and label state during draw to ensure child widgets are available. + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); if let Some(mut arrow) = self.view.child_by_path(ids!(collapse_icon)).borrow_mut::() { arrow.set_is_open_no_animate(self.is_expanded); } - self.view.child_by_path(ids!(label)).set_text(cx, self.category.as_str()); + self.view.child_by_path(ids!(label)).set_text( + cx, + self.category + .i18n_key() + .map_or("", |key| tr_key(app_language, key)), + ); self.view.child_by_path(ids!(unread_badge)) .as_unread_badge() .update_counts(false, self.num_unread_mentions, 0); diff --git a/src/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index ccbee0601..d89a7fed8 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -5,6 +5,7 @@ //! reused consistently across both Desktop and Mobile layouts. use makepad_widgets::*; +use crate::{app::AppState, i18n::{AppLanguage, tr_key}}; script_mod! { use mod.prelude.widgets.* @@ -69,6 +70,7 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct RoomFilterInputBar { #[deref] view: View, + #[rust] app_language: AppLanguage, } /// Actions emitted by the `RoomFilterInputBar` based on user interaction with it. @@ -89,11 +91,23 @@ impl ActionDefaultRef for RoomFilterAction { impl Widget for RoomFilterInputBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.view.draw_walk(cx, scope, walk) } } @@ -131,3 +145,13 @@ impl WidgetMatchEvent for RoomFilterInputBar { } } } + +impl RoomFilterInputBar { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view + .text_input(cx, ids!(input)) + .set_empty_text(cx, tr_key(self.app_language, "room_filter_input.placeholder").to_string()); + self.view.redraw(cx); + } +} diff --git a/src/tsp/tsp_settings_screen.rs b/src/tsp/tsp_settings_screen.rs index 83d0e6f87..adc9130b1 100644 --- a/src/tsp/tsp_settings_screen.rs +++ b/src/tsp/tsp_settings_screen.rs @@ -1,9 +1,7 @@ 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"; +use crate::{app::AppState, i18n::{AppLanguage, tr_fmt, tr_key}, 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}}; script_mod! { link tsp_enabled @@ -12,19 +10,17 @@ script_mod! { use mod.widgets.* - mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT = "Republish Current Identity to DID Server" - // The view containing all TSP-related settings. mod.widgets.TspSettingsScreen = #(TspSettingsScreen::register_widget(vm)) { width: Fill, height: Fit flow: Down - TitleLabel { - text: "TSP Wallet Settings" + title := TitleLabel { + text: "" } - SubsectionLabel { - text: "Your active identity:" + section_active_identity := SubsectionLabel { + text: "" } View { @@ -57,17 +53,17 @@ script_mod! { draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_UPLOAD) icon_walk: Walk{width: 16, height: 16} - text: (REPUBLISH_IDENTITY_BUTTON_TEXT) + text: "" } - SubsectionLabel { - text: "Your Wallets:" + section_wallets := SubsectionLabel { + text: "" } no_wallets_label := View { width: Fill, height: Fit - Label { + no_wallets_text := Label { width: Fill, height: Fit margin: Inset{top: 10, bottom: 8, left: 13, right: 10}, flow: Flow.Right{wrap: true}, @@ -75,7 +71,7 @@ script_mod! { color: (COLOR_TEXT_WARNING_NOT_FOUND), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } - text: "No wallets found. Create or import a wallet." + text: "" } } @@ -117,7 +113,7 @@ script_mod! { draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_USER) icon_walk: Walk{width: 21, height: Fit, margin: 0} - text: "Create New Identity (DID)" + text: "" } create_wallet_button := RobrixPositiveIconButton { @@ -127,13 +123,13 @@ script_mod! { 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" + text: "" } import_wallet_button := RobrixIconButton { padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} - text: "Import Existing Wallet" + text: "" // TODO: fix this icon, or pick a different SVG // draw_icon +: { // svg: (ICON_IMPORT) @@ -161,18 +157,18 @@ impl WalletState { self.active_wallet.is_some() as usize + self.other_wallets.len() } - fn get(&self, index: usize) -> Option<(&TspWalletMetadata, WalletStatusAndDefault)> { + fn get(&self, index: usize, app_language: AppLanguage) -> Option<(&TspWalletMetadata, WalletStatusAndDefault)> { if let Some(active) = self.active_wallet.as_ref() { if index == 0 { - Some((active, WalletStatusAndDefault::new(WalletStatus::Opened, true))) + Some((active, WalletStatusAndDefault::new(WalletStatus::Opened, true, app_language))) } else { self.other_wallets.get(index - 1).map(|(m, s)| - (m, WalletStatusAndDefault::new(*s, false)) + (m, WalletStatusAndDefault::new(*s, false, app_language)) ) } } else { self.other_wallets.get(index).map(|(m, s)| - (m, WalletStatusAndDefault::new(*s, false)) + (m, WalletStatusAndDefault::new(*s, false, app_language)) ) } } @@ -188,10 +184,11 @@ pub enum WalletStatus { pub struct WalletStatusAndDefault { pub status: WalletStatus, pub is_default: bool, + pub app_language: AppLanguage, } impl WalletStatusAndDefault { - pub fn new(status: WalletStatus, is_default: bool) -> Self { - Self { status, is_default } + pub fn new(status: WalletStatus, is_default: bool, app_language: AppLanguage) -> Self { + Self { status, is_default, app_language } } } @@ -211,15 +208,29 @@ pub struct TspSettingsScreen { /// to avoid having to re-fetch them from the shared TSP state every time, /// as that requires locking the mutex and can be expensive. #[rust] wallets: Option, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, } impl Widget for TspSettingsScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } self.match_event(cx, event); self.view.handle_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } if self.wallets.is_none() { // If we don't have any wallets, load them from the TSP state. self.refresh_wallets(); @@ -231,7 +242,7 @@ impl Widget for TspSettingsScreen { self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) { Some(current_did) => (current_did.to_string(), COLOR_FG_ACCEPT_GREEN, true), - None => ("No default identity has been set.".to_string(), COLOR_TEXT_WARNING_NOT_FOUND, false), + None => (tr_key(self.app_language, "tsp.settings.identity.none_set").to_string(), COLOR_TEXT_WARNING_NOT_FOUND, false), }; let mut current_identity_label = self.view.label(cx, ids!(current_identity_label)); script_apply_eval!(cx, current_identity_label, { @@ -256,7 +267,7 @@ impl Widget for TspSettingsScreen { return DrawStep::done(); }; - for (metadata, mut status_and_default) in (0..wallets.len()).filter_map(|i| wallets.get(i)) { + for (metadata, mut status_and_default) in (0..wallets.len()).filter_map(|i| wallets.get(i, self.app_language)) { let item_live_id = LiveId::from_str(metadata.url.as_url_unencoded()); let item = list.item(cx, item_live_id, id!(wallet_entry)).unwrap(); // Pass the wallet metadata in through Scope via props, @@ -300,7 +311,7 @@ impl MatchEvent for TspSettingsScreen { continue; } enqueue_popup_notification( - format!("Removed wallet \"{}\".", metadata.wallet_name), + tr_fmt(self.app_language, "tsp.settings.popup.wallet.removed", &[("wallet_name", metadata.wallet_name.as_str())]), PopupKind::Success, Some(4.0), ); @@ -308,8 +319,7 @@ impl MatchEvent for TspSettingsScreen { // If the removed wallet was the default wallet, notify the user. // The user should then select another wallet as the default. enqueue_popup_notification( - "The default wallet was removed.\n\n\ - TSP features will not work properly until you set a default wallet.", + tr_key(self.app_language, "tsp.settings.popup.wallet.default_removed_warning"), PopupKind::Warning, None, ); @@ -335,7 +345,7 @@ impl MatchEvent for TspSettingsScreen { } Some(TspWalletAction::DefaultWalletChanged(Err(_))) => { enqueue_popup_notification( - "Failed to set default wallet, could not find or open selected wallet.", + tr_key(self.app_language, "tsp.settings.popup.wallet.set_default_failed"), PopupKind::Error, None, ); @@ -355,7 +365,7 @@ impl MatchEvent for TspSettingsScreen { } Some(TspWalletAction::WalletOpened(Err(e))) => { enqueue_popup_notification( - format!("Failed to open wallet: {e}"), + tr_fmt(self.app_language, "tsp.settings.popup.wallet.open_failed", &[("error", &e.to_string())]), PopupKind::Error, None, ); @@ -381,19 +391,19 @@ 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: #(tr_key(self.app_language, "tsp.settings.button.republish_identity")), }); match result { Ok(did) => { enqueue_popup_notification( - format!("Successfully republished identity \"{}\" to the DID server.", did), + tr_fmt(self.app_language, "tsp.settings.popup.identity.republish_success", &[("did", did.as_str())]), PopupKind::Success, Some(5.0), ); } Err(e) => { enqueue_popup_notification( - format!("Failed to republish identity to the DID server: {e}"), + tr_fmt(self.app_language, "tsp.settings.popup.identity.republish_failed", &[("error", &e.to_string())]), PopupKind::Error, None, ); @@ -415,13 +425,13 @@ impl MatchEvent for TspSettingsScreen { if let Some(did) = self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) { cx.copy_to_clipboard(did); enqueue_popup_notification( - "Copied your default TSP identity to the clipboard.", + tr_key(self.app_language, "tsp.settings.popup.identity.copied"), PopupKind::Success, Some(3.0), ); } else { enqueue_popup_notification( - "No default TSP identity has been set.", + tr_key(self.app_language, "tsp.settings.popup.identity.none_set"), PopupKind::Warning, Some(4.0), ); @@ -436,13 +446,13 @@ impl MatchEvent for TspSettingsScreen { if let Some(our_did) = self.wallets.as_ref().and_then(|ws| ws.active_identity.as_deref()) { script_apply_eval!(cx, republish_identity_button, { enabled: false, - text: "Republishing DID now...", + text: #(tr_key(self.app_language, "tsp.settings.button.republishing_now")), }); submit_tsp_request(TspRequest::RepublishDid { did: our_did.to_string() }); } else { enqueue_popup_notification( - "You must set a default TSP identity to be republished.", + tr_key(self.app_language, "tsp.settings.popup.identity.must_set_default"), PopupKind::Error, Some(5.0), ); @@ -463,7 +473,7 @@ impl MatchEvent for TspSettingsScreen { if self.view.button(cx, ids!(import_wallet_button)).clicked(actions) { // TODO: support importing an existing wallet. enqueue_popup_notification( - "Importing an existing wallet is not yet implemented.", + tr_key(self.app_language, "tsp.settings.popup.wallet.import_not_implemented"), PopupKind::Warning, Some(4.0), ); @@ -472,6 +482,36 @@ impl MatchEvent for TspSettingsScreen { } impl TspSettingsScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.view + .label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.title")); + self.view + .label(cx, ids!(section_active_identity)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.section.active_identity")); + self.view + .button(cx, ids!(republish_identity_button)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.button.republish_identity")); + self.view + .label(cx, ids!(section_wallets)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.section.wallets")); + self.view + .label(cx, ids!(no_wallets_text)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.wallet.none")); + self.view + .button(cx, ids!(create_did_button)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.button.create_identity")); + self.view + .button(cx, ids!(create_wallet_button)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.button.create_wallet")); + self.view + .button(cx, ids!(import_wallet_button)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.button.import_wallet")); + self.view.redraw(cx); + } + /// Re-fetches the TSP state and populates this widget's list of wallets. fn refresh_wallets(&mut self) { let tsp_state = tsp_state_ref().lock().unwrap(); @@ -499,7 +539,7 @@ impl TspSettingsScreen { fn has_default_wallet(&self) -> bool { let Some(wallets) = self.wallets.as_ref() else { enqueue_popup_notification( - "No TSP wallets found.\n\nPlease create or import a wallet.", + tr_key(self.app_language, "tsp.settings.popup.wallet.none_found"), PopupKind::Warning, Some(5.0), ); @@ -507,7 +547,7 @@ impl TspSettingsScreen { }; if wallets.active_wallet.is_none() { enqueue_popup_notification( - "No default TSP wallet is set.\n\nPlease select or create a default wallet.", + tr_key(self.app_language, "tsp.settings.popup.wallet.no_default"), PopupKind::Warning, Some(5.0), ); diff --git a/src/tsp/wallet_entry/mod.rs b/src/tsp/wallet_entry/mod.rs index 2c2de8ab4..6832eada4 100644 --- a/src/tsp/wallet_entry/mod.rs +++ b/src/tsp/wallet_entry/mod.rs @@ -5,6 +5,7 @@ use makepad_widgets::*; use crate::{ app::ConfirmDeleteAction, + i18n::{AppLanguage, tr_fmt, tr_key}, shared::{confirmation_modal::ConfirmationModalContent, popup_list::{enqueue_popup_notification, PopupKind}}, tsp::{submit_tsp_request, tsp_settings_screen::{WalletStatus, WalletStatusAndDefault}, TspRequest, TspWalletMetadata} }; @@ -32,7 +33,7 @@ script_mod! { color: (MESSAGE_TEXT_COLOR), text_style: theme.font_bold { font_size: 12 }, } - text: "[Wallet Name]" + text: "" } wallet_path := Label { @@ -43,14 +44,14 @@ script_mod! { color: (MESSAGE_TEXT_COLOR), text_style: theme.font_regular { font_size: 11 }, } - text: "[Wallet Path/URL]" + text: "" } is_default_label_view := View { visible: false, width: Fit, height: Fit margin: Inset{left: 20} - Label { + is_default_label := Label { margin: Inset{top: 2.9} width: Fit, height: Fit flow: Right, @@ -58,7 +59,7 @@ script_mod! { color: (COLOR_FG_ACCEPT_GREEN), text_style: theme.font_bold { font_size: 11 }, } - text: "✅ Default" + text: "" } } @@ -66,7 +67,7 @@ script_mod! { visible: false, width: Fit, height: Fit margin: Inset{left: 20} - Label { + not_found_label := Label { margin: Inset{top: 2.9} width: Fit, height: Fit flow: Right, @@ -74,7 +75,7 @@ script_mod! { color: (COLOR_FG_DANGER_RED), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } - text: "Wallet not found!" + text: "" } } @@ -83,7 +84,7 @@ script_mod! { margin: Inset{left: 20} draw_icon.svg: (ICON_CHECKMARK) icon_walk: Walk{width: 16, height: 16} - text: "Set As Default" + text: "" } remove_wallet_button := RobrixNegativeIconButton { @@ -91,7 +92,7 @@ script_mod! { margin: Inset{left: 20} draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{ width: 16, height: 16 } - text: "Remove From List" + text: "" } delete_wallet_button := RobrixNegativeIconButton { @@ -99,7 +100,7 @@ script_mod! { margin: Inset{left: 20} draw_icon.svg: (ICON_TRASH) icon_walk: Walk{ width: 16, height: 16 } - text: "Delete Wallet" + text: "" } } @@ -115,6 +116,7 @@ pub struct WalletEntry { #[deref] view: View, #[rust] metadata: Option, + #[rust] app_language: AppLanguage, } impl Widget for WalletEntry { @@ -130,13 +132,11 @@ impl Widget for WalletEntry { if self.view.button(cx, ids!(remove_wallet_button)).clicked(actions) { let metadata_clone = metadata.clone(); let content = ConfirmationModalContent { - title_text: "Remove Wallet".into(), - body_text: format!( - "Are you sure you want to remove the wallet \"{}\" \ - from the list?\n\nThis won't delete the actual wallet file.", - metadata.wallet_name - ).into(), - accept_button_text: Some("Remove".into()), + title_text: tr_key(self.app_language, "tsp.wallet_entry.modal.remove.title").into(), + body_text: tr_fmt(self.app_language, "tsp.wallet_entry.modal.remove.body", &[ + ("wallet_name", metadata.wallet_name.as_str()), + ]).into(), + accept_button_text: Some(tr_key(self.app_language, "tsp.wallet_entry.modal.remove.accept").into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_tsp_request(TspRequest::RemoveWallet(metadata_clone)); })), @@ -148,7 +148,7 @@ impl Widget for WalletEntry { if self.view.button(cx, ids!(delete_wallet_button)).clicked(actions) { // TODO: Implement the delete wallet feature. enqueue_popup_notification( - "Delete wallet feature is not yet implemented.", + tr_key(self.app_language, "tsp.wallet_entry.popup.delete_not_implemented"), PopupKind::Warning, None, ); @@ -164,6 +164,7 @@ impl Widget for WalletEntry { if self.metadata.as_ref().is_none_or(|m| m != metadata) { self.metadata = Some(metadata.clone()); } + self.app_language = sd.app_language; self.label(cx, ids!(wallet_name)).set_text( cx, @@ -173,6 +174,26 @@ impl Widget for WalletEntry { cx, metadata.url.as_url_unencoded() ); + self.label(cx, ids!(is_default_label_view.is_default_label)).set_text( + cx, + tr_key(self.app_language, "tsp.wallet_entry.default_label"), + ); + self.label(cx, ids!(not_found_label_view.not_found_label)).set_text( + cx, + tr_key(self.app_language, "tsp.wallet_entry.not_found"), + ); + self.button(cx, ids!(set_default_wallet_button)).set_text( + cx, + tr_key(self.app_language, "tsp.wallet_entry.button.set_default"), + ); + self.button(cx, ids!(remove_wallet_button)).set_text( + cx, + tr_key(self.app_language, "tsp.wallet_entry.button.remove"), + ); + self.button(cx, ids!(delete_wallet_button)).set_text( + cx, + tr_key(self.app_language, "tsp.wallet_entry.button.delete"), + ); // There is a weird makepad bug where if we re-style one instance of the // `set_default_wallet_button` in one WalletEntry, all other instances of that button // get their styling messed up in weird ways. diff --git a/src/tsp_dummy/mod.rs b/src/tsp_dummy/mod.rs index c9451f506..e8c37e8aa 100644 --- a/src/tsp_dummy/mod.rs +++ b/src/tsp_dummy/mod.rs @@ -17,22 +17,23 @@ //! will be replaced with these dummy widgets when the `tsp` feature is not enabled. use makepad_widgets::*; +use crate::{app::AppState, i18n::{AppLanguage, tr_key}}; script_mod! { use mod.prelude.widgets.* use mod.widgets.* - mod.widgets.TspSettingsScreen = View { + mod.widgets.TspSettingsScreen = #(TspSettingsScreen::register_widget(vm)) { width: Fill, height: Fit flow: Down align: Align{x: 0} - TitleLabel { - text: "TSP Wallet Settings" + title := TitleLabel { + text: "" } - Label { + message := Label { width: Fill, height: Fit flow: Flow.Right{wrap: true}, align: Align{x: 0} @@ -41,7 +42,7 @@ script_mod! { color: (MESSAGE_TEXT_COLOR), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } - text: "TSP features are not included in this build.\nTo use TSP, build Robrix with the 'tsp' feature enabled." + text: "" } } @@ -70,3 +71,46 @@ script_mod! { visible: false } } + +#[derive(Script, ScriptHook, Widget)] +pub struct TspSettingsScreen { + #[deref] view: View, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, +} + +impl Widget for TspSettingsScreen { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + self.view.handle_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + self.view.draw_walk(cx, scope, walk) + } +} + +impl TspSettingsScreen { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.view + .label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "tsp.settings.title")); + self.view + .label(cx, ids!(message)) + .set_text(cx, tr_key(self.app_language, "tsp_dummy.message.disabled")); + self.view.redraw(cx); + } +} From b05340aecd06b70187eca3022458707b1168629f Mon Sep 17 00:00:00 2001 From: Alvin Date: Thu, 2 Apr 2026 12:28:34 +0800 Subject: [PATCH 056/283] docs: add deployment guide for Robrix + Palpo + Octos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a comprehensive, self-contained deployment guide (English + Chinese) with ready-to-run Docker Compose example configs. Users can go from zero to a working Robrix → Palpo → Octos → LLM setup in 5 steps. Includes: - Step-by-step quick start with setup.sh for repo cloning - Full configuration reference (palpo.toml, appservice registration, botfather.json, compose.yml) with field-level explanations - Robrix client usage guide (registration, bot interaction, bot management) - End-to-end verification checklist and troubleshooting tables - Example configs that work out of the box (only API key needed) - Registration screenshot - README link to the deployment guide --- README.md | 3 + docs/deployment-guide-zh.md | 603 +++++++++++++++++ docs/deployment-guide.md | 605 ++++++++++++++++++ docs/examples/.env.example | 20 + docs/examples/.gitignore | 8 + .../appservices/octos-registration.yaml | 30 + docs/examples/compose.yml | 96 +++ docs/examples/config/botfather.json | 29 + docs/examples/config/octos.json | 5 + docs/examples/palpo.toml | 44 ++ docs/examples/setup.sh | 37 ++ docs/examples/static/index.html | 9 + docs/images/register-account.png | Bin 0 -> 61809 bytes 13 files changed, 1489 insertions(+) create mode 100644 docs/deployment-guide-zh.md create mode 100644 docs/deployment-guide.md create mode 100644 docs/examples/.env.example create mode 100644 docs/examples/.gitignore create mode 100644 docs/examples/appservices/octos-registration.yaml create mode 100644 docs/examples/compose.yml create mode 100644 docs/examples/config/botfather.json create mode 100644 docs/examples/config/octos.json create mode 100644 docs/examples/palpo.toml create mode 100755 docs/examples/setup.sh create mode 100644 docs/examples/static/index.html create mode 100644 docs/images/register-account.png diff --git a/README.md b/README.md index 529dee4a6..6b8b9d2b6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ Robrix is a Matrix chat client written in Rust to exemplify the features of [Project Robius](https://github.com/project-robius), a framework for multi-platform application development in Rust. Robrix is written using the [Makepad UI toolkit](https://github.com/makepad/makepad/). +> [!TIP] +> **Want to deploy Robrix with an AI bot?** Check out the [Deployment Guide](docs/deployment-guide.md) ([中文版](docs/deployment-guide-zh.md)) — a step-by-step guide to set up Robrix + [Palpo](https://github.com/palpo-im/palpo) (Matrix homeserver) + [Octos](https://github.com/octos-org/octos) (AI bot) together. + Check out our most recent talks and presentations for more info: * Robrix: a complex, multi-platform app in Rust for secure chat using Matrix ([Rust China Conf 2025](https://rustcc.cn/2025conf/schedule.html)) * Videos: [YouTube link](https://www.youtube.com/watch?v=kB-JdmG5kE4), [BiliBili Link](https://www.bilibili.com/video/BV1XJnjzKEZQ) diff --git a/docs/deployment-guide-zh.md b/docs/deployment-guide-zh.md new file mode 100644 index 000000000..4614dc46f --- /dev/null +++ b/docs/deployment-guide-zh.md @@ -0,0 +1,603 @@ +# 部署指南:Robrix + Palpo + Octos + +[English Version](deployment-guide.md) + +本指南帮助你部署一套完整的 **Matrix AI 聊天系统**:Matrix 主服务器、AI 机器人后端,以及 Robrix 客户端——三者协同工作,让你在 Robrix 中与 AI 机器人对话。 + +> **只想快速试试?** 跳到 [快速开始](#2-快速开始) — 5 步即可运行。 + +--- + +## 目录 + +1. [这些项目是什么?](#1-这些项目是什么) +2. [快速开始](#2-快速开始) +3. [配置详解](#3-配置详解) +4. [使用 Robrix](#4-使用-robrix) +5. [端到端验证](#5-端到端验证) +6. [故障排除](#6-故障排除) +7. [延伸阅读](#7-延伸阅读) + +--- + +## 1. 这些项目是什么? + +三个开源项目协同工作,构成一个完整的 AI 聊天系统: + +| 项目 | 角色 | 说明 | +| ---------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 用 Rust 编写的跨平台 Matrix 聊天客户端,基于[Makepad](https://github.com/makepad/makepad/) UI 框架。这是你看到并直接使用的程序——原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix 主服务器 | Rust 原生的 Matrix 主服务器。它存储用户、房间和消息,并在客户端(Robrix)和应用服务(Octos)之间路由事件。可以把它理解为整个系统的"邮局"。 | +| [**Octos**](https://github.com/octos-org/octos) | AI 机器人(应用服务) | Rust 原生的 AI 智能体平台,以[Matrix Application Service](https://spec.matrix.org/latest/application-service-api/)(应用服务)身份运行。它从 Palpo 接收消息,发送给 LLM(如 DeepSeek、OpenAI 等),然后将 AI 的回复发回。 | + +### 架构 + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────┐ +│ Robrix │ Client-Server API │ Palpo │ Appservice API │ Octos │ HTTPS │ LLM │ +│ (客户端) │ ────────────────────► │ (服务器) │ ─────────────────► │ (机器人) │ ──────► │ │ +│ │ ◄──────────────────── │ │ ◄─────────────────── │ │ ◄────── │ │ +└──────────┘ Sliding Sync └──────────┘ Client-Server API └──────────┘ └─────┘ + 你的电脑 Docker :8128 Docker :8009 外部服务 +``` + +**发送消息时的数据流:** + +1. 你在 Robrix 中输入一条消息 +2. Robrix 通过 Matrix Client-Server API 将消息发送给 Palpo +3. Palpo 发现该消息所在的房间有 Octos 存在,通过 Appservice API 将事件推送给 Octos +4. Octos 收到事件后,调用配置的 LLM(如 DeepSeek)获取回复 +5. Octos 通过 Palpo 的 Client-Server API 将 AI 回复发回 +6. Palpo 将回复推送给 Robrix,你看到机器人的回复 + +### 端口与协议 + +| 连接 | 协议 | 默认端口 | 备注 | +| --------------- | -------------------------------- | ----------------------------- | ------------------------- | +| Robrix → Palpo | Client-Server API (Sliding Sync) | 8128(宿主机)→ 8008(容器) | Robrix 唯一需要访问的端口 | +| Palpo → Octos | Appservice API | 8009(Docker 内部网络) | Palpo 向 Octos 推送事件 | +| Octos → Palpo | Client-Server API | 8008(Docker 内部网络) | Octos 通过 Palpo 回复消息 | +| Octos 控制面板 | HTTP | 8010(宿主机)→ 8080(容器) | 可选的管理界面 | +| Octos → LLM | HTTPS | 443(出站) | 外部 API 调用 | + +--- + +## 2. 快速开始 + +4 步在本地跑通所有服务。 + +### 前提条件 + +- **Docker** 和 **Docker Compose**(v2+) +- **Git** +- **一个 LLM API Key** — 如 [DeepSeek](https://platform.deepseek.com/)(有免费额度) +- **Robrix** — [下载预编译版本](https://github.com/Project-Robius-China/robrix2/releases),或从源码构建:`cargo run --release` + +### 步骤 1:获取示例配置 + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/docs/examples +``` + +### 步骤 2:运行初始化脚本 + +```bash +./setup.sh +``` + +此脚本会克隆 Palpo 和 Octos 的源码仓库,并创建 `.env` 文件。两者均从源码构建,以支持所有架构(x86_64、ARM64/Apple Silicon 等)。 + +### 步骤 3:设置 API Key + +编辑 `.env`,将 `your-api-key-here` 替换为你的 DeepSeek API Key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 步骤 4:启动服务 + +```bash +docker compose up -d +``` + +> **注意:** 首次运行会从源码编译 Palpo 和 Octos,这可能需要 **10–30 分钟**(取决于你的机器性能和网络速度)。Palpo 需要编译其 Rust 代码;Octos 还额外需要下载 Node.js、Chromium 等技能插件的运行时工具。后续启动会使用缓存镜像,几秒内完成。 + +检查运行状态: + +```bash +docker compose ps +``` + +你应该看到三个服务(`palpo_postgres`、`palpo`、`octos`)都处于 `running` 状态。 + +### 步骤 5:用 Robrix 连接(构建完成后) + +1. **打开 Robrix**(还没有?见 [4.1 获取 Robrix](#41-获取-robrix)) +2. **设置服务器地址**:在登录界面,在 **Homeserver URL** 输入框中(密码框下方)输入 `http://127.0.0.1:8128` + + +3. **注册新账号**:输入用户名和密码,点击 **Sign up** + + ![注册账号 — 输入用户名、密码和服务器地址](images/register-account.png) +4. **与 AI 机器人对话**:登录后,加入或创建一个房间,然后邀请机器人: + + - 点击房间中的邀请按钮 + - 输入 `@octosbot:127.0.0.1:8128` + - 发送一条消息——AI 机器人应该会回复! + + + +**完成!** 你现在拥有了一个可工作的 Robrix + Palpo + Octos 系统。继续阅读了解配置详情,或跳到 [故障排除](#6-故障排除) 解决问题。 + +--- + +## 3. 配置详解 + +本节解释 `examples/` 目录中的每个配置文件。快速开始已经让你跑起来了——当你需要自定义时再来这里查阅。 + +### 3.1 目录结构 + +``` +examples/ +├── compose.yml # Docker Compose — 编排所有服务 +├── .env.example # 环境变量模板 +├── palpo.toml # Palpo 主服务器配置 +├── appservices/ +│ └── octos-registration.yaml # 应用服务注册文件(连接 Palpo ↔ Octos) +├── config/ +│ ├── botfather.json # Octos 机器人配置(Matrix 通道) +│ └── octos.json # Octos 全局设置 +├── data/ # 持久化数据(运行时自动创建) +│ ├── pgsql/ # PostgreSQL 数据库文件 +│ ├── octos/ # Octos 运行时数据 +│ └── media/ # Palpo 媒体存储 +└── static/ + └── index.html # Palpo 首页(可选) +``` + +### 3.2 令牌生成 + +应用服务注册文件和 Octos 机器人配置共享两个密钥令牌,用于双向认证。示例文件中已预填开发用令牌,但**在生产环境中你必须重新生成**: + +```bash +openssl rand -hex 32 # → 用作 as_token +openssl rand -hex 32 # → 用作 hs_token +``` + +这两个值必须在 `appservices/octos-registration.yaml` 和 `config/botfather.json` 中完全一致。如果不匹配,机器人将无法工作。详见 [令牌匹配检查清单](#37-令牌匹配检查清单)。 + +### 3.3 应用服务注册文件(`appservices/octos-registration.yaml`) + +此文件告诉 Palpo 关于 Octos 的信息——Octos 管理哪些用户命名空间,以及将事件发送到哪里。 + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "<你的-as-token>" +hs_token: "<你的-hs-token>" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| 字段 | 说明 | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `id` | 此应用服务注册的唯一标识符。 | +| `url` | Palpo 发送事件的目标地址。使用 Docker 服务名 `octos`(不是 `localhost`),因为两个容器在同一个 Docker 网络中。 | +| `as_token` | Octos 调用 Palpo API 时使用的令牌。必须与 `botfather.json` 匹配。 | +| `hs_token` | Palpo 向 Octos 推送事件时使用的令牌。必须与 `botfather.json` 匹配。 | +| `sender_localpart` | 机器人的 Matrix 本地用户名。最终变为 `@octosbot:127.0.0.1:8128`。 | +| `rate_limited` | 设为 `false`,让机器人回复不受速率限制。 | +| `namespaces.users` | 此应用服务管理的用户 ID 正则匹配模式。包含机器人本身(`@octosbot:...`)和动态创建的子机器人(`@octosbot_*:...`)。 | + +### 3.4 Palpo 配置(`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP 监听器(Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| 字段 | 说明 | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `server_name` | 所有 Matrix ID 的域名部分(如 `@user:127.0.0.1:8128`)。生产环境使用你的实际域名。 | +| `allow_registration` | 是否允许新用户注册。设为 `true` 以便 Robrix 用户可以创建账号。生产环境中初始配置完成后可改为 `false`。 | +| `yes_i_am_very_very_sure_...` | 当 `allow_registration = true` 时必填的安全确认。字段名故意很长,提醒你开放注册的安全风险。 | +| `enable_admin_room` | 启用服务器管理员房间。 | +| `appservice_registration_dir` | Palpo 启动时自动加载此目录下所有 `.yaml` 文件。Octos 就是通过这种方式被发现的。 | +| `[[listeners]]` | 网络监听器。每个条目定义一个 Palpo 监听的地址。 | +| `[logger]` | 日志格式。`"pretty"` 用于开发,`"json"` 用于生产。 | +| `[db]` | PostgreSQL 连接配置。`palpo_postgres` 是 Docker 服务名。密码必须与 `compose.yml` 中的 `POSTGRES_PASSWORD` 匹配。 | +| `[well_known]` | 用于客户端发现服务器。必须与外部可访问的地址匹配。 | + +> **注意:** 在这个本地 Docker 示例里,Matrix 身份统一使用 `127.0.0.1:8128`。因此 `server_name`、应用服务正则和机器人用户 ID 都必须写成 `127.0.0.1:8128`。只有容器之间通信时才使用 `palpo:8008`、`octos:8009` 这类 Docker 服务名。 + +> **延伸阅读:** [Palpo GitHub](https://github.com/palpo-im/palpo) 了解更多高级配置(联邦、TLS、TURN 等)。 + +### 3.5 Octos 机器人配置(`config/botfather.json`) + +此文件定义机器人的身份、LLM 提供商和 Matrix 通道配置。 + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "<你的-as-token>", + "hs_token": "<你的-hs-token>", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **重要:** `created_at` 和 `updated_at` 字段是 Octos **必需的**。如果缺少这两个字段,Octos 会跳过该 profile,机器人将无法启动。 + +**LLM 提供商设置:** + +| 字段 | 说明 | +| --------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `provider` | LLM 提供商名称。Octos 支持 `deepseek`、`openai`、`anthropic` 等 [14 种提供商](https://octos-org.github.io/octos/)。 | +| `model` | 模型标识符(如 `deepseek-chat`、`gpt-4o`、`claude-sonnet-4-20250514`)。 | +| `api_key_env` | 存放 API Key 的环境变量名称。 | + +**Matrix 通道设置:** + +| 字段 | 说明 | +| --------------------------- | --------------------------------------------------------------------- | +| `type` | 必须为 `"matrix"`。 | +| `homeserver` | Palpo 的内部 URL。使用 Docker 服务名 `palpo`,不是 `localhost`。 | +| `as_token` / `hs_token` | 必须与应用服务注册 YAML 文件匹配。 | +| `server_name` | Matrix 域名。必须与 `palpo.toml` 中的 `server_name` 一致。 | +| `sender_localpart` | 机器人用户名。必须与注册文件一致。 | +| `user_prefix` | 动态创建的子机器人用户 ID 前缀(如 `octosbot_translator`)。 | +| `port` | Octos 监听 Palpo 应用服务事件的端口。 | +| `allowed_senders` | 允许与机器人对话的 Matrix 用户 ID。空数组 `[]` = 所有人都可以对话。 | + +> **注意:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关,但不能混用。 + +**Gateway 设置:** + +| 字段 | 说明 | +| --------------- | --------------------------------------------------------------- | +| `max_history` | 作为 LLM 上下文发送的最大历史消息数量。 | +| `queue_mode` | Octos 处理传入消息的方式。`followup` 将新消息排队并顺序处理。 | + +> **延伸阅读:** [Octos Book — LLM 提供商与路由](https://octos-org.github.io/octos/) 了解全部 14 种提供商、降级链和自适应路由。 + +### 3.6 Docker Compose(`compose.yml`) + +提供的 `compose.yml` 启动三个服务: + +| 服务 | 镜像 | 暴露端口 | 用途 | +| ------------------ | --------------------------------- | ---------------------------- | ----------------- | +| `palpo_postgres` | `postgres:17` | *(无,仅内部)* | Palpo 的数据库 | +| `palpo` | 从源码构建 | `8128:8008` | Matrix 主服务器 | +| `octos` | 从源码构建 | `8009:8009`、`8010:8080` | AI 机器人应用服务 | + +**端口映射说明:** + +- `8128` → Robrix 连接此端口(Client-Server API) +- `8009` → Palpo 向 Octos 推送事件(Appservice API) +- `8010` → Octos 管理控制面板(可选,用于监控) + +**持久化卷:** + +| 卷 | 用途 | +| ---------------- | ------------------------------------------------- | +| `./data/pgsql` | PostgreSQL 数据。`docker compose down` 后保留。 | +| `./data/octos` | Octos 运行时数据(会话、记忆)。 | +| `./data/media` | 通过 Matrix 上传的媒体文件(图片、文件)。 | + +**环境变量(`.env`):** + +| 变量 | 必填 | 默认值 | 说明 | +| -------------------- | ------------ | ---------------------- | ----------------------- | +| `DEEPSEEK_API_KEY` | **是** | — | 你的 LLM API Key | +| `DB_PASSWORD` | 否 | `palpo_dev_password` | PostgreSQL 密码 | +| `RUST_LOG` | 否 | `octos=debug,info` | 日志详细程度 | + +### 3.7 令牌匹配检查清单 + +最常见的配置错误是令牌不匹配。以下值在两个文件中**必须完全一致**: + +| 值 | 在 `octos-registration.yaml` 中 | 在 `botfather.json` 中 | +| -------------------- | --------------------------------- | ---------------------------------- | +| `as_token` | `as_token: "abc..."` | `"as_token": "abc..."` | +| `hs_token` | `hs_token: "def..."` | `"hs_token": "def..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | `regex: "@octosbot:127\\.0\\.0\\.1:8128"` | `"server_name": "127.0.0.1:8128"` | + +如果有任何不匹配,机器人将不会响应消息。提交 bug 报告前请先检查! + +--- + +## 4. 使用 Robrix + +本节介绍如何使用 Robrix 客户端连接 Palpo 服务器并与 Octos AI 机器人交互。 + +### 4.1 获取 Robrix + +**下载预编译版本(推荐):** + +从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载。支持 macOS、Linux 和 Windows。 + +**或从源码构建:** + +1. [安装 Rust](https://www.rust-lang.org/tools/install) +2. Linux 上安装依赖: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` +3. 构建并运行: + ```bash + cargo run --release + ``` + +移动端构建(Android/iOS)和打包分发的说明,详见 [Robrix README](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop)。 + +### 4.2 连接到 Palpo 服务器 + +启动 Robrix 后,你会看到登录界面: + + + +**Homeserver URL** 输入框位于登录表单底部。如果留空,默认连接 `matrix.org`。要连接你的本地 Palpo 实例: + +- **本地部署:** 输入 `http://127.0.0.1:8128` +- **远程部署:** 输入 `https://your.server.name`(或 `http://服务器IP:8128`) + + + +> **注意:** Robrix 要求主服务器支持 [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync)。Palpo 原生支持此功能。 + +### 4.3 注册与登录 + +**首次使用——注册新账号:** + +1. 输入你想要的**用户名**和**密码** +2. 在**确认密码**栏(注册时出现)再次输入密码 +3. 输入 **Homeserver URL**(如 `http://127.0.0.1:8128`) +4. 点击 **Sign up** + +![注册账号 — 输入用户名、密码和服务器地址](images/register-account.png) + +**再次使用——登录:** + +1. 输入**用户名**和**密码** +2. 输入 **Homeserver URL** +3. 点击 **Log in** + +登录成功后,你会看到房间列表(新账号为空)。 + + + +### 4.4 与 AI 机器人交互 + +有两种方式开始与机器人聊天: + +#### 方式一:邀请机器人到房间 + +1. 创建一个新房间或打开现有房间 +2. 点击房间中的**邀请**按钮 +3. 输入机器人的 Matrix ID:`@octosbot:127.0.0.1:8128`(将 `127.0.0.1:8128` 替换为你的 `server_name`) +4. 机器人会自动加入房间 + + + +#### 方式二:加入机器人所在的房间 + +1. 点击**加入房间**按钮(或使用房间浏览器) +2. 输入机器人已设置的房间别名或 ID +3. 开始聊天 + + + +#### 与机器人对话 + +机器人加入房间后,直接输入消息并发送即可。机器人会通过配置的 LLM 处理你的消息并回复。 + + + +### 4.5 机器人管理(高级功能) + +Robrix 内置了通过 BotFather 系统管理 Matrix 机器人的功能。 + +#### 启用应用服务支持 + +1. 打开 Robrix 的**设置** +2. 导航到 **Bot Settings** +3. 开启 **Enable App Service** +4. 输入 **BotFather User ID**(如 `@octosbot:127.0.0.1:8128`) +5. 点击 **Save** + + + +#### 创建子机器人 + +启用 BotFather 后,你可以创建专用的子机器人: + +1. 使用 **Create Bot** 对话框 +2. 填写: + - **Username** — 仅限小写字母、数字和下划线(如 `translator_bot`) + - **Display Name** — 可读的显示名称(如 "翻译机器人") + - **System Prompt** — 机器人的初始指令(如 "你是一个翻译器。将所有消息翻译成中文。") +3. 点击 **Create Bot** + +机器人将以 `@octosbot_:127.0.0.1:8128` 的身份创建。 + + + +--- + +## 5. 端到端验证 + +部署完成后,按照以下检查清单确认一切正常: + +### 服务健康检查 + +```bash +# 检查所有容器是否运行 +docker compose ps + +# 检查 Palpo 日志是否有启动错误 +docker compose logs palpo | tail -20 + +# 检查 Octos 日志——寻找 "appservice listening" 或类似信息 +docker compose logs octos | tail -20 + +# 验证 Palpo 是否响应 +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### 客户端连接 + +- [ ] Robrix 能连接到 `http://127.0.0.1:8128` +- [ ] 能注册新账号 +- [ ] 登录后房间列表能加载(新账号可能为空) +- [ ] 能创建新房间 + +### 机器人交互 + +- [ ] 能邀请 `@octosbot:127.0.0.1:8128` 到房间 +- [ ] 机器人加入房间(如果没有,检查 `docker compose logs octos`) +- [ ] 发送消息后机器人回复 +- [ ] 回复内容合理(确认 LLM 连接正常) + +### 如果某步失败 + +按照数据流顺序检查日志: + +```bash +# 1. Palpo 是否收到了 Robrix 的消息? +docker compose logs palpo --since 1m + +# 2. Palpo 是否将事件转发给了 Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Octos 是否收到并处理了事件? +docker compose logs octos --since 1m + +# 4. Octos 是否成功调用了 LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 6. 故障排除 + +### 6.1 服务启动问题 + +| 症状 | 原因 | 解决方法 | +| --------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------- | +| `palpo_postgres` 无法启动 | 端口 5432 已被占用,或数据损坏 | 检查 `docker compose logs palpo_postgres`。删除 `data/pgsql/` 重新开始。 | +| `palpo` 构建失败 | 网络问题或源码获取失败 | 确保 Docker 能访问 `github.com`。检查 `docker compose logs palpo` 查看构建错误。 | +| `palpo` 启动时崩溃 | `palpo.toml` 语法错误或数据库连接失败 | 检查日志。确保 `palpo_postgres` 先正常运行。验证数据库密码一致。 | +| `octos` 构建失败 | 缺少 Dockerfile 或网络问题 | 确保 Docker 能访问 `github.com`。或者在本地构建 Octos 并修改 `compose.yml` 使用本地镜像。 | +| `octos` 启动但日志有错误 | `botfather.json` 无效或缺少 API Key | 检查 JSON 语法。验证 `.env` 中已设置 `DEEPSEEK_API_KEY`。 | + +### 6.2 Robrix 连接问题 + +| 症状 | 原因 | 解决方法 | +| -------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| "无法连接到服务器" | Homeserver URL 错误或 Palpo 未运行 | 确认 Palpo 正在运行(`docker compose ps`)。确认 URL 为 `http://127.0.0.1:8128`。 | +| 登录成功但没有房间 | 新账号的正常现象 | 创建一个新房间。加入或创建后房间会出现在列表中。 | +| 注册失败 | `palpo.toml` 中 `allow_registration = false`,或 server_name 不匹配 | 检查 `palpo.toml`。确保 `allow_registration = true`。 | +| "Homeserver 不支持 Sliding Sync" | Palpo 版本过旧 | 重新构建 Palpo:`docker compose build --no-cache palpo`。 | +| 连接超时 | 防火墙阻止了端口 8128 | 检查防火墙规则。macOS 上在系统设置中允许传入连接。 | + +### 6.3 机器人问题 + +| 症状 | 原因 | 解决方法 | +| --------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| 机器人不响应消息 | 注册文件和配置文件之间令牌不匹配 | 验证[令牌匹配检查清单](#37-令牌匹配检查清单)。 | +| Palpo 日志中出现 `Connection refused` | Octos 未运行,或注册 YAML 中 `url` 错误 | 确保 Octos 正在运行。`url` 必须使用 Docker 服务名(`http://octos:8009`),不能用 `localhost`。 | +| `User ID not in namespace` | `sender_localpart` 与 `namespaces.users` 正则不匹配 | 更新 `octos-registration.yaml` 中的正则表达式,包含机器人的完整用户 ID 模式。 | +| 机器人加入房间但回复空消息 | LLM API Key 无效或额度不足 | 检查 `docker compose logs octos` 中的 API 错误。验证 API Key 和账户余额。 | +| 部分用户的消息被忽略 | `botfather.json` 中的 `allowed_senders` 过滤 | 将用户的 Matrix ID 添加到 `allowed_senders` 数组中,或设为 `[]` 允许所有人。 | + +### 6.4 常用调试命令 + +```bash +# 实时查看所有服务日志 +docker compose logs -f + +# 查看特定服务的日志 +docker compose logs -f palpo +docker compose logs -f octos + +# 重启单个服务 +docker compose restart octos + +# 检查 Palpo 的 API +curl http://127.0.0.1:8128/_matrix/client/versions + +# 完全重置(警告:删除所有数据) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +--- + +## 7. 延伸阅读 + +- **Octos 完整文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — 覆盖所有 LLM 提供商、通道(Telegram、Slack、Discord 等)、技能、记忆系统和高级配置。 +- **Octos Matrix Appservice 指南:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — 本文档参考的原始指南,包含更多上下文。 +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo 主服务器文档。 +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix 客户端、构建说明和功能追踪。 +- **Matrix Appservice 规范:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — 应用服务的 Matrix 协议规范。 + +--- + +*本指南内容截至 2026 年 4 月。最新更新请查看各项目的仓库。* diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 000000000..d0d40cac5 --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,605 @@ +# Deploying Robrix with Palpo and Octos + +[中文版 (Chinese Version)](deployment-guide-zh.md) + +This guide walks you through deploying a complete **Matrix AI chat system**: a Matrix homeserver, an AI bot backend, and the Robrix client — all working together so you can chat with an AI bot from Robrix. + +> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start) — 5 steps to get running. + +--- + +## Table of Contents + +1. [What Are These Projects?](#1-what-are-these-projects) +2. [Quick Start](#2-quick-start) +3. [Configuration Details](#3-configuration-details) +4. [Using Robrix](#4-using-robrix) +5. [End-to-End Verification](#5-end-to-end-verification) +6. [Troubleshooting](#6-troubleshooting) +7. [Further Reading](#7-further-reading) + +--- + +## 1. What Are These Projects? + +Three open-source projects work together to form a complete AI chat system: + +| Project | Role | What it does | +|---------|------|-------------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client written in Rust using [Makepad](https://github.com/makepad/makepad/). This is what you see and interact with — it runs natively on macOS, Linux, Windows, Android, and iOS. | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix Homeserver | A Rust-native Matrix homeserver. It stores users, rooms, and messages, and routes events between clients (Robrix) and application services (Octos). Think of it as the "post office" of the system. | +| [**Octos**](https://github.com/octos-org/octos) | AI Bot (Appservice) | A Rust-native AI agent platform that runs as a [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/). It receives messages from Palpo, sends them to an LLM (like DeepSeek, OpenAI, etc.), and posts the AI's reply back. | + +### Architecture + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────┐ +│ Robrix │ Client-Server API │ Palpo │ Appservice API │ Octos │ HTTPS │ LLM │ +│ (Client) │ ────────────────────► │ (Server) │ ─────────────────► │ (Bot) │ ──────► │ │ +│ │ ◄──────────────────── │ │ ◄─────────────────── │ │ ◄────── │ │ +└──────────┘ Sliding Sync └──────────┘ Client-Server API └──────────┘ └─────┘ + Your machine Docker :8128 Docker :8009 External +``` + +**Data flow when you send a message:** + +1. You type a message in Robrix +2. Robrix sends it to Palpo via the Matrix Client-Server API +3. Palpo sees the message is in a room where Octos is present, and pushes the event to Octos via the Appservice API +4. Octos receives the event, calls the configured LLM (e.g., DeepSeek), and gets a response +5. Octos posts the AI reply back through Palpo's Client-Server API +6. Palpo delivers the reply to Robrix, where you see the bot's response + +### Ports and Protocols + +| Connection | Protocol | Default Port | Notes | +|-----------|----------|-------------|-------| +| Robrix → Palpo | Client-Server API (Sliding Sync) | 8128 (host) → 8008 (container) | The only port Robrix needs | +| Palpo → Octos | Appservice API | 8009 (internal Docker network) | Palpo pushes events to Octos | +| Octos → Palpo | Client-Server API | 8008 (internal Docker network) | Octos replies through Palpo | +| Octos Dashboard | HTTP | 8010 (host) → 8080 (container) | Optional admin UI | +| Octos → LLM | HTTPS | 443 (outbound) | External API call | + +--- + +## 2. Quick Start + +Get everything running locally in 4 steps. + +### Prerequisites + +- **Docker** and **Docker Compose** (v2+) +- **Git** +- **An LLM API key** — e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available) +- **Robrix** — [download a pre-built release](https://github.com/Project-Robius-China/robrix2/releases), or build from source with `cargo run --release` + +### Step 1: Get the Example Configuration + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/docs/examples +``` + +### Step 2: Run Setup + +```bash +./setup.sh +``` + +This clones the Palpo and Octos source repos and creates your `.env` file. Both are built from source to support all architectures (x86_64, ARM64/Apple Silicon, etc.). + +### Step 3: Set Your API Key + +Edit `.env` and replace `your-api-key-here` with your actual DeepSeek API key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### Step 4: Start the Services + +```bash +docker compose up -d +``` + +> **Note:** The first run builds both Palpo and Octos from source, which can take **10–30 minutes** depending on your machine and network speed. Palpo compiles its Rust codebase; Octos additionally downloads runtime tools (Node.js, Chromium) for its skill plugins. Subsequent runs use cached images and start in seconds. + +Check that everything is running: + +```bash +docker compose ps +``` + +You should see three services (`palpo_postgres`, `palpo`, `octos`) all in `running` state. + +### Step 5: Connect with Robrix (after build completes) + +1. **Open Robrix** (don't have it yet? See [4.1 Getting Robrix](#41-getting-robrix)) + +2. **Set the homeserver**: In the login screen, enter `http://127.0.0.1:8128` in the **Homeserver URL** field (below the password field). + + + +3. **Register a new account**: Enter a username and password, then click **Sign up**. + + ![Register account — enter username, password, and homeserver URL](images/register-account.png) + +4. **Talk to the AI bot**: After logging in, join a room or create one, then invite the bot: + - Click the invite button in the room + - Enter `@octosbot:127.0.0.1:8128` + - Send a message — the AI bot should reply! + + + +**That's it!** You now have a working Robrix + Palpo + Octos setup. Read on for configuration details, or jump to [Troubleshooting](#6-troubleshooting) if something isn't working. + +--- + +## 3. Configuration Details + +This section explains every configuration file in the `examples/` directory. You already have a working setup from the Quick Start — come here when you want to customize. + +### 3.1 Directory Layout + +``` +examples/ +├── compose.yml # Docker Compose — orchestrates all services +├── .env.example # Environment variables template +├── palpo.toml # Palpo homeserver configuration +├── appservices/ +│ └── octos-registration.yaml # Appservice registration (links Palpo ↔ Octos) +├── config/ +│ ├── botfather.json # Octos bot profile (Matrix channel config) +│ └── octos.json # Octos global settings +├── data/ # Persistent data (created at runtime) +│ ├── pgsql/ # PostgreSQL database files +│ ├── octos/ # Octos runtime data +│ └── media/ # Palpo media storage +└── static/ + └── index.html # Palpo homepage (optional) +``` + +### 3.2 Token Generation + +The Appservice registration and the Octos bot profile share two secret tokens for mutual authentication. The example files come with pre-filled development tokens, but **you must generate new tokens for production**: + +```bash +openssl rand -hex 32 # → use as as_token +openssl rand -hex 32 # → use as hs_token +``` + +These two values must be identical in `appservices/octos-registration.yaml` and `config/botfather.json`. If they don't match, nothing works. See [Token Matching Checklist](#37-token-matching-checklist). + +### 3.3 Appservice Registration (`appservices/octos-registration.yaml`) + +This file tells Palpo about Octos — which user namespaces Octos manages and where to send events. + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "" +hs_token: "" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| Field | Description | +|-------|-------------| +| `id` | A unique identifier for this appservice registration. | +| `url` | Where Palpo sends events. Uses the Docker service name `octos` (not `localhost`), because both containers share the same Docker network. | +| `as_token` | Token that Octos uses when calling Palpo's API. Must match `botfather.json`. | +| `hs_token` | Token that Palpo uses when pushing events to Octos. Must match `botfather.json`. | +| `sender_localpart` | The bot's Matrix local username. Becomes `@octosbot:127.0.0.1:8128`. | +| `rate_limited` | Set to `false` so the bot can respond without rate limits. | +| `namespaces.users` | Regex patterns for user IDs that this appservice owns. Include the bot itself (`@octosbot:...`) and any dynamically-created bot users (`@octosbot_*:...`). | + +### 3.4 Palpo Configuration (`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP listener (Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| Field | Description | +|-------|-------------| +| `server_name` | The domain part of all Matrix IDs (e.g., `@user:127.0.0.1:8128`). For production, use your actual domain. | +| `allow_registration` | Whether new users can register. Set to `true` so Robrix users can create accounts. For production, consider setting to `false` after initial setup. | +| `yes_i_am_very_very_sure_...` | Required safety confirmation when `allow_registration = true`. The intentionally long name reminds you of the security implications. | +| `enable_admin_room` | Enables the server admin room for management. | +| `appservice_registration_dir` | Palpo loads all `.yaml` files from this directory on startup. This is how it discovers Octos. | +| `[[listeners]]` | Network listeners. Each entry defines an address Palpo listens on. | +| `[logger]` | Log format. `"pretty"` for development, `"json"` for production. | +| `[db]` | PostgreSQL connection. `palpo_postgres` is the Docker service name. The password must match `POSTGRES_PASSWORD` in `compose.yml`. | +| `[well_known]` | Used by clients for server discovery. Must match externally-reachable addresses. | + +> **Important:** In this local Docker example, the Matrix identity is `127.0.0.1:8128`, so `server_name`, the appservice regex, and bot user IDs must all use `127.0.0.1:8128`. Only container-to-container traffic should use Docker service names like `palpo:8008` or `octos:8009`. + +> **Further reading:** [Palpo on GitHub](https://github.com/palpo-im/palpo) for advanced configuration (federation, TLS, TURN, etc.). + +### 3.5 Octos Bot Profile (`config/botfather.json`) + +This file defines the bot's identity, LLM provider, and Matrix channel configuration. + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "", + "hs_token": "", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **Important:** The `created_at` and `updated_at` fields are **required** by Octos. If they are missing, Octos will skip this profile and the bot will never start. + +**LLM Provider settings:** + +| Field | Description | +|-------|-------------| +| `provider` | LLM provider name. Octos supports `deepseek`, `openai`, `anthropic`, and [12 more](https://octos-org.github.io/octos/). | +| `model` | Model identifier (e.g., `deepseek-chat`, `gpt-4o`, `claude-sonnet-4-20250514`). | +| `api_key_env` | Name of the environment variable holding your API key. | + +**Matrix channel settings:** + +| Field | Description | +|-------|-------------| +| `type` | Must be `"matrix"`. | +| `homeserver` | Palpo's internal URL. Uses Docker service name `palpo`, not `localhost`. | +| `as_token` / `hs_token` | Must match the appservice registration YAML. | +| `server_name` | The Matrix domain. Must match `server_name` in `palpo.toml`. | +| `sender_localpart` | Bot username. Must match the registration file. | +| `user_prefix` | Prefix for dynamically-created bot users (e.g., `octosbot_translator`). | +| `port` | Port Octos listens on for Appservice events from Palpo. | +| `allowed_senders` | Matrix user IDs allowed to talk to the bot. Empty `[]` = everyone can talk to it. | + +> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related, but they are not interchangeable. + +**Gateway settings:** + +| Field | Description | +|-------|-------------| +| `max_history` | Maximum number of messages to include as context for the LLM. | +| `queue_mode` | How Octos handles incoming messages. `followup` queues new messages and processes them sequentially. | + +> **Further reading:** [Octos Book — LLM Providers & Routing](https://octos-org.github.io/octos/) for all 14 supported providers, fallback chains, and adaptive routing. + +### 3.6 Docker Compose (`compose.yml`) + +The provided `compose.yml` starts three services: + +| Service | Image | Exposed Ports | Purpose | +|---------|-------|--------------|---------| +| `palpo_postgres` | `postgres:17` | *(none, internal only)* | Database for Palpo | +| `palpo` | Built from source | `8128:8008` | Matrix homeserver | +| `octos` | Built from source | `8009:8009`, `8010:8080` | AI bot appservice | + +**Port mapping explanation:** + +- `8128` → Robrix connects here (Client-Server API) +- `8009` → Palpo pushes events to Octos here (Appservice API) +- `8010` → Octos admin dashboard (optional, for monitoring) + +**Persistent volumes:** + +| Volume | Purpose | +|--------|---------| +| `./data/pgsql` | PostgreSQL data. Survives `docker compose down`. | +| `./data/octos` | Octos runtime data (sessions, memory). | +| `./data/media` | Media files uploaded through Matrix (images, files). | + +**Environment variables (`.env`):** + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DEEPSEEK_API_KEY` | **Yes** | — | Your LLM API key | +| `DB_PASSWORD` | No | `palpo_dev_password` | PostgreSQL password | +| `RUST_LOG` | No | `octos=debug,info` | Log verbosity | + +### 3.7 Token Matching Checklist + +The most common configuration error is a token mismatch. These values **must be identical** across both files: + +| Value | In `octos-registration.yaml` | In `botfather.json` | +|-------|------------------------------|---------------------| +| `as_token` | `as_token: "abc..."` | `"as_token": "abc..."` | +| `hs_token` | `hs_token: "def..."` | `"hs_token": "def..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | `regex: "@octosbot:127\\.0\\.0\\.1:8128"` | `"server_name": "127.0.0.1:8128"` | + +If any of these don't match, the bot will not respond to messages. Double-check before filing a bug report! + +--- + +## 4. Using Robrix + +This section covers how to use Robrix as a client to connect to your Palpo server and interact with the Octos AI bot. + +### 4.1 Getting Robrix + +**Download a pre-built release (recommended):** + +Download from the [Robrix releases page](https://github.com/Project-Robius-China/robrix2/releases). Available for macOS, Linux, and Windows. + +**Or build from source:** + +1. [Install Rust](https://www.rust-lang.org/tools/install) +2. On Linux, install dependencies: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` +3. Build and run: + ```bash + cargo run --release + ``` + +For mobile builds (Android/iOS) and packaging for distribution, see the [Robrix README](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop). + +### 4.2 Connecting to Palpo + +When you launch Robrix, you'll see the login screen: + + + +The **Homeserver URL** field is at the bottom of the login form. It defaults to `matrix.org` if left empty. To connect to your local Palpo instance: + +- **Local deployment:** Enter `http://127.0.0.1:8128` +- **Remote deployment:** Enter `https://your.server.name` (or `http://your-server-ip:8128`) + + + +> **Important:** Robrix requires [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync) support from the homeserver. Palpo supports this natively. + +### 4.3 Registration and Login + +**First time — Register a new account:** + +1. Enter your desired **username** and **password** +2. Enter the password again in the **Confirm password** field (appears for registration) +3. Enter the **Homeserver URL** (e.g., `http://127.0.0.1:8128`) +4. Click **Sign up** + +![Register account — enter username, password, and homeserver URL](images/register-account.png) + +**Returning — Log in:** + +1. Enter your **username** and **password** +2. Enter the **Homeserver URL** +3. Click **Log in** + +After successful login, you'll see your room list (empty if this is a fresh account). + + + +### 4.4 Interacting with the AI Bot + +There are two ways to start chatting with the bot: + +#### Method 1: Invite the bot to a room + +1. Create a new room or open an existing one +2. Click the **invite** button in the room +3. Enter the bot's Matrix ID: `@octosbot:127.0.0.1:8128` (replace `127.0.0.1:8128` with your `server_name`) +4. The bot will automatically join the room + + + +#### Method 2: Join a room where the bot is present + +1. Click the **Join Room** button (or use the room browser) +2. Enter a room alias or ID where the bot has been set up +3. Start chatting + + + +#### Chatting with the bot + +Once the bot is in the room, simply type a message and send it. The bot will process your message through the configured LLM and respond. + + + +### 4.5 Bot Management (Advanced) + +Robrix has built-in support for managing Matrix bots through the BotFather system. + +#### Enabling App Service support + +1. Open **Settings** in Robrix +2. Navigate to **Bot Settings** +3. Toggle **Enable App Service** on +4. Enter the **BotFather User ID** (e.g., `@octosbot:127.0.0.1:8128`) +5. Click **Save** + + + +#### Creating child bots + +With BotFather enabled, you can create specialized child bots: + +1. Use the **Create Bot** dialog +2. Fill in: + - **Username** — lowercase letters, digits, and underscores only (e.g., `translator_bot`) + - **Display Name** — human-readable name (e.g., "Translator Bot") + - **System Prompt** — initial instructions for the bot (e.g., "You are a translator. Translate all messages to English.") +3. Click **Create Bot** + +The bot will be created as `@octosbot_:127.0.0.1:8128`. + + + +--- + +## 5. End-to-End Verification + +After setting up, run through this checklist to confirm everything works: + +### Service Health + +```bash +# Check all containers are running +docker compose ps + +# Check Palpo logs for startup errors +docker compose logs palpo | tail -20 + +# Check Octos logs — look for "appservice listening" or similar +docker compose logs octos | tail -20 + +# Verify Palpo is responding +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### Client Connectivity + +- [ ] Robrix can connect to `http://127.0.0.1:8128` +- [ ] You can register a new account +- [ ] After login, the room list loads (may be empty) +- [ ] You can create a new room + +### Bot Interaction + +- [ ] You can invite `@octosbot:127.0.0.1:8128` to a room +- [ ] The bot joins the room (check `docker compose logs octos` if it doesn't) +- [ ] Sending a message triggers a response from the bot +- [ ] The response content makes sense (confirms LLM connection works) + +### If something fails + +Check the logs in this order — they follow the data flow: + +```bash +# 1. Is Palpo receiving messages from Robrix? +docker compose logs palpo --since 1m + +# 2. Is Palpo forwarding events to Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Is Octos receiving and processing events? +docker compose logs octos --since 1m + +# 4. Is Octos successfully calling the LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 6. Troubleshooting + +### 6.1 Service Startup Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `palpo_postgres` won't start | Port 5432 already in use, or corrupt data | Check `docker compose logs palpo_postgres`. Remove `data/pgsql/` to start fresh. | +| `palpo` build fails | Network issue or missing source | Ensure Docker can reach `github.com`. Check `docker compose logs palpo` for build errors. | +| `palpo` crashes on startup | Bad `palpo.toml` syntax or DB connection failure | Check logs. Ensure `palpo_postgres` is healthy first. Verify DB password matches. | +| `octos` build fails | Missing Dockerfile or network issue | Ensure Docker can reach `github.com`. Alternatively, build Octos locally and update `compose.yml` to use a local image. | +| `octos` starts but logs show errors | Invalid `botfather.json` or missing API key | Check JSON syntax. Verify `DEEPSEEK_API_KEY` is set in `.env`. | + +### 6.2 Robrix Connection Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Cannot connect to server" | Wrong homeserver URL or Palpo not running | Verify Palpo is running (`docker compose ps`). Confirm URL is `http://127.0.0.1:8128`. | +| Login succeeds but no rooms appear | Normal for a fresh account | Create a new room. Rooms will appear as you join or create them. | +| Registration fails | `allow_registration = false` in `palpo.toml`, or server_name mismatch | Check `palpo.toml`. Ensure `allow_registration = true`. | +| "Homeserver does not support Sliding Sync" | Palpo version too old | Rebuild Palpo: `docker compose build --no-cache palpo`. | +| Connection times out | Firewall blocking port 8128 | Check firewall rules. On macOS, allow incoming connections in System Settings. | + +### 6.3 Bot Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Bot does not respond to messages | Token mismatch between registration and profile | Verify the [Token Matching Checklist](#37-token-matching-checklist). | +| `Connection refused` in Palpo logs | Octos not running, or wrong `url` in registration YAML | Ensure Octos is running. The `url` must use the Docker service name (`http://octos:8009`), not `localhost`. | +| `User ID not in namespace` | `sender_localpart` doesn't match `namespaces.users` regex | Update the regex in `octos-registration.yaml` to include the bot's full user ID pattern. | +| Bot joins room but gives empty replies | LLM API key invalid or quota exceeded | Check `docker compose logs octos` for API errors. Verify your API key and account balance. | +| Messages from some users are ignored | `allowed_senders` filtering in `botfather.json` | Add the user's Matrix ID to the `allowed_senders` array, or set it to `[]` to allow everyone. | + +### 6.4 Useful Debug Commands + +```bash +# View real-time logs for all services +docker compose logs -f + +# View logs for a specific service +docker compose logs -f palpo +docker compose logs -f octos + +# Restart a single service +docker compose restart octos + +# Check Palpo's client API +curl http://127.0.0.1:8128/_matrix/client/versions + +# Full reset (WARNING: deletes all data) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +--- + +## 7. Further Reading + +- **Octos Documentation (full):** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — covers all LLM providers, channels (Telegram, Slack, Discord, etc.), skills, memory system, and advanced configuration. +- **Octos Matrix Appservice Guide:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — the original guide this document is based on, with additional context. +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo homeserver documentation. +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix client, build instructions, and feature tracker. +- **Matrix Appservice Spec:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — the Matrix protocol specification for application services. + +--- + +*This guide covers the deployment as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/examples/.env.example b/docs/examples/.env.example new file mode 100644 index 000000000..4f67cecd5 --- /dev/null +++ b/docs/examples/.env.example @@ -0,0 +1,20 @@ +# ============================================================ +# Robrix + Palpo + Octos — Environment Variables +# ============================================================ +# Copy this file to .env and fill in the required values. +# +# cp .env.example .env +# +# Only DEEPSEEK_API_KEY is required. Everything else has +# sensible defaults for local development. +# ============================================================ + +# [REQUIRED] Your LLM API key +# Get one at https://platform.deepseek.com/ +DEEPSEEK_API_KEY=your-api-key-here + +# [Optional] PostgreSQL password for Palpo's database +DB_PASSWORD=palpo_dev_password + +# [Optional] Log level for Octos +RUST_LOG=octos=debug,info diff --git a/docs/examples/.gitignore b/docs/examples/.gitignore new file mode 100644 index 000000000..cbfd38077 --- /dev/null +++ b/docs/examples/.gitignore @@ -0,0 +1,8 @@ +# Runtime data (created by docker compose) +data/ + +# Source repos cloned by setup.sh +repos/ + +# User's local env file (contains API keys) +.env diff --git a/docs/examples/appservices/octos-registration.yaml b/docs/examples/appservices/octos-registration.yaml new file mode 100644 index 000000000..e99b83843 --- /dev/null +++ b/docs/examples/appservices/octos-registration.yaml @@ -0,0 +1,30 @@ +# Matrix Appservice Registration — Octos +# ---------------------------------------- +# This file tells Palpo how to communicate with Octos. +# Palpo auto-loads all .yaml files from the appservice directory on startup. +# +# IMPORTANT: as_token and hs_token MUST match the values in config/botfather.json. +# For production, regenerate tokens with: openssl rand -hex 32 + +id: octos-matrix-appservice + +# URL where Palpo pushes events to Octos. +# Uses Docker service name (not localhost) because both run in the same Docker network. +url: "http://octos:8009" + +# Tokens for mutual authentication between Palpo and Octos. +# These are pre-filled for local development. Replace for production! +as_token: "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4" +hs_token: "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] diff --git a/docs/examples/compose.yml b/docs/examples/compose.yml new file mode 100644 index 000000000..ed2154df9 --- /dev/null +++ b/docs/examples/compose.yml @@ -0,0 +1,96 @@ +# ============================================================ +# Robrix + Palpo + Octos — Docker Compose +# ============================================================ +# Starts Palpo (Matrix homeserver), Octos (AI bot), and PostgreSQL. +# Robrix runs natively on your machine — it is NOT in this file. +# +# Before first run, clone the source repos: +# git clone https://github.com/palpo-im/palpo.git repos/palpo +# git clone https://github.com/octos-org/octos.git repos/octos +# +# Then: +# cp .env.example .env # fill in your API key +# docker compose up -d # start all services +# docker compose logs -f # watch logs +# docker compose down # stop all services +# ============================================================ + +services: + # ---------- PostgreSQL (Palpo's database) ---------- + palpo_postgres: + image: postgres:17 + restart: always + volumes: + - ./data/pgsql:/var/lib/postgresql/data + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD:-palpo_dev_password} + POSTGRES_USER: palpo + POSTGRES_DB: palpo + healthcheck: + test: ["CMD-SHELL", "pg_isready -U palpo"] + interval: 5s + timeout: 5s + retries: 5 + networks: + - internal + + # ---------- Palpo (Matrix homeserver) ---------- + # Built from source to support all architectures (x86_64, ARM64, etc.). + palpo: + build: + context: ./repos/palpo + dockerfile_inline: | + FROM rust:bookworm AS builder + WORKDIR /work + RUN apt-get update && apt-get install -y --no-install-recommends libclang-dev libpq-dev cmake && rm -rf /var/lib/apt/lists/* + COPY . . + RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/work/target cargo build --release && cp target/release/palpo /usr/local/bin/palpo + FROM debian:bookworm-slim + RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl libpq5 && rm -rf /var/lib/apt/lists/* && mkdir -p /var/palpo/media + COPY --from=builder /usr/local/bin/palpo /usr/local/bin/palpo + ENV PALPO_CONFIG=/var/palpo/palpo.toml + EXPOSE 8008 + HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=15s CMD curl -sf http://localhost:8008/_matrix/client/versions || exit 1 + CMD ["/usr/local/bin/palpo"] + restart: unless-stopped + ports: + - "8128:8008" # Client-Server API (Robrix connects here) + environment: + PALPO_CONFIG: "/var/palpo/palpo.toml" + volumes: + - ./palpo.toml:/var/palpo/palpo.toml:ro + - ./appservices:/var/palpo/appservices:ro + - ./data/media:/var/palpo/media + - ./static:/var/palpo/static:ro + depends_on: + palpo_postgres: + condition: service_healthy + networks: + - internal + + # ---------- Octos (AI Bot Appservice) ---------- + # Built from source with the "matrix" feature enabled. + octos: + build: + context: ./repos/octos + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8009:8009" # Appservice listener (receives events from Palpo) + - "8010:8080" # Octos dashboard / admin API + environment: + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + RUST_LOG: ${RUST_LOG:-octos=debug,info} + volumes: + - ./data/octos:/root/.octos + - ./config/botfather.json:/root/.octos/profiles/botfather.json:ro + - ./config/octos.json:/config/octos.json:ro + command: ["serve", "--host", "0.0.0.0", "--port", "8080", "--config", "/config/octos.json"] + depends_on: + - palpo + networks: + - internal + +networks: + internal: + attachable: true diff --git a/docs/examples/config/botfather.json b/docs/examples/config/botfather.json new file mode 100644 index 000000000..2120139ea --- /dev/null +++ b/docs/examples/config/botfather.json @@ -0,0 +1,29 @@ +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4", + "hs_token": "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} diff --git a/docs/examples/config/octos.json b/docs/examples/config/octos.json new file mode 100644 index 000000000..f61c5cbb9 --- /dev/null +++ b/docs/examples/config/octos.json @@ -0,0 +1,5 @@ +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} diff --git a/docs/examples/palpo.toml b/docs/examples/palpo.toml new file mode 100644 index 000000000..089a83743 --- /dev/null +++ b/docs/examples/palpo.toml @@ -0,0 +1,44 @@ +# Palpo — Matrix Homeserver Configuration +# ----------------------------------------- +# Palpo is the Matrix homeserver that sits between Robrix (client) +# and Octos (AI bot appservice). +# +# For full configuration reference, see: +# https://github.com/palpo-im/palpo + +server_name = "127.0.0.1:8128" + +# Allow new users to register from Robrix. +# Set to false in production if you want invite-only registration. +allow_registration = true +# Required when allow_registration = true. This flag name is intentionally +# long to remind you of the security implications of open registration. +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true + +# Enable the admin room for server management. +enable_admin_room = true + +# Palpo auto-loads all .yaml files from this directory on startup. +# This is where the Octos appservice registration lives. +appservice_registration_dir = "/var/palpo/appservices" + +# ---------- Listeners ---------- +# HTTP listener (Client-Server API — Robrix connects here) +[[listeners]] +address = "0.0.0.0:8008" + +# ---------- Logging ---------- +[logger] +format = "pretty" # Use "pretty" for development, "json" for production + +# ---------- Database ---------- +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +# ---------- Well-Known ---------- +# Used by clients and other servers for discovery. +# Must match the externally-reachable addresses. +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" diff --git a/docs/examples/setup.sh b/docs/examples/setup.sh new file mode 100755 index 000000000..1bd80a8c5 --- /dev/null +++ b/docs/examples/setup.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# ============================================================ +# Robrix + Palpo + Octos — One-time Setup +# ============================================================ +# Run this once before "docker compose up -d". +# It clones the required source repos and prepares .env. +# ============================================================ +set -e + +cd "$(dirname "$0")" + +echo "==> Cloning Palpo (Matrix homeserver)..." +if [ -d repos/palpo ]; then + echo " repos/palpo already exists, skipping." +else + git clone --depth 1 https://github.com/palpo-im/palpo.git repos/palpo +fi + +echo "==> Cloning Octos (AI bot)..." +if [ -d repos/octos ]; then + echo " repos/octos already exists, skipping." +else + git clone --depth 1 --recurse-submodules=no https://github.com/octos-org/octos.git repos/octos +fi + +if [ ! -f .env ]; then + cp .env.example .env + echo "==> Created .env from .env.example." + echo " IMPORTANT: Edit .env and set your DEEPSEEK_API_KEY before starting." +else + echo "==> .env already exists, skipping." +fi + +echo "" +echo "Setup complete! Next steps:" +echo " 1. Edit .env and set DEEPSEEK_API_KEY" +echo " 2. docker compose up -d" diff --git a/docs/examples/static/index.html b/docs/examples/static/index.html new file mode 100644 index 000000000..a71080f17 --- /dev/null +++ b/docs/examples/static/index.html @@ -0,0 +1,9 @@ + + +Palpo Matrix Homeserver + +

Palpo Matrix Homeserver

+

This is a Palpo homeserver running with Octos AI bot support.

+

Connect with Robrix or any Matrix client.

+ + diff --git a/docs/images/register-account.png b/docs/images/register-account.png new file mode 100644 index 0000000000000000000000000000000000000000..1b7d0af56710fab1cb8059503271050504d4b93f GIT binary patch literal 61809 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%0DVO_C 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 literal 0 HcmV?d00001 From 322a15530c890d3a611c7e7132880f3647480161 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Thu, 2 Apr 2026 17:13:00 +0800 Subject: [PATCH 057/283] fix missing account action --- src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index d27e82db1..292b1138a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,7 +14,7 @@ use crate::{ 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, 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::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } From 7d17415fa8fc95e7e23756bc45b50689ef4b9bfa Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:11:33 +0800 Subject: [PATCH 058/283] Skip app state restore after explicit logout --- src/logout/logout_state_machine.rs | 20 +++++++++++++- src/persistence/app_state.rs | 42 ++++++++++++++++++++++++++++++ src/sliding_sync.rs | 13 ++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/logout/logout_state_machine.rs b/src/logout/logout_state_machine.rs index a8776377b..80f347bfb 100644 --- a/src/logout/logout_state_machine.rs +++ b/src/logout/logout_state_machine.rs @@ -90,7 +90,7 @@ use anyhow::{anyhow, Result}; use makepad_widgets::{Cx, log}; use crate::home::navigation_tab_bar::NavigationBarAction; -use crate::persistence::delete_latest_user_id; +use crate::persistence::{delete_latest_user_id, skip_app_state_restore_once}; use crate::sliding_sync::clear_app_state; use crate::{ home::main_desktop_ui::MainDesktopUiAction, @@ -324,6 +324,12 @@ impl LogoutStateMachine { "Point of no return reached".to_string(), 50 ).await?; + + if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { + if let Err(e) = skip_app_state_restore_once(&user_id).await { + log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); + } + } // We delete latest_user_id after reaching LOGOUT_POINT_OF_NO_RETURN: // 1. To prevent auto-login with invalid session on next start @@ -343,6 +349,12 @@ impl LogoutStateMachine { "Token already invalidated".to_string(), 50 ).await?; + + if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { + if let Err(e) = skip_app_state_restore_once(&user_id).await { + log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); + } + } // Same delete operation as in the success case above if let Err(e) = delete_latest_user_id().await { @@ -358,6 +370,12 @@ impl LogoutStateMachine { 50 ).await?; + if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { + if let Err(e) = skip_app_state_restore_once(&user_id).await { + log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); + } + } + // Same delete operation as in the success case above if let Err(e) = delete_latest_user_id().await { log!("Warning: Failed to delete latest user ID: {}", e); diff --git a/src/persistence/app_state.rs b/src/persistence/app_state.rs index 6bc88714f..4b1b0ebf1 100644 --- a/src/persistence/app_state.rs +++ b/src/persistence/app_state.rs @@ -7,6 +7,7 @@ use crate::{app::AppState, app_data_dir, persistence::persistent_state_dir}; const LATEST_APP_STATE_FILE_NAME: &str = "latest_app_state.json"; +const SKIP_APP_STATE_RESTORE_ONCE_FILE_NAME: &str = "skip_app_state_restore_once"; const WINDOW_GEOM_STATE_FILE_NAME: &str = "window_geom_state.json"; @@ -38,6 +39,26 @@ pub fn save_app_state( Ok(()) } +/// Marks that the next login for this user should skip automatic app-state restore once. +pub async fn skip_app_state_restore_once(user_id: &UserId) -> anyhow::Result<()> { + let marker_path = persistent_state_dir(user_id).join(SKIP_APP_STATE_RESTORE_ONCE_FILE_NAME); + if let Some(parent) = marker_path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + tokio::fs::write(marker_path, b"1").await?; + Ok(()) +} + +/// Consumes the one-shot "skip automatic restore" marker for the given user, if present. +pub async fn take_skip_app_state_restore_once(user_id: &UserId) -> anyhow::Result { + let marker_path = persistent_state_dir(user_id).join(SKIP_APP_STATE_RESTORE_ONCE_FILE_NAME); + match tokio::fs::remove_file(marker_path).await { + Ok(()) => Ok(true), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(e) => Err(e.into()), + } +} + /// Save the current state of the given window's geometry to persistent storage. pub fn save_window_state(window_ref: WindowRef, cx: &Cx) -> anyhow::Result<()> { let inner_size = window_ref.get_inner_size(cx); @@ -114,3 +135,24 @@ pub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result<( ); Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn skip_restore_marker_is_consumed_once() { + let user_id = UserId::parse("@robrix-test-skip-restore:example.invalid") + .unwrap() + .to_owned(); + + let _ = tokio::fs::remove_dir_all(persistent_state_dir(&user_id)).await; + + skip_app_state_restore_once(&user_id).await.unwrap(); + + assert!(take_skip_app_state_restore_once(&user_id).await.unwrap()); + assert!(!take_skip_app_state_restore_once(&user_id).await.unwrap()); + + let _ = tokio::fs::remove_dir_all(persistent_state_dir(&user_id)).await; + } +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 4b488edcf..0245ff79e 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -45,7 +45,7 @@ use crate::{ account_manager::{self, Account}, app::{AppStateAction, RoomFilterRemoteSearchAction}, app_data_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{ add_room::{CreatableSpacesAction, CreateRoomAction, CreateRoomContext, KnockResultAction}, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, build_room_search_text, 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::{ + }, 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, take_skip_app_state_restore_once}, profile::{ user_profile::UserProfile, user_profile_cache::{UserProfileUpdate, enqueue_user_profile_update}, }, room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{ @@ -4401,6 +4401,17 @@ fn handle_ignore_user_list_subscriber(client: Client) { /// 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 take_skip_app_state_restore_once(&user_id).await { + Ok(true) => { + log!("Skipping automatic app state restore once for {user_id} after explicit logout."); + return; + } + Ok(false) => {} + Err(e) => { + warning!("Failed to check skip-restore marker for {user_id}: {e}"); + } + } + match load_app_state(&user_id).await { Ok(app_state) => { if !app_state.saved_dock_state_home.open_rooms.is_empty() From 2ab1b5fa4a65d4cb01868259987adb3eca72ce4b Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:15:04 +0800 Subject: [PATCH 059/283] Remove skip-restore marker test --- src/persistence/app_state.rs | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/persistence/app_state.rs b/src/persistence/app_state.rs index 4b1b0ebf1..7201ad033 100644 --- a/src/persistence/app_state.rs +++ b/src/persistence/app_state.rs @@ -135,24 +135,3 @@ pub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result<( ); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn skip_restore_marker_is_consumed_once() { - let user_id = UserId::parse("@robrix-test-skip-restore:example.invalid") - .unwrap() - .to_owned(); - - let _ = tokio::fs::remove_dir_all(persistent_state_dir(&user_id)).await; - - skip_app_state_restore_once(&user_id).await.unwrap(); - - assert!(take_skip_app_state_restore_once(&user_id).await.unwrap()); - assert!(!take_skip_app_state_restore_once(&user_id).await.unwrap()); - - let _ = tokio::fs::remove_dir_all(persistent_state_dir(&user_id)).await; - } -} From 16291e2ceb9ba253affd579bfb90b888f15f254c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Fri, 3 Apr 2026 13:17:54 +0800 Subject: [PATCH 060/283] Add room threads pane and thread pagination support --- src/home/room_screen.rs | 640 ++++++++++++++++++++++++++++++++++++- src/room/room_input_bar.rs | 94 ++++-- src/sliding_sync.rs | 162 +++++++++- 3 files changed, 869 insertions(+), 27 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index b773f8ecd..a92eb35f0 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -34,7 +34,7 @@ use crate::{ shared::{ avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, - sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} + sliding_sync::{BackwardsPaginateUntilEventRequest, FetchedRoomThread, MatrixRequest, PaginationDirection, RoomThreadsAction, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -744,6 +744,218 @@ script_mod! { } } + mod.widgets.ThreadsPaneEntry = #(ThreadsPaneEntry::register_widget(vm)) { + ..mod.widgets.RoundedView + + width: Fill + height: Fit + flow: Down + spacing: 5 + padding: Inset{top: 12, right: 12, bottom: 12, left: 12} + margin: Inset{left: 12, right: 12, top: 6, bottom: 0} + cursor: MouseCursor.Hand + + show_bg: true + draw_bg +: { + color: #F8FAFD + border_radius: 4.0 + border_size: 1.0 + border_color: #D8E0EA + } + + title_row := View { + width: Fill + height: Fit + flow: Right + spacing: 8 + + title := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 10.8 } + color: #1F1F1F + } + text: "" + } + + time := Label { + width: Fit + height: Fit + draw_text +: { + text_style: TIMESTAMP_TEXT_STYLE { font_size: 7.5 } + color: (TIMESTAMP_TEXT_COLOR) + } + text: "" + } + } + + subtitle := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 9.8 } + color: #7B7B7B + } + text: "" + } + + preview := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.0 } + color: (COLOR_TEXT) + } + text: "" + } + } + + mod.widgets.ThreadsSlidingPane = #(ThreadsSlidingPane::register_widget(vm)) { + visible: false, + flow: Overlay, + width: Fill, + height: Fill, + align: Align{x: 1.0, y: 0} + + bg_view := SolidView { + width: Fill + height: Fill + visible: false, + show_bg: true + draw_bg.color: #000000BB + } + + main_content := SolidView { + width: 320, + height: Fill + flow: Down, + align: Align{x: 1.0} + + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) + + header := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + padding: Inset{top: 12, right: 10, bottom: 12, left: 15} + + title := Label { + width: Fit + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 12.5 } + color: #000 + } + text: "Threads" + } + + spacer := View { + width: Fill + height: Fit + } + + close_button := RobrixNeutralIconButton { + width: Fit, + height: Fit, + spacing: 0, + padding: 15, + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14} + text: "" + } + } + + room_name := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + padding: Inset{left: 15, right: 15, bottom: 10} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + color: #6E6E6E + } + text: "" + } + + loading_indicator := View { + visible: false + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + spacing: 8 + padding: Inset{left: 15, right: 15, top: 6, bottom: 10} + + spinner := LoadingSpinner { + width: 18 + height: 18 + } + + loading_label := Label { + width: Fit + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + color: #7B7B7B + } + text: "Loading threads..." + } + } + + empty_state := Label { + visible: false + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + padding: Inset{left: 15, right: 15, top: 20, bottom: 20} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + color: #7B7B7B + } + text: "No threads yet." + } + + threads_list := PortalList { + width: Fill + height: Fill + flow: Down + max_pull_down: 0.0 + + ThreadEntry := mod.widgets.ThreadsPaneEntry {} + } + } + + slide: 1.0, + + animator: Animator { + panel: { + default: @hide + show: AnimatorState{ + redraw: true, + from: {all: Forward {duration: 0.5}} + ease: Ease.ExpDecay {d1: 0.80, d2: 0.97} + apply: { + slide: 0.0 + } + } + hide: AnimatorState{ + redraw: true, + from: {all: Forward {duration: 0.5}} + ease: Ease.ExpDecay {d1: 0.80, d2: 0.97} + apply: { + slide: 1.0 + } + } + } + } + } + mod.widgets.AppServicePanel = #(AppServicePanel::register_widget(vm)) { width: Fill height: Fit @@ -1013,6 +1225,8 @@ script_mod! { // (on top of all other views that are always visible). user_profile_sliding_pane := mod.widgets.UserProfileSlidingPane { } + threads_sliding_pane := mod.widgets.ThreadsSlidingPane { } + // The loading pane appears while the user is waiting for something in the room screen // to finish loading, e.g., when loading an older replied-to message. loading_pane := LoadingPane { } @@ -1052,6 +1266,258 @@ script_mod! { } } +#[derive(Clone, Default, Debug)] +pub enum ThreadsPaneAction { + OpenThread(OwnedEventId), + LoadMoreRequested, + #[default] + None, +} + +impl ActionDefaultRef for ThreadsPaneAction { + fn default_ref() -> &'static Self { + static DEFAULT: ThreadsPaneAction = ThreadsPaneAction::None; + &DEFAULT + } +} + +#[derive(Clone, Debug)] +struct ThreadsPaneEntryInfo { + thread_root_event_id: OwnedEventId, + title: String, + subtitle: String, + time: String, + preview: String, +} + +#[derive(Clone, Debug)] +struct ThreadsPaneInfo { + room_name: String, + entries: Vec, + status_text: String, + show_entries: bool, + loading_text: String, + show_loading: bool, +} + +#[derive(Default)] +struct ThreadsPaneState { + room_id: Option, + entries: Vec, + prev_batch_token: Option, + is_loading: bool, + initialized: bool, + status_text: String, +} + +#[derive(Script, ScriptHook, Widget)] +pub struct ThreadsPaneEntry { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + #[rust] thread_root_event_id: Option, +} + +impl Widget for ThreadsPaneEntry { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + let Some(thread_root_event_id) = self.thread_root_event_id.clone() else { return }; + match event.hits(cx, self.view.area()) { + Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { + cx.widget_action( + self.widget_uid(), + ThreadsPaneAction::OpenThread(thread_root_event_id), + ); + } + _ => {} + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl ThreadsPaneEntry { + fn set_entry(&mut self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { + self.thread_root_event_id = Some(entry.thread_root_event_id.clone()); + self.label(cx, ids!(title)).set_text(cx, &entry.title); + self.label(cx, ids!(time)).set_text(cx, &entry.time); + self.label(cx, ids!(subtitle)).set_text(cx, &entry.subtitle); + self.label(cx, ids!(preview)).set_text(cx, &entry.preview); + } +} + +impl ThreadsPaneEntryRef { + fn set_entry(&self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_entry(cx, entry); + } +} + +#[derive(Script, ScriptHook, Widget, Animator)] +pub struct ThreadsSlidingPane { + #[source] source: ScriptObjectRef, + #[deref] view: View, + #[apply_default] animator: Animator, + #[live] slide: f32, + + #[rust] info: Option, + #[rust] is_animating_out: bool, +} + +impl Widget for ThreadsSlidingPane { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + if !self.visible { return; } + + let animator_action = self.animator_handle_event(cx, event); + if animator_action.must_redraw() { + self.redraw(cx); + } + + if self.is_animating_out && !self.animator.is_track_animating(id!(panel)) { + self.visible = false; + self.is_animating_out = false; + cx.revert_key_focus(); + self.view(cx, ids!(bg_view)).set_visible(cx, false); + self.redraw(cx); + return; + } + + let area = self.view.area(); + let close_pane = { + matches!( + event, + Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) + ) + || event.back_pressed() + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + Hit::FingerUp(fue) if fue.is_over => { + fue.mouse_button().is_some_and(|b| b.is_back()) + || !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + } + _ => false, + } + }; + if close_pane { + self.hide(cx); + } + + if let Event::Actions(actions) = event { + let threads_list = self.portal_list(cx, ids!(threads_list)); + if threads_list.scrolled(actions) + && threads_list.first_id() == 0 + && threads_list.scroll_position() >= -0.5 + { + cx.widget_action( + self.widget_uid(), + ThreadsPaneAction::LoadMoreRequested, + ); + } + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let Some(info) = self.info.as_ref() else { + self.visible = false; + return self.view.draw_walk(cx, scope, walk); + }; + + let panel_width = 320.0; + let right_margin = -(self.slide * panel_width); + let mut main_content = self.view(cx, ids!(main_content)); + script_apply_eval!(cx, main_content, { + margin.right: #(right_margin) + }); + let bg_alpha = (1.0 - self.slide) * 0.733; + let bg_color = vec4(0.0, 0.0, 0.0, bg_alpha); + let mut bg_view = self.view(cx, ids!(bg_view)); + script_apply_eval!(cx, bg_view, { + draw_bg +: { color: #(bg_color) } + }); + + self.label(cx, ids!(room_name)).set_text(cx, &info.room_name); + self.label(cx, ids!(loading_label)).set_text(cx, &info.loading_text); + self.view(cx, ids!(loading_indicator)).set_visible(cx, info.show_loading); + self.label(cx, ids!(empty_state)).set_text(cx, &info.status_text); + self.view(cx, ids!(empty_state)).set_visible(cx, !info.show_entries && !info.show_loading); + self.view(cx, ids!(threads_list)).set_visible(cx, info.show_entries); + + while let Some(widget) = self.view.draw_walk(cx, scope, walk).step() { + let portal_list_ref = widget.as_portal_list(); + let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + + list.set_item_range(cx, 0, info.entries.len()); + while let Some(item_id) = list.next_visible_item(cx) { + let Some(entry) = info.entries.get(item_id) else { continue }; + let item = list.item(cx, item_id, id!(ThreadEntry)); + item.as_threads_pane_entry().set_entry(cx, entry); + item.draw_all(cx, &mut Scope::empty()); + } + } + DrawStep::done() + } +} + +impl ThreadsSlidingPane { + pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { + self.visible + } + + fn set_info(&mut self, _cx: &mut Cx, info: ThreadsPaneInfo) { + self.info = Some(info); + } + + pub fn show(&mut self, cx: &mut Cx) { + self.visible = true; + self.is_animating_out = false; + cx.set_key_focus(self.view.area()); + self.animator_play(cx, ids!(panel.show)); + self.view(cx, ids!(bg_view)).set_visible(cx, true); + self.view.button(cx, ids!(close_button)).reset_hover(cx); + self.redraw(cx); + } + + pub fn hide(&mut self, cx: &mut Cx) { + if !self.visible { + return; + } + self.is_animating_out = true; + self.animator_play(cx, ids!(panel.hide)); + self.redraw(cx); + } +} + +impl ThreadsSlidingPaneRef { + pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { + let Some(inner) = self.borrow() else { return false }; + inner.is_currently_shown(cx) + } + + fn set_info(&self, cx: &mut Cx, info: ThreadsPaneInfo) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_info(cx, info); + } + + pub fn show(&self, cx: &mut Cx) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.show(cx); + } + + pub fn hide(&self, cx: &mut Cx) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.hide(cx); + } +} + /// The main widget that displays a single Matrix room. #[derive(Script, Widget)] pub struct RoomScreen { @@ -1079,6 +1545,7 @@ pub struct RoomScreen { streaming_timeout_timer: Timer, /// Whether the in-room app service quick actions card is currently visible. #[rust] show_app_service_actions: bool, + #[rust] threads_pane_state: ThreadsPaneState, } impl Drop for RoomScreen { @@ -1111,6 +1578,7 @@ impl Widget for RoomScreen { let room_screen_widget_uid = self.widget_uid(); let portal_list = self.portal_list(cx, ids!(timeline.list)); let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); + let threads_sliding_pane = self.threads_sliding_pane(cx, ids!(threads_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); // Streaming animation frame handler @@ -1354,6 +1822,40 @@ impl Widget for RoomScreen { } } + match action.as_widget_action().cast_ref() { + ThreadsPaneAction::OpenThread(thread_root_event_id) => { + let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { continue }; + threads_sliding_pane.hide(cx); + cx.widget_action( + room_screen_widget_uid, + RoomsListAction::Selected(SelectedRoom::Thread { + room_name_id, + thread_root_event_id: thread_root_event_id.clone(), + }), + ); + } + ThreadsPaneAction::LoadMoreRequested => { + self.request_more_threads(cx, true); + } + ThreadsPaneAction::None => {} + } + + if let Some(RoomThreadsAction::Loaded { room_id, from, threads, prev_batch_token }) = action.downcast_ref() { + if self.threads_pane_state.room_id.as_ref().is_some_and(|current| current == room_id) { + self.on_threads_loaded( + cx, + from.as_ref(), + threads, + prev_batch_token.clone(), + ); + } + } + if let Some(RoomThreadsAction::Failed { room_id, from: _, error }) = action.downcast_ref() { + if self.threads_pane_state.room_id.as_ref().is_some_and(|current| current == room_id) { + self.on_threads_failed(cx, error); + } + } + // Handle the highlight animation for a message. let Some(tl) = self.tl_state.as_mut() else { continue }; if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { @@ -1418,6 +1920,9 @@ impl Widget for RoomScreen { } self.process_timeline_updates(cx, &portal_list, scope.data.get::()); + if threads_sliding_pane.is_currently_shown(cx) { + self.refresh_threads_pane(cx); + } // Ideally we would do this elsewhere on the main thread, because it's not room-specific, // but it doesn't hurt to do it here. @@ -1441,6 +1946,12 @@ impl Widget for RoomScreen { loading_pane.handle_event(cx, event, scope); } } + else if threads_sliding_pane.is_currently_shown(cx) { + is_pane_shown = true; + if is_interactive_hit { + threads_sliding_pane.handle_event(cx, event, scope); + } + } else if user_profile_sliding_pane.is_currently_shown(cx) { is_pane_shown = true; if is_interactive_hit { @@ -2036,7 +2547,8 @@ impl Widget for RoomScreen { // If the list is not filling the viewport, we need to back paginate the timeline // until we have enough events items to fill the viewport. - if !tl_state.fully_paginated + if tl_state.kind.thread_root_event_id().is_none() + && !tl_state.fully_paginated && !tl_state.backwards_pagination_in_flight && !list.is_filling_viewport() { @@ -3177,6 +3689,9 @@ impl RoomScreen { }), ); } + MessageAction::ShowThreadsPane => { + self.show_threads_pane(cx); + } MessageAction::Redact { details, reason } => { let Some(tl) = self.tl_state.as_ref() else { return }; let timeline_event_id = details.timeline_event_id.clone(); @@ -3312,6 +3827,123 @@ impl RoomScreen { self.redraw(cx); } + fn show_threads_pane(&mut self, cx: &mut Cx) { + self.ensure_threads_state_for_current_room(); + if !self.threads_pane_state.initialized && !self.threads_pane_state.is_loading { + self.request_more_threads(cx, false); + } + self.refresh_threads_pane(cx); + self.threads_sliding_pane(cx, ids!(threads_sliding_pane)).show(cx); + self.redraw(cx); + } + + fn refresh_threads_pane(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.as_ref() else { return }; + self.threads_sliding_pane(cx, ids!(threads_sliding_pane)).set_info( + cx, + ThreadsPaneInfo { + room_name: room_name_id.to_string(), + entries: self.threads_pane_state.entries.iter() + .map(|entry| ThreadsPaneEntryInfo { + thread_root_event_id: entry.thread_root_event_id.clone(), + title: entry.title.clone(), + subtitle: match entry.reply_count { + 1 => String::from("1 reply"), + n => format!("{n} replies"), + }, + time: utils::relative_format(entry.timestamp) + .unwrap_or_else(|| String::from("")), + preview: entry.latest_reply_preview.clone().unwrap_or_else(|| String::from("Tap to open thread")), + }) + .collect(), + status_text: self.threads_pane_state.status_text.clone(), + show_entries: !self.threads_pane_state.entries.is_empty(), + loading_text: if self.threads_pane_state.entries.is_empty() { + String::from("Loading threads...") + } else { + String::from("Loading more threads...") + }, + show_loading: self.threads_pane_state.is_loading, + }, + ); + } + + fn hide_threads_pane(&mut self, cx: &mut Cx) { + self.threads_sliding_pane(cx, ids!(threads_sliding_pane)).hide(cx); + } + + fn ensure_threads_state_for_current_room(&mut self) { + let Some(room_id) = self.room_id().cloned() else { return }; + if self.threads_pane_state.room_id.as_ref().is_some_and(|current| current == &room_id) { + return; + } + self.threads_pane_state = ThreadsPaneState { + room_id: Some(room_id), + status_text: String::from("Loading threads..."), + ..Default::default() + }; + } + + fn request_more_threads(&mut self, _cx: &mut Cx, load_more: bool) { + self.ensure_threads_state_for_current_room(); + let Some(room_id) = self.threads_pane_state.room_id.clone() else { return }; + if self.threads_pane_state.is_loading { + return; + } + let from = if load_more { + let Some(from) = self.threads_pane_state.prev_batch_token.clone() else { return }; + Some(from) + } else { + None + }; + self.threads_pane_state.is_loading = true; + if !self.threads_pane_state.initialized { + self.threads_pane_state.status_text = String::from("Loading threads..."); + } + submit_async_request(MatrixRequest::ListRoomThreads { + room_id, + from, + }); + } + + fn on_threads_loaded( + &mut self, + cx: &mut Cx, + _from: Option<&String>, + threads: &[FetchedRoomThread], + prev_batch_token: Option, + ) { + self.threads_pane_state.is_loading = false; + self.threads_pane_state.initialized = true; + self.threads_pane_state.prev_batch_token = prev_batch_token; + self.threads_pane_state.entries.extend_from_slice(threads); + self.threads_pane_state.entries.sort_by_key(|entry| u64::from(entry.timestamp.0)); + self.threads_pane_state.entries.dedup_by(|a, b| a.thread_root_event_id == b.thread_root_event_id); + self.threads_pane_state.status_text = if self.threads_pane_state.entries.is_empty() { + String::from("No threads yet.") + } else { + String::new() + }; + self.refresh_threads_pane(cx); + self.redraw(cx); + } + + fn on_threads_failed(&mut self, cx: &mut Cx, error: &str) { + self.threads_pane_state.is_loading = false; + self.threads_pane_state.initialized = true; + if self.threads_pane_state.entries.is_empty() { + self.threads_pane_state.status_text = format!("Failed to load threads.\n\nError: {error}"); + } else { + enqueue_popup_notification( + format!("Failed to load more threads.\n\nError: {error}"), + PopupKind::Error, + Some(5.0), + ); + } + self.refresh_threads_pane(cx); + self.redraw(cx); + } + /// Invoke this when this timeline is being shown, /// e.g., when the user navigates to this timeline. fn show_timeline(&mut self, cx: &mut Cx) { @@ -3618,6 +4250,8 @@ impl RoomScreen { self.hide_timeline(); self.reset_app_service_ui(cx); + self.hide_threads_pane(cx); + self.threads_pane_state = Default::default(); // Reset the the state of the inner loading pane. self.loading_pane(cx, ids!(loading_pane)).take_state(); @@ -4114,6 +4748,7 @@ struct FetchedThreadSummary { num_replies: u32, latest_reply_preview_text: Option, } + impl ItemDrawnStatus { /// Returns a new `ItemDrawnStatus` with both `profile_drawn` and `content_drawn` set to `false`. const fn new() -> Self { @@ -5764,6 +6399,7 @@ pub enum MessageAction { ActionBarClose, /// The user requested toggling the in-room app service quick actions card. ToggleAppServiceActions, + ShowThreadsPane, #[default] None, } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 8c6a6fb64..569ed3241 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -29,6 +29,7 @@ script_mod! { mod.widgets.ICO_LOCATION_PERSON = crate_resource("self://resources/icons/location-person.svg") mod.widgets.ICO_MENU = crate_resource("self://resources/icons/menu.svg") + mod.widgets.ICO_THREADS = crate_resource("self://resources/icons/double_chat.svg") mod.widgets.RoomEmojiButton = mod.widgets.RobrixIconButton { spacing: 0 @@ -100,32 +101,67 @@ script_mod! { padding: 6, spacing: 4 - location_card_button := RobrixIconButton { + more_actions_popup := View { visible: false - width: 230 + width: Fill + height: Fit + flow: Right{wrap: true} + spacing: 6 align: Align{x: 0.0, y: 0.5} - margin: Inset{top: 1, bottom: 1} - padding: Inset{left: 10, right: 10, top: 8, bottom: 8} - spacing: 8 - draw_icon +: { - svg: (mod.widgets.ICO_LOCATION_PERSON) - color: (COLOR_ACTIVE_PRIMARY_DARKER) - }, - draw_bg +: { - color: (COLOR_BG_PREVIEW) - color_hover: #E0E8F0 - color_down: #D0D8E8 - border_size: 1.0 - border_color: (COLOR_SECONDARY) + + location_card_button := RobrixIconButton { + width: Fit + align: Align{x: 0.0, y: 0.5} + margin: Inset{top: 1, bottom: 1} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 8 + draw_icon +: { + svg: (mod.widgets.ICO_LOCATION_PERSON) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + icon_walk: Walk{width: 20, height: 20} + text: "location", } - draw_text +: { - color: (COLOR_TEXT) - color_hover: (COLOR_TEXT) - color_down: (COLOR_TEXT) - text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + + threads_card_button := RobrixIconButton { + width: Fit + align: Align{x: 0.0, y: 0.5} + margin: Inset{top: 1, bottom: 1} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 8 + draw_icon +: { + svg: (mod.widgets.ICO_THREADS) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + icon_walk: Walk{width: 20, height: 20} + text: "threads", } - icon_walk: Walk{width: 20, height: 20} - text: "Share your current location", } emoji_picker_popup := View { @@ -347,7 +383,7 @@ impl RoomInputBar { // Handle the more actions button being clicked. if self.button(cx, ids!(more_actions_button)).clicked(actions) { self.is_location_card_expanded = !self.is_location_card_expanded; - self.button(cx, ids!(location_card_button)).set_visible(cx, self.is_location_card_expanded); + self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, self.is_location_card_expanded); self.redraw(cx); } @@ -396,6 +432,8 @@ impl RoomInputBar { // Handle the location card being clicked. if self.button(cx, ids!(location_card_button)).clicked(actions) { log!("Location card clicked; requesting current location..."); + self.is_location_card_expanded = false; + self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); if let Err(_e) = init_location_subscriber(cx) { error!("Failed to initialize location subscriber"); enqueue_popup_notification( @@ -408,6 +446,14 @@ impl RoomInputBar { self.redraw(cx); } + if self.button(cx, ids!(threads_card_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::ShowThreadsPane, + ); + self.redraw(cx); + } + // Handle the send location button being clicked. if self.button(cx, ids!(location_preview.send_location_button)).clicked(actions) { let location_preview = self.location_preview(cx, ids!(location_preview)); @@ -885,7 +931,7 @@ impl RoomInputBarRef { .is_empty(); inner.enable_send_message_button(cx, !is_text_input_empty); inner.is_location_card_expanded = false; - inner.button(cx, ids!(location_card_button)).set_visible(cx, false); + inner.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); inner.is_emoji_picker_expanded = false; inner.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 4b488edcf..796f02c71 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -9,7 +9,7 @@ use makepad_widgets::{error, log, warning, Cx, SignalToUI}; use mime::{IMAGE_JPEG, IMAGE_PNG}; 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::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, ListThreadsOptions, RelationsOptions, RoomMember}, ruma::{ api::{Direction, client::{ account::register::v3::Request as RegistrationRequest, room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, @@ -295,6 +295,11 @@ fn is_invalid_batch_token_timeline_error(error: &matrix_sdk_ui::timeline::Error) || error_text.contains("must start with 's' or 't'") } +fn is_thread_unknown_parent_timeline_error(error: &matrix_sdk_ui::timeline::Error) -> bool { + let error_text = error.to_string().to_ascii_lowercase(); + error_text.contains("unknown parent event") +} + /// Build a new client. async fn build_client( @@ -613,6 +618,30 @@ pub enum DirectMessageRoomAction { }, } +#[derive(Clone, Debug)] +pub struct FetchedRoomThread { + pub thread_root_event_id: OwnedEventId, + pub timestamp: MilliSecondsSinceUnixEpoch, + pub title: String, + pub reply_count: u32, + pub latest_reply_preview: Option, +} + +#[derive(Clone, Debug)] +pub enum RoomThreadsAction { + Loaded { + room_id: OwnedRoomId, + from: Option, + threads: Vec, + prev_batch_token: Option, + }, + Failed { + room_id: OwnedRoomId, + from: Option, + error: String, + }, +} + /// Either a main room timeline or a thread-focused timeline. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum TimelineKind { @@ -688,6 +717,11 @@ pub enum MatrixRequest { thread_root_event_id: OwnedEventId, timeline_item_index: usize, }, + /// Request to fetch a page of thread roots for the given room. + ListRoomThreads { + room_id: OwnedRoomId, + from: Option, + }, /// 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. @@ -1279,6 +1313,20 @@ async fn matrix_worker_task( SignalToUI::set_ui_signal(); } Err(error) => { + if direction == PaginationDirection::Backwards + && matches!(timeline_kind, TimelineKind::Thread { .. }) + && is_thread_unknown_parent_timeline_error(&error) + { + warning!( + "Treating unknown parent event as end-of-thread for {timeline_kind}." + ); + sender.send(TimelineUpdate::PaginationIdle { + fully_paginated: true, + direction, + }).unwrap(); + SignalToUI::set_ui_signal(); + return; + } error!("Error sending {direction} pagination request for {timeline_kind}: {error:?}"); sender.send(TimelineUpdate::PaginationError { error, @@ -1368,6 +1416,37 @@ async fn matrix_worker_task( }); } + MatrixRequest::ListRoomThreads { room_id, from } => { + let Some(room) = get_client().and_then(|client| client.get_room(&room_id)) else { + Cx::post_action(RoomThreadsAction::Failed { + room_id, + from, + error: String::from("Room not found."), + }); + continue; + }; + + let _list_threads_task = Handle::current().spawn(async move { + match fetch_room_threads_page(&room, from.clone()).await { + Ok((threads, prev_batch_token)) => { + Cx::post_action(RoomThreadsAction::Loaded { + room_id, + from, + threads, + prev_batch_token, + }); + } + Err(error) => { + Cx::post_action(RoomThreadsAction::Failed { + room_id, + from, + error: error.to_string(), + }); + } + } + }); + } + 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"); @@ -4747,6 +4826,87 @@ async fn text_preview_of_latest_thread_reply( } } +async fn sender_display_name_for_timeline_event( + room: &Room, + event: &matrix_sdk::deserialized_responses::TimelineEvent, +) -> Option<(OwnedUserId, String)> { + let raw = 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), + _ => None, + }; + let sender_name = sender_room_member.as_ref() + .and_then(|rm| rm.display_name()) + .unwrap_or(sender_id.as_str()) + .to_string(); + Some((sender_id, sender_name)) +} + +fn fallback_preview_for_timeline_event( + event: &matrix_sdk::deserialized_responses::TimelineEvent, + sender_name: &str, + as_html: bool, +) -> String { + text_preview_of_raw_timeline_event(event.raw(), sender_name) + .unwrap_or_else(|| { + let event_type = event.raw().get_field::("type").ok().flatten(); + TextPreview::from(( + event_type.unwrap_or_else(|| "unknown event type".to_string()), + BeforeText::UsernameWithColon, + )) + }) + .format_with(sender_name, as_html) +} + +async fn fetch_room_threads_page( + room: &Room, + from: Option, +) -> Result<(Vec, Option), matrix_sdk::Error> { + let response = room.list_threads(ListThreadsOptions { + from: from.clone(), + limit: Some(uint!(20)), + ..Default::default() + }).await?; + + let mut threads = Vec::new(); + for event in response.chunk { + let Some(thread_root_event_id) = event.event_id() else { continue }; + let timestamp = event.timestamp().unwrap_or_else(MilliSecondsSinceUnixEpoch::now); + let sender_name = sender_display_name_for_timeline_event(room, &event).await + .map(|(_, sender_name)| sender_name) + .unwrap_or_else(|| String::from("Unknown user")); + let title = utils::replace_linebreaks_separators( + &fallback_preview_for_timeline_event(&event, &sender_name, false), + true, + ).into_owned(); + let title = if title.trim().is_empty() { + String::from("(No message preview)") + } else { + title + }; + + let reply_count = event.thread_summary.summary() + .map(|summary| summary.num_replies) + .unwrap_or(0); + let latest_reply_preview = if let Some(latest_event) = event.bundled_latest_thread_event.as_ref() { + text_preview_of_latest_thread_reply(room, latest_event).await + } else { + None + }; + + threads.push(FetchedRoomThread { + thread_root_event_id, + timestamp, + title, + reply_count, + latest_reply_preview, + }); + } + + Ok((threads, response.prev_batch_token)) +} + /// Returns the timestamp and an HTML-formatted text preview of the given `latest_event`. /// From b7bcf74e1a63aac497bf16e50b3589b9ae8d7687 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:31:32 +0800 Subject: [PATCH 061/283] Reset room and space widgets on logout --- src/home/rooms_list.rs | 27 +++++++++++++++++++++++++++ src/home/spaces_bar.rs | 12 +++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 444e0bd31..638bdebb5 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -42,6 +42,7 @@ use crate::{ popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction, }, + logout::logout_confirm_modal::LogoutAction, sliding_sync::{MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request}, space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, utils::{RoomNameId, VecDiff}, }; @@ -1396,6 +1397,32 @@ impl Widget for RoomsList { // Second, handle any other actions that came from other widgets/components. if let Event::Actions(actions) = event { for action in actions { + if let Some(LogoutAction::ClearAppState { .. }) = action.downcast_ref() { + self.invited_rooms.borrow_mut().clear(); + self.all_joined_rooms.clear(); + self.all_known_rooms_order.clear(); + self.selected_space = None; + self.space_request_sender = None; + self.space_map.clear(); + self.hidden_rooms.clear(); + self.displayed_invited_rooms.clear(); + self.is_invited_rooms_header_expanded = false; + self.invited_rooms_indexes = RoomCategoryIndexes::default(); + self.displayed_direct_rooms.clear(); + self.is_direct_rooms_header_expanded = false; + self.direct_rooms_indexes = RoomCategoryIndexes::default(); + self.displayed_regular_rooms.clear(); + self.is_regular_rooms_header_expanded = true; + self.regular_rooms_indexes = RoomCategoryIndexes::default(); + self.status.clear(); + self.current_active_room = None; + self.max_known_rooms = None; + self.indexes_dirty = true; + self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, false); + self.redraw(cx); + continue; + } + if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { self.regenerate_display_filter_and_sort_fn(keywords); self.update_displayed_rooms(cx, true); diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 0d9b392ba..01bfeb7fa 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -13,7 +13,7 @@ use matrix_sdk::{RoomDisplayName, RoomState}; use ruma::{OwnedRoomAliasId, OwnedRoomId, room::JoinRuleSummary}; use crate::{ - home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, login::login_screen::LoginAction, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, sliding_sync::AccountSwitchAction, utils::{self, RoomNameId} + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, sliding_sync::AccountSwitchAction, utils::{self, RoomNameId} }; script_mod! { @@ -520,6 +520,16 @@ impl Widget for SpacesBar { if let Event::Actions(actions) = event { for action in actions { + if let Some(LogoutAction::ClearAppState { .. }) = action.downcast_ref() { + self.all_joined_spaces.clear(); + self.display_filter = RoomDisplayFilter::default(); + self.displayed_spaces.clear(); + self.is_filtered = false; + self.selected_space = None; + self.redraw(cx); + continue; + } + // The room filter input bar is also used to filter which spaces are visible. if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast() { self.update_displayed_spaces(cx, &keywords); From d814219e43f959650a7e07214f042f9e4d6ebc17 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:32:04 +0800 Subject: [PATCH 062/283] Handle dropped timeline receivers without panicking --- src/sliding_sync.rs | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 0245ff79e..e539b8813 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1224,7 +1224,10 @@ async fn matrix_worker_task( // 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(); + if sender.send(TimelineUpdate::PaginationRunning(direction)).is_err() { + warning!("Skipping {direction} pagination request for {timeline_kind}: timeline receiver was dropped before start."); + return; + } SignalToUI::set_ui_signal(); let mut res = if direction == PaginationDirection::Forwards { @@ -1272,19 +1275,25 @@ async fn matrix_worker_task( if direction == PaginationDirection::Forwards { "end" } else { "start" }, if fully_paginated { "yes" } else { "no" }, ); - sender.send(TimelineUpdate::PaginationIdle { + if sender.send(TimelineUpdate::PaginationIdle { fully_paginated, direction, - }).unwrap(); - SignalToUI::set_ui_signal(); + }).is_ok() { + SignalToUI::set_ui_signal(); + } else { + warning!("Dropping completed {direction} pagination update for {timeline_kind}: timeline receiver was dropped."); + } } Err(error) => { error!("Error sending {direction} pagination request for {timeline_kind}: {error:?}"); - sender.send(TimelineUpdate::PaginationError { + if sender.send(TimelineUpdate::PaginationError { error, direction, - }).unwrap(); - SignalToUI::set_ui_signal(); + }).is_ok() { + SignalToUI::set_ui_signal(); + } else { + warning!("Dropping failed {direction} pagination update for {timeline_kind}: timeline receiver was dropped."); + } } } }); @@ -1378,8 +1387,11 @@ async fn matrix_worker_task( 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(); + if sender.send(TimelineUpdate::RoomMembersSynced).is_ok() { + SignalToUI::set_ui_signal(); + } else { + warning!("Dropping room members synced update for {timeline_kind}: timeline receiver was dropped."); + } }); } @@ -1644,8 +1656,11 @@ async fn matrix_worker_task( 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(); + if sender.send(TimelineUpdate::RoomMembersListFetched { members }).is_ok() { + SignalToUI::set_ui_signal(); + } else { + warning!("Dropping room members list update for {timeline_kind}: timeline receiver was dropped."); + } }; let room = timeline.room(); From a21cb2a7d628eab90bb33c07472019750888b0a2 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:39:07 +0800 Subject: [PATCH 063/283] Reset desktop dock layout on logout --- src/home/main_desktop_ui.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 7827983cc..01e329e2a 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, sliding_sync::AccountSwitchAction, utils::RoomNameId}; +use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, logout::logout_confirm_modal::LogoutAction, sliding_sync::AccountSwitchAction, utils::RoomNameId}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; script_mod! { @@ -289,6 +289,23 @@ impl MainDesktopUI { self.most_recently_selected_room = None; } + fn reset_to_default_layout(&mut self, cx: &mut Cx) { + self.open_rooms.clear(); + self.tab_to_close = None; + self.room_order.clear(); + self.most_recently_selected_room = None; + self.selected_space = None; + + if let Some(mut dock) = self.view.dock(cx, ids!(dock)).borrow_mut() { + dock.load_state(cx, self.default_layout.dock_items.clone()); + } else { + error!("BUG: failed to borrow dock widget to reset desktop UI to its default layout."); + } + + cx.action(AppStateAction::FocusNone); + self.redraw(cx); + } + /// Replaces an invite with a joined room in the dock. fn replace_invite_with_joined_room( &mut self, @@ -413,9 +430,14 @@ impl WidgetMatchEvent for MainDesktopUI { continue; } + if let Some(LogoutAction::ClearAppState { .. }) = action.downcast_ref() { + self.reset_to_default_layout(cx); + continue; + } + // When switching accounts, close all room tabs (keeping only the home tab) if let Some(AccountSwitchAction::Starting(_)) = action.downcast_ref() { - self.close_all_tabs(cx); + self.reset_to_default_layout(cx); continue; } From 1e3b8d8da287e41825c1d829d00817b8a57846b5 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 13:56:45 +0800 Subject: [PATCH 064/283] Consolidate point-of-no-return logic and fix remaining cleanup gaps - Extract duplicated point-of-no-return blocks into enter_point_of_no_return() - Fix missed sender.send().unwrap() on MessageEdited path - Reset display_filter, sort_fn, and drawn_previously on logout - Drain PENDING_ROOM_UPDATES and PENDING_SPACE_UPDATES in ClearAppState handlers --- src/home/main_desktop_ui.rs | 1 + src/home/rooms_list.rs | 3 ++ src/home/spaces_bar.rs | 1 + src/logout/logout_state_machine.rs | 83 ++++++++++-------------------- src/sliding_sync.rs | 9 ++-- 5 files changed, 38 insertions(+), 59 deletions(-) diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 01e329e2a..04c05ed21 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -295,6 +295,7 @@ impl MainDesktopUI { self.room_order.clear(); self.most_recently_selected_room = None; self.selected_space = None; + self.drawn_previously = false; if let Some(mut dock) = self.view.dock(cx, ids!(dock)).borrow_mut() { dock.load_state(cx, self.default_layout.dock_items.clone()); diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 638bdebb5..32f0f26cb 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -1398,6 +1398,7 @@ impl Widget for RoomsList { if let Event::Actions(actions) = event { for action in actions { if let Some(LogoutAction::ClearAppState { .. }) = action.downcast_ref() { + while PENDING_ROOM_UPDATES.pop().is_some() {} self.invited_rooms.borrow_mut().clear(); self.all_joined_rooms.clear(); self.all_known_rooms_order.clear(); @@ -1414,6 +1415,8 @@ impl Widget for RoomsList { self.displayed_regular_rooms.clear(); self.is_regular_rooms_header_expanded = true; self.regular_rooms_indexes = RoomCategoryIndexes::default(); + self.display_filter = RoomDisplayFilter::default(); + self.sort_fn = None; self.status.clear(); self.current_active_room = None; self.max_known_rooms = None; diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 01bfeb7fa..79e212dc0 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -521,6 +521,7 @@ impl Widget for SpacesBar { if let Event::Actions(actions) = event { for action in actions { if let Some(LogoutAction::ClearAppState { .. }) = action.downcast_ref() { + while PENDING_SPACE_UPDATES.pop().is_some() {} self.all_joined_spaces.clear(); self.display_filter = RoomDisplayFilter::default(); self.displayed_spaces.clear(); diff --git a/src/logout/logout_state_machine.rs b/src/logout/logout_state_machine.rs index 80f347bfb..9d2c4bde4 100644 --- a/src/logout/logout_state_machine.rs +++ b/src/logout/logout_state_machine.rs @@ -317,69 +317,16 @@ impl LogoutStateMachine { match self.perform_server_logout().await { Ok(_) => { - self.point_of_no_return.store(true, Ordering::Release); - set_logout_point_of_no_return(true); - self.transition_to( - LogoutState::PointOfNoReturn, - "Point of no return reached".to_string(), - 50 - ).await?; - - if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { - if let Err(e) = skip_app_state_restore_once(&user_id).await { - log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); - } - } - - // We delete latest_user_id after reaching LOGOUT_POINT_OF_NO_RETURN: - // 1. To prevent auto-login with invalid session on next start - // 2. While keeping session file intact for potential future login - if let Err(e) = delete_latest_user_id().await { - log!("Warning: Failed to delete latest user ID: {}", e); - } + self.enter_point_of_no_return("Point of no return reached").await?; } Err(e) => { // Check if it's an M_UNKNOWN_TOKEN error if matches!(&e, LogoutError::Recoverable(RecoverableError::ServerLogoutFailed(msg)) if msg.contains("M_UNKNOWN_TOKEN")) { log!("Token already invalidated, continuing with logout"); - self.point_of_no_return.store(true, Ordering::Release); - set_logout_point_of_no_return(true); - self.transition_to( - LogoutState::PointOfNoReturn, - "Token already invalidated".to_string(), - 50 - ).await?; - - if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { - if let Err(e) = skip_app_state_restore_once(&user_id).await { - log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); - } - } - - // Same delete operation as in the success case above - if let Err(e) = delete_latest_user_id().await { - log!("Warning: Failed to delete latest user ID: {}", e); - } + self.enter_point_of_no_return("Token already invalidated").await?; } else if should_continue_local_logout_without_server(&e) { log!("Homeserver appears unavailable, continuing with local logout: {}", e); - self.point_of_no_return.store(true, Ordering::Release); - set_logout_point_of_no_return(true); - self.transition_to( - LogoutState::PointOfNoReturn, - "Homeserver unavailable, continuing with local logout".to_string(), - 50 - ).await?; - - if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { - if let Err(e) = skip_app_state_restore_once(&user_id).await { - log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); - } - } - - // Same delete operation as in the success case above - if let Err(e) = delete_latest_user_id().await { - log!("Warning: Failed to delete latest user ID: {}", e); - } + self.enter_point_of_no_return("Homeserver unavailable, continuing with local logout").await?; } else { // Restart sync service since we haven't reached point of no return if let Some(sync_service) = get_sync_service() { @@ -484,6 +431,30 @@ impl LogoutStateMachine { Ok(()) } + /// Sets the global point-of-no-return flags, writes the skip-restore marker, + /// and deletes the saved user ID so the next app start won't auto-login. + async fn enter_point_of_no_return(&self, message: &str) -> Result<()> { + self.point_of_no_return.store(true, Ordering::Release); + set_logout_point_of_no_return(true); + self.transition_to( + LogoutState::PointOfNoReturn, + message.to_string(), + 50 + ).await?; + + if let Some(user_id) = get_client().and_then(|client| client.user_id().map(ToOwned::to_owned)) { + if let Err(e) = skip_app_state_restore_once(&user_id).await { + log!("Warning: Failed to mark app state restore to skip once for {user_id}: {e}"); + } + } + + if let Err(e) = delete_latest_user_id().await { + log!("Warning: Failed to delete latest user ID: {}", e); + } + + Ok(()) + } + // Individual step implementations async fn perform_prechecks(&self) -> Result<(), LogoutError> { log!("perform_prechecks started"); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index e539b8813..41a573cd0 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1313,11 +1313,14 @@ async fn matrix_worker_task( 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 { + if sender.send(TimelineUpdate::MessageEdited { timeline_event_item_id, result, - }).unwrap(); - SignalToUI::set_ui_signal(); + }).is_ok() { + SignalToUI::set_ui_signal(); + } else { + warning!("Dropping message edited update for {timeline_kind}: timeline receiver was dropped."); + } }); } From c6e76414fc71808cc42d026f42809ae38e9706cd Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 15:24:26 +0800 Subject: [PATCH 065/283] docs: restructure deployment guides into categorized documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the monolithic deployment guide into a clear documentation hierarchy with two categories and descriptive filenames: - robrix/ — standalone Robrix getting-started guide - robrix-with-palpo-and-octos/ — integrated system docs: 01-deploying-palpo-and-octos (deployment) 02-how-robrix-palpo-octos-work-together (architecture/App Service) 03-using-robrix-with-palpo-and-octos (usage with screenshots) 04-federation-with-palpo (federation deployment and usage) Move deployment configs from docs/examples/ to palpo-and-octos-deploy/ at the project root, separating runnable files from documentation. Each document now has a clear goal statement at the top. Bilingual (EN/ZH) versions maintained for all files. --- README.md | 2 +- docs/README.md | 55 ++ docs/deployment-guide-zh.md | 603 ----------------- docs/deployment-guide.md | 605 ------------------ docs/examples/static/index.html | 9 - .../01-deploying-palpo-and-octos-zh.md | 500 +++++++++++++++ .../01-deploying-palpo-and-octos.md | 500 +++++++++++++++ ...how-robrix-palpo-octos-work-together-zh.md | 321 ++++++++++ ...02-how-robrix-palpo-octos-work-together.md | 321 ++++++++++ ...03-using-robrix-with-palpo-and-octos-zh.md | 213 ++++++ .../03-using-robrix-with-palpo-and-octos.md | 214 +++++++ .../04-federation-with-palpo-zh.md | 417 ++++++++++++ .../04-federation-with-palpo.md | 417 ++++++++++++ docs/robrix/getting-started-with-robrix-zh.md | 72 +++ docs/robrix/getting-started-with-robrix.md | 72 +++ .../.env.example | 0 .../.gitignore | 0 .../appservices/octos-registration.yaml | 0 .../compose.yml | 1 - .../config/botfather.json | 0 .../config/octos.json | 0 .../palpo.toml | 0 .../setup.sh | 0 23 files changed, 3103 insertions(+), 1219 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/deployment-guide-zh.md delete mode 100644 docs/deployment-guide.md delete mode 100644 docs/examples/static/index.html create mode 100644 docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md create mode 100644 docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md create mode 100644 docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md create mode 100644 docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md create mode 100644 docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md create mode 100644 docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md create mode 100644 docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md create mode 100644 docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md create mode 100644 docs/robrix/getting-started-with-robrix-zh.md create mode 100644 docs/robrix/getting-started-with-robrix.md rename {docs/examples => palpo-and-octos-deploy}/.env.example (100%) rename {docs/examples => palpo-and-octos-deploy}/.gitignore (100%) rename {docs/examples => palpo-and-octos-deploy}/appservices/octos-registration.yaml (100%) rename {docs/examples => palpo-and-octos-deploy}/compose.yml (98%) rename {docs/examples => palpo-and-octos-deploy}/config/botfather.json (100%) rename {docs/examples => palpo-and-octos-deploy}/config/octos.json (100%) rename {docs/examples => palpo-and-octos-deploy}/palpo.toml (100%) rename {docs/examples => palpo-and-octos-deploy}/setup.sh (100%) diff --git a/README.md b/README.md index 6b8b9d2b6..d7773d0e0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Robrix is a Matrix chat client written in Rust to exemplify the features of [Project Robius](https://github.com/project-robius), a framework for multi-platform application development in Rust. Robrix is written using the [Makepad UI toolkit](https://github.com/makepad/makepad/). > [!TIP] -> **Want to deploy Robrix with an AI bot?** Check out the [Deployment Guide](docs/deployment-guide.md) ([中文版](docs/deployment-guide-zh.md)) — a step-by-step guide to set up Robrix + [Palpo](https://github.com/palpo-im/palpo) (Matrix homeserver) + [Octos](https://github.com/octos-org/octos) (AI bot) together. +> **Want to deploy Palpo and Octos, then use Robrix to chat with an AI bot?** Check out the [Documentation](docs/README.md) for guides on running [Palpo](https://github.com/palpo-im/palpo) (Matrix homeserver) + [Octos](https://github.com/octos-org/octos) (AI bot), understanding the App Service architecture, using Robrix as the client, and enabling federation. Quick links: [Deployment](docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) · [Architecture](docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) · [Usage](docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md) · [Federation](docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md) Check out our most recent talks and presentations for more info: * Robrix: a complex, multi-platform app in Rust for secure chat using Matrix ([Rust China Conf 2025](https://rustcc.cn/2025conf/schedule.html)) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..217613645 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,55 @@ +# Robrix Documentation + +Welcome to the Robrix documentation. Choose a guide based on your use case. + +--- + +## Robrix Only + +For users who want to use Robrix as a standalone Matrix client, connecting to matrix.org or any existing homeserver: + +| Guide | Goal | +|-------|------| +| [Getting Started with Robrix](robrix/getting-started-with-robrix.md) | **Install Robrix and start chatting.** Download or build Robrix, connect to a Matrix server, register an account, and join rooms. | + +> Chinese: [Robrix 快速开始](robrix/getting-started-with-robrix-zh.md) + +--- + +## Robrix + Palpo + Octos (AI Bot System) + +For users who want to deploy a complete AI chat system — running your own Matrix homeserver with AI bot capabilities, then using Robrix to chat with AI bots: + +| Guide | Goal | +|-------|------| +| [1. Deploying Palpo and Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) | **Get Palpo homeserver and Octos AI bot running.** Clone, configure, and launch all backend services with Docker Compose so Robrix can connect to your own server. | +| [2. How Robrix, Palpo, and Octos Work Together](robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) | **Understand the Application Service mechanism.** Learn how Octos registers as a Matrix App Service on Palpo, how messages flow from Robrix through Palpo to the AI bot, and how the BotFather system manages multiple bots. | +| [3. Using Robrix with Palpo and Octos](robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md) | **Use Robrix to chat with AI bots on your Palpo server.** Step-by-step with screenshots: log in, create rooms, invite bots, have conversations, and manage bots through the BotFather system. | +| [4. Federation with Palpo](robrix-with-palpo-and-octos/04-federation-with-palpo.md) | **Enable cross-server communication.** Configure Palpo for Matrix federation so users on different servers can chat with each other and access your AI bots. | + +> Chinese: +> [1. 部署 Palpo 和 Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) · +> [2. Robrix、Palpo、Octos 协作原理](robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md) · +> [3. 在 Robrix 上使用 Palpo 和 Octos](robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md) · +> [4. Palpo 联邦功能](robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md) + +--- + +## Palpo and Octos Deployment Files + +The [`palpo-and-octos-deploy/`](../palpo-and-octos-deploy/) directory (at the repository root) contains the runnable deployment files for Palpo and Octos, including Docker Compose and configuration templates: + +``` +palpo-and-octos-deploy/ +├── compose.yml # Docker Compose orchestration +├── setup.sh # One-time setup script (clones source repos) +├── palpo.toml # Palpo homeserver config +├── .env.example # Environment variables template +├── appservices/ +│ └── octos-registration.yaml # Appservice registration (Palpo ↔ Octos) +├── config/ +│ ├── botfather.json # Bot profile and LLM settings +│ └── octos.json # Octos global settings +├── repos/ # Source repos (created by setup.sh, gitignored) +└── data/ # Runtime data (created by Docker, gitignored) +``` diff --git a/docs/deployment-guide-zh.md b/docs/deployment-guide-zh.md deleted file mode 100644 index 4614dc46f..000000000 --- a/docs/deployment-guide-zh.md +++ /dev/null @@ -1,603 +0,0 @@ -# 部署指南:Robrix + Palpo + Octos - -[English Version](deployment-guide.md) - -本指南帮助你部署一套完整的 **Matrix AI 聊天系统**:Matrix 主服务器、AI 机器人后端,以及 Robrix 客户端——三者协同工作,让你在 Robrix 中与 AI 机器人对话。 - -> **只想快速试试?** 跳到 [快速开始](#2-快速开始) — 5 步即可运行。 - ---- - -## 目录 - -1. [这些项目是什么?](#1-这些项目是什么) -2. [快速开始](#2-快速开始) -3. [配置详解](#3-配置详解) -4. [使用 Robrix](#4-使用-robrix) -5. [端到端验证](#5-端到端验证) -6. [故障排除](#6-故障排除) -7. [延伸阅读](#7-延伸阅读) - ---- - -## 1. 这些项目是什么? - -三个开源项目协同工作,构成一个完整的 AI 聊天系统: - -| 项目 | 角色 | 说明 | -| ---------------------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 用 Rust 编写的跨平台 Matrix 聊天客户端,基于[Makepad](https://github.com/makepad/makepad/) UI 框架。这是你看到并直接使用的程序——原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 | -| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix 主服务器 | Rust 原生的 Matrix 主服务器。它存储用户、房间和消息,并在客户端(Robrix)和应用服务(Octos)之间路由事件。可以把它理解为整个系统的"邮局"。 | -| [**Octos**](https://github.com/octos-org/octos) | AI 机器人(应用服务) | Rust 原生的 AI 智能体平台,以[Matrix Application Service](https://spec.matrix.org/latest/application-service-api/)(应用服务)身份运行。它从 Palpo 接收消息,发送给 LLM(如 DeepSeek、OpenAI 等),然后将 AI 的回复发回。 | - -### 架构 - -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────┐ -│ Robrix │ Client-Server API │ Palpo │ Appservice API │ Octos │ HTTPS │ LLM │ -│ (客户端) │ ────────────────────► │ (服务器) │ ─────────────────► │ (机器人) │ ──────► │ │ -│ │ ◄──────────────────── │ │ ◄─────────────────── │ │ ◄────── │ │ -└──────────┘ Sliding Sync └──────────┘ Client-Server API └──────────┘ └─────┘ - 你的电脑 Docker :8128 Docker :8009 外部服务 -``` - -**发送消息时的数据流:** - -1. 你在 Robrix 中输入一条消息 -2. Robrix 通过 Matrix Client-Server API 将消息发送给 Palpo -3. Palpo 发现该消息所在的房间有 Octos 存在,通过 Appservice API 将事件推送给 Octos -4. Octos 收到事件后,调用配置的 LLM(如 DeepSeek)获取回复 -5. Octos 通过 Palpo 的 Client-Server API 将 AI 回复发回 -6. Palpo 将回复推送给 Robrix,你看到机器人的回复 - -### 端口与协议 - -| 连接 | 协议 | 默认端口 | 备注 | -| --------------- | -------------------------------- | ----------------------------- | ------------------------- | -| Robrix → Palpo | Client-Server API (Sliding Sync) | 8128(宿主机)→ 8008(容器) | Robrix 唯一需要访问的端口 | -| Palpo → Octos | Appservice API | 8009(Docker 内部网络) | Palpo 向 Octos 推送事件 | -| Octos → Palpo | Client-Server API | 8008(Docker 内部网络) | Octos 通过 Palpo 回复消息 | -| Octos 控制面板 | HTTP | 8010(宿主机)→ 8080(容器) | 可选的管理界面 | -| Octos → LLM | HTTPS | 443(出站) | 外部 API 调用 | - ---- - -## 2. 快速开始 - -4 步在本地跑通所有服务。 - -### 前提条件 - -- **Docker** 和 **Docker Compose**(v2+) -- **Git** -- **一个 LLM API Key** — 如 [DeepSeek](https://platform.deepseek.com/)(有免费额度) -- **Robrix** — [下载预编译版本](https://github.com/Project-Robius-China/robrix2/releases),或从源码构建:`cargo run --release` - -### 步骤 1:获取示例配置 - -```bash -git clone https://github.com/Project-Robius-China/robrix2.git -cd robrix2/docs/examples -``` - -### 步骤 2:运行初始化脚本 - -```bash -./setup.sh -``` - -此脚本会克隆 Palpo 和 Octos 的源码仓库,并创建 `.env` 文件。两者均从源码构建,以支持所有架构(x86_64、ARM64/Apple Silicon 等)。 - -### 步骤 3:设置 API Key - -编辑 `.env`,将 `your-api-key-here` 替换为你的 DeepSeek API Key: - -``` -DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx -``` - -### 步骤 4:启动服务 - -```bash -docker compose up -d -``` - -> **注意:** 首次运行会从源码编译 Palpo 和 Octos,这可能需要 **10–30 分钟**(取决于你的机器性能和网络速度)。Palpo 需要编译其 Rust 代码;Octos 还额外需要下载 Node.js、Chromium 等技能插件的运行时工具。后续启动会使用缓存镜像,几秒内完成。 - -检查运行状态: - -```bash -docker compose ps -``` - -你应该看到三个服务(`palpo_postgres`、`palpo`、`octos`)都处于 `running` 状态。 - -### 步骤 5:用 Robrix 连接(构建完成后) - -1. **打开 Robrix**(还没有?见 [4.1 获取 Robrix](#41-获取-robrix)) -2. **设置服务器地址**:在登录界面,在 **Homeserver URL** 输入框中(密码框下方)输入 `http://127.0.0.1:8128` - - -3. **注册新账号**:输入用户名和密码,点击 **Sign up** - - ![注册账号 — 输入用户名、密码和服务器地址](images/register-account.png) -4. **与 AI 机器人对话**:登录后,加入或创建一个房间,然后邀请机器人: - - - 点击房间中的邀请按钮 - - 输入 `@octosbot:127.0.0.1:8128` - - 发送一条消息——AI 机器人应该会回复! - - - -**完成!** 你现在拥有了一个可工作的 Robrix + Palpo + Octos 系统。继续阅读了解配置详情,或跳到 [故障排除](#6-故障排除) 解决问题。 - ---- - -## 3. 配置详解 - -本节解释 `examples/` 目录中的每个配置文件。快速开始已经让你跑起来了——当你需要自定义时再来这里查阅。 - -### 3.1 目录结构 - -``` -examples/ -├── compose.yml # Docker Compose — 编排所有服务 -├── .env.example # 环境变量模板 -├── palpo.toml # Palpo 主服务器配置 -├── appservices/ -│ └── octos-registration.yaml # 应用服务注册文件(连接 Palpo ↔ Octos) -├── config/ -│ ├── botfather.json # Octos 机器人配置(Matrix 通道) -│ └── octos.json # Octos 全局设置 -├── data/ # 持久化数据(运行时自动创建) -│ ├── pgsql/ # PostgreSQL 数据库文件 -│ ├── octos/ # Octos 运行时数据 -│ └── media/ # Palpo 媒体存储 -└── static/ - └── index.html # Palpo 首页(可选) -``` - -### 3.2 令牌生成 - -应用服务注册文件和 Octos 机器人配置共享两个密钥令牌,用于双向认证。示例文件中已预填开发用令牌,但**在生产环境中你必须重新生成**: - -```bash -openssl rand -hex 32 # → 用作 as_token -openssl rand -hex 32 # → 用作 hs_token -``` - -这两个值必须在 `appservices/octos-registration.yaml` 和 `config/botfather.json` 中完全一致。如果不匹配,机器人将无法工作。详见 [令牌匹配检查清单](#37-令牌匹配检查清单)。 - -### 3.3 应用服务注册文件(`appservices/octos-registration.yaml`) - -此文件告诉 Palpo 关于 Octos 的信息——Octos 管理哪些用户命名空间,以及将事件发送到哪里。 - -```yaml -id: octos-matrix-appservice -url: "http://octos:8009" - -as_token: "<你的-as-token>" -hs_token: "<你的-hs-token>" - -sender_localpart: octosbot -rate_limited: false - -namespaces: - users: - - exclusive: true - regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" - - exclusive: true - regex: "@octosbot:127\\.0\\.0\\.1:8128" - aliases: [] - rooms: [] -``` - -| 字段 | 说明 | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `id` | 此应用服务注册的唯一标识符。 | -| `url` | Palpo 发送事件的目标地址。使用 Docker 服务名 `octos`(不是 `localhost`),因为两个容器在同一个 Docker 网络中。 | -| `as_token` | Octos 调用 Palpo API 时使用的令牌。必须与 `botfather.json` 匹配。 | -| `hs_token` | Palpo 向 Octos 推送事件时使用的令牌。必须与 `botfather.json` 匹配。 | -| `sender_localpart` | 机器人的 Matrix 本地用户名。最终变为 `@octosbot:127.0.0.1:8128`。 | -| `rate_limited` | 设为 `false`,让机器人回复不受速率限制。 | -| `namespaces.users` | 此应用服务管理的用户 ID 正则匹配模式。包含机器人本身(`@octosbot:...`)和动态创建的子机器人(`@octosbot_*:...`)。 | - -### 3.4 Palpo 配置(`palpo.toml`) - -```toml -server_name = "127.0.0.1:8128" - -allow_registration = true -yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true -enable_admin_room = true - -appservice_registration_dir = "/var/palpo/appservices" - -# HTTP 监听器(Client-Server API) -[[listeners]] -address = "0.0.0.0:8008" - -[logger] -format = "pretty" - -[db] -url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" -pool_size = 10 - -[well_known] -server = "127.0.0.1:8128" -client = "http://127.0.0.1:8128" -``` - -| 字段 | 说明 | -| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `server_name` | 所有 Matrix ID 的域名部分(如 `@user:127.0.0.1:8128`)。生产环境使用你的实际域名。 | -| `allow_registration` | 是否允许新用户注册。设为 `true` 以便 Robrix 用户可以创建账号。生产环境中初始配置完成后可改为 `false`。 | -| `yes_i_am_very_very_sure_...` | 当 `allow_registration = true` 时必填的安全确认。字段名故意很长,提醒你开放注册的安全风险。 | -| `enable_admin_room` | 启用服务器管理员房间。 | -| `appservice_registration_dir` | Palpo 启动时自动加载此目录下所有 `.yaml` 文件。Octos 就是通过这种方式被发现的。 | -| `[[listeners]]` | 网络监听器。每个条目定义一个 Palpo 监听的地址。 | -| `[logger]` | 日志格式。`"pretty"` 用于开发,`"json"` 用于生产。 | -| `[db]` | PostgreSQL 连接配置。`palpo_postgres` 是 Docker 服务名。密码必须与 `compose.yml` 中的 `POSTGRES_PASSWORD` 匹配。 | -| `[well_known]` | 用于客户端发现服务器。必须与外部可访问的地址匹配。 | - -> **注意:** 在这个本地 Docker 示例里,Matrix 身份统一使用 `127.0.0.1:8128`。因此 `server_name`、应用服务正则和机器人用户 ID 都必须写成 `127.0.0.1:8128`。只有容器之间通信时才使用 `palpo:8008`、`octos:8009` 这类 Docker 服务名。 - -> **延伸阅读:** [Palpo GitHub](https://github.com/palpo-im/palpo) 了解更多高级配置(联邦、TLS、TURN 等)。 - -### 3.5 Octos 机器人配置(`config/botfather.json`) - -此文件定义机器人的身份、LLM 提供商和 Matrix 通道配置。 - -```json -{ - "id": "botfather", - "name": "BotFather", - "enabled": true, - "config": { - "provider": "deepseek", - "model": "deepseek-chat", - "api_key_env": "DEEPSEEK_API_KEY", - "channels": [ - { - "type": "matrix", - "homeserver": "http://palpo:8008", - "as_token": "<你的-as-token>", - "hs_token": "<你的-hs-token>", - "server_name": "127.0.0.1:8128", - "sender_localpart": "octosbot", - "user_prefix": "octosbot_", - "port": 8009, - "allowed_senders": [] - } - ], - "gateway": { - "max_history": 50, - "queue_mode": "followup" - } - }, - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z" -} -``` - -> **重要:** `created_at` 和 `updated_at` 字段是 Octos **必需的**。如果缺少这两个字段,Octos 会跳过该 profile,机器人将无法启动。 - -**LLM 提供商设置:** - -| 字段 | 说明 | -| --------------- | ---------------------------------------------------------------------------------------------------------------------- | -| `provider` | LLM 提供商名称。Octos 支持 `deepseek`、`openai`、`anthropic` 等 [14 种提供商](https://octos-org.github.io/octos/)。 | -| `model` | 模型标识符(如 `deepseek-chat`、`gpt-4o`、`claude-sonnet-4-20250514`)。 | -| `api_key_env` | 存放 API Key 的环境变量名称。 | - -**Matrix 通道设置:** - -| 字段 | 说明 | -| --------------------------- | --------------------------------------------------------------------- | -| `type` | 必须为 `"matrix"`。 | -| `homeserver` | Palpo 的内部 URL。使用 Docker 服务名 `palpo`,不是 `localhost`。 | -| `as_token` / `hs_token` | 必须与应用服务注册 YAML 文件匹配。 | -| `server_name` | Matrix 域名。必须与 `palpo.toml` 中的 `server_name` 一致。 | -| `sender_localpart` | 机器人用户名。必须与注册文件一致。 | -| `user_prefix` | 动态创建的子机器人用户 ID 前缀(如 `octosbot_translator`)。 | -| `port` | Octos 监听 Palpo 应用服务事件的端口。 | -| `allowed_senders` | 允许与机器人对话的 Matrix 用户 ID。空数组 `[]` = 所有人都可以对话。 | - -> **注意:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关,但不能混用。 - -**Gateway 设置:** - -| 字段 | 说明 | -| --------------- | --------------------------------------------------------------- | -| `max_history` | 作为 LLM 上下文发送的最大历史消息数量。 | -| `queue_mode` | Octos 处理传入消息的方式。`followup` 将新消息排队并顺序处理。 | - -> **延伸阅读:** [Octos Book — LLM 提供商与路由](https://octos-org.github.io/octos/) 了解全部 14 种提供商、降级链和自适应路由。 - -### 3.6 Docker Compose(`compose.yml`) - -提供的 `compose.yml` 启动三个服务: - -| 服务 | 镜像 | 暴露端口 | 用途 | -| ------------------ | --------------------------------- | ---------------------------- | ----------------- | -| `palpo_postgres` | `postgres:17` | *(无,仅内部)* | Palpo 的数据库 | -| `palpo` | 从源码构建 | `8128:8008` | Matrix 主服务器 | -| `octos` | 从源码构建 | `8009:8009`、`8010:8080` | AI 机器人应用服务 | - -**端口映射说明:** - -- `8128` → Robrix 连接此端口(Client-Server API) -- `8009` → Palpo 向 Octos 推送事件(Appservice API) -- `8010` → Octos 管理控制面板(可选,用于监控) - -**持久化卷:** - -| 卷 | 用途 | -| ---------------- | ------------------------------------------------- | -| `./data/pgsql` | PostgreSQL 数据。`docker compose down` 后保留。 | -| `./data/octos` | Octos 运行时数据(会话、记忆)。 | -| `./data/media` | 通过 Matrix 上传的媒体文件(图片、文件)。 | - -**环境变量(`.env`):** - -| 变量 | 必填 | 默认值 | 说明 | -| -------------------- | ------------ | ---------------------- | ----------------------- | -| `DEEPSEEK_API_KEY` | **是** | — | 你的 LLM API Key | -| `DB_PASSWORD` | 否 | `palpo_dev_password` | PostgreSQL 密码 | -| `RUST_LOG` | 否 | `octos=debug,info` | 日志详细程度 | - -### 3.7 令牌匹配检查清单 - -最常见的配置错误是令牌不匹配。以下值在两个文件中**必须完全一致**: - -| 值 | 在 `octos-registration.yaml` 中 | 在 `botfather.json` 中 | -| -------------------- | --------------------------------- | ---------------------------------- | -| `as_token` | `as_token: "abc..."` | `"as_token": "abc..."` | -| `hs_token` | `hs_token: "def..."` | `"hs_token": "def..."` | -| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | -| `server_name` | `regex: "@octosbot:127\\.0\\.0\\.1:8128"` | `"server_name": "127.0.0.1:8128"` | - -如果有任何不匹配,机器人将不会响应消息。提交 bug 报告前请先检查! - ---- - -## 4. 使用 Robrix - -本节介绍如何使用 Robrix 客户端连接 Palpo 服务器并与 Octos AI 机器人交互。 - -### 4.1 获取 Robrix - -**下载预编译版本(推荐):** - -从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载。支持 macOS、Linux 和 Windows。 - -**或从源码构建:** - -1. [安装 Rust](https://www.rust-lang.org/tools/install) -2. Linux 上安装依赖: - ```bash - sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev - ``` -3. 构建并运行: - ```bash - cargo run --release - ``` - -移动端构建(Android/iOS)和打包分发的说明,详见 [Robrix README](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop)。 - -### 4.2 连接到 Palpo 服务器 - -启动 Robrix 后,你会看到登录界面: - - - -**Homeserver URL** 输入框位于登录表单底部。如果留空,默认连接 `matrix.org`。要连接你的本地 Palpo 实例: - -- **本地部署:** 输入 `http://127.0.0.1:8128` -- **远程部署:** 输入 `https://your.server.name`(或 `http://服务器IP:8128`) - - - -> **注意:** Robrix 要求主服务器支持 [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync)。Palpo 原生支持此功能。 - -### 4.3 注册与登录 - -**首次使用——注册新账号:** - -1. 输入你想要的**用户名**和**密码** -2. 在**确认密码**栏(注册时出现)再次输入密码 -3. 输入 **Homeserver URL**(如 `http://127.0.0.1:8128`) -4. 点击 **Sign up** - -![注册账号 — 输入用户名、密码和服务器地址](images/register-account.png) - -**再次使用——登录:** - -1. 输入**用户名**和**密码** -2. 输入 **Homeserver URL** -3. 点击 **Log in** - -登录成功后,你会看到房间列表(新账号为空)。 - - - -### 4.4 与 AI 机器人交互 - -有两种方式开始与机器人聊天: - -#### 方式一:邀请机器人到房间 - -1. 创建一个新房间或打开现有房间 -2. 点击房间中的**邀请**按钮 -3. 输入机器人的 Matrix ID:`@octosbot:127.0.0.1:8128`(将 `127.0.0.1:8128` 替换为你的 `server_name`) -4. 机器人会自动加入房间 - - - -#### 方式二:加入机器人所在的房间 - -1. 点击**加入房间**按钮(或使用房间浏览器) -2. 输入机器人已设置的房间别名或 ID -3. 开始聊天 - - - -#### 与机器人对话 - -机器人加入房间后,直接输入消息并发送即可。机器人会通过配置的 LLM 处理你的消息并回复。 - - - -### 4.5 机器人管理(高级功能) - -Robrix 内置了通过 BotFather 系统管理 Matrix 机器人的功能。 - -#### 启用应用服务支持 - -1. 打开 Robrix 的**设置** -2. 导航到 **Bot Settings** -3. 开启 **Enable App Service** -4. 输入 **BotFather User ID**(如 `@octosbot:127.0.0.1:8128`) -5. 点击 **Save** - - - -#### 创建子机器人 - -启用 BotFather 后,你可以创建专用的子机器人: - -1. 使用 **Create Bot** 对话框 -2. 填写: - - **Username** — 仅限小写字母、数字和下划线(如 `translator_bot`) - - **Display Name** — 可读的显示名称(如 "翻译机器人") - - **System Prompt** — 机器人的初始指令(如 "你是一个翻译器。将所有消息翻译成中文。") -3. 点击 **Create Bot** - -机器人将以 `@octosbot_:127.0.0.1:8128` 的身份创建。 - - - ---- - -## 5. 端到端验证 - -部署完成后,按照以下检查清单确认一切正常: - -### 服务健康检查 - -```bash -# 检查所有容器是否运行 -docker compose ps - -# 检查 Palpo 日志是否有启动错误 -docker compose logs palpo | tail -20 - -# 检查 Octos 日志——寻找 "appservice listening" 或类似信息 -docker compose logs octos | tail -20 - -# 验证 Palpo 是否响应 -curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 -``` - -### 客户端连接 - -- [ ] Robrix 能连接到 `http://127.0.0.1:8128` -- [ ] 能注册新账号 -- [ ] 登录后房间列表能加载(新账号可能为空) -- [ ] 能创建新房间 - -### 机器人交互 - -- [ ] 能邀请 `@octosbot:127.0.0.1:8128` 到房间 -- [ ] 机器人加入房间(如果没有,检查 `docker compose logs octos`) -- [ ] 发送消息后机器人回复 -- [ ] 回复内容合理(确认 LLM 连接正常) - -### 如果某步失败 - -按照数据流顺序检查日志: - -```bash -# 1. Palpo 是否收到了 Robrix 的消息? -docker compose logs palpo --since 1m - -# 2. Palpo 是否将事件转发给了 Octos? -docker compose logs palpo --since 1m | grep -i appservice - -# 3. Octos 是否收到并处理了事件? -docker compose logs octos --since 1m - -# 4. Octos 是否成功调用了 LLM? -docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" -``` - ---- - -## 6. 故障排除 - -### 6.1 服务启动问题 - -| 症状 | 原因 | 解决方法 | -| --------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------- | -| `palpo_postgres` 无法启动 | 端口 5432 已被占用,或数据损坏 | 检查 `docker compose logs palpo_postgres`。删除 `data/pgsql/` 重新开始。 | -| `palpo` 构建失败 | 网络问题或源码获取失败 | 确保 Docker 能访问 `github.com`。检查 `docker compose logs palpo` 查看构建错误。 | -| `palpo` 启动时崩溃 | `palpo.toml` 语法错误或数据库连接失败 | 检查日志。确保 `palpo_postgres` 先正常运行。验证数据库密码一致。 | -| `octos` 构建失败 | 缺少 Dockerfile 或网络问题 | 确保 Docker 能访问 `github.com`。或者在本地构建 Octos 并修改 `compose.yml` 使用本地镜像。 | -| `octos` 启动但日志有错误 | `botfather.json` 无效或缺少 API Key | 检查 JSON 语法。验证 `.env` 中已设置 `DEEPSEEK_API_KEY`。 | - -### 6.2 Robrix 连接问题 - -| 症状 | 原因 | 解决方法 | -| -------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| "无法连接到服务器" | Homeserver URL 错误或 Palpo 未运行 | 确认 Palpo 正在运行(`docker compose ps`)。确认 URL 为 `http://127.0.0.1:8128`。 | -| 登录成功但没有房间 | 新账号的正常现象 | 创建一个新房间。加入或创建后房间会出现在列表中。 | -| 注册失败 | `palpo.toml` 中 `allow_registration = false`,或 server_name 不匹配 | 检查 `palpo.toml`。确保 `allow_registration = true`。 | -| "Homeserver 不支持 Sliding Sync" | Palpo 版本过旧 | 重新构建 Palpo:`docker compose build --no-cache palpo`。 | -| 连接超时 | 防火墙阻止了端口 8128 | 检查防火墙规则。macOS 上在系统设置中允许传入连接。 | - -### 6.3 机器人问题 - -| 症状 | 原因 | 解决方法 | -| --------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -| 机器人不响应消息 | 注册文件和配置文件之间令牌不匹配 | 验证[令牌匹配检查清单](#37-令牌匹配检查清单)。 | -| Palpo 日志中出现 `Connection refused` | Octos 未运行,或注册 YAML 中 `url` 错误 | 确保 Octos 正在运行。`url` 必须使用 Docker 服务名(`http://octos:8009`),不能用 `localhost`。 | -| `User ID not in namespace` | `sender_localpart` 与 `namespaces.users` 正则不匹配 | 更新 `octos-registration.yaml` 中的正则表达式,包含机器人的完整用户 ID 模式。 | -| 机器人加入房间但回复空消息 | LLM API Key 无效或额度不足 | 检查 `docker compose logs octos` 中的 API 错误。验证 API Key 和账户余额。 | -| 部分用户的消息被忽略 | `botfather.json` 中的 `allowed_senders` 过滤 | 将用户的 Matrix ID 添加到 `allowed_senders` 数组中,或设为 `[]` 允许所有人。 | - -### 6.4 常用调试命令 - -```bash -# 实时查看所有服务日志 -docker compose logs -f - -# 查看特定服务的日志 -docker compose logs -f palpo -docker compose logs -f octos - -# 重启单个服务 -docker compose restart octos - -# 检查 Palpo 的 API -curl http://127.0.0.1:8128/_matrix/client/versions - -# 完全重置(警告:删除所有数据) -docker compose down -v -rm -rf data/ -docker compose up -d -``` - ---- - -## 7. 延伸阅读 - -- **Octos 完整文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — 覆盖所有 LLM 提供商、通道(Telegram、Slack、Discord 等)、技能、记忆系统和高级配置。 -- **Octos Matrix Appservice 指南:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — 本文档参考的原始指南,包含更多上下文。 -- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo 主服务器文档。 -- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix 客户端、构建说明和功能追踪。 -- **Matrix Appservice 规范:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — 应用服务的 Matrix 协议规范。 - ---- - -*本指南内容截至 2026 年 4 月。最新更新请查看各项目的仓库。* diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md deleted file mode 100644 index d0d40cac5..000000000 --- a/docs/deployment-guide.md +++ /dev/null @@ -1,605 +0,0 @@ -# Deploying Robrix with Palpo and Octos - -[中文版 (Chinese Version)](deployment-guide-zh.md) - -This guide walks you through deploying a complete **Matrix AI chat system**: a Matrix homeserver, an AI bot backend, and the Robrix client — all working together so you can chat with an AI bot from Robrix. - -> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start) — 5 steps to get running. - ---- - -## Table of Contents - -1. [What Are These Projects?](#1-what-are-these-projects) -2. [Quick Start](#2-quick-start) -3. [Configuration Details](#3-configuration-details) -4. [Using Robrix](#4-using-robrix) -5. [End-to-End Verification](#5-end-to-end-verification) -6. [Troubleshooting](#6-troubleshooting) -7. [Further Reading](#7-further-reading) - ---- - -## 1. What Are These Projects? - -Three open-source projects work together to form a complete AI chat system: - -| Project | Role | What it does | -|---------|------|-------------| -| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client written in Rust using [Makepad](https://github.com/makepad/makepad/). This is what you see and interact with — it runs natively on macOS, Linux, Windows, Android, and iOS. | -| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix Homeserver | A Rust-native Matrix homeserver. It stores users, rooms, and messages, and routes events between clients (Robrix) and application services (Octos). Think of it as the "post office" of the system. | -| [**Octos**](https://github.com/octos-org/octos) | AI Bot (Appservice) | A Rust-native AI agent platform that runs as a [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/). It receives messages from Palpo, sends them to an LLM (like DeepSeek, OpenAI, etc.), and posts the AI's reply back. | - -### Architecture - -``` -┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────┐ -│ Robrix │ Client-Server API │ Palpo │ Appservice API │ Octos │ HTTPS │ LLM │ -│ (Client) │ ────────────────────► │ (Server) │ ─────────────────► │ (Bot) │ ──────► │ │ -│ │ ◄──────────────────── │ │ ◄─────────────────── │ │ ◄────── │ │ -└──────────┘ Sliding Sync └──────────┘ Client-Server API └──────────┘ └─────┘ - Your machine Docker :8128 Docker :8009 External -``` - -**Data flow when you send a message:** - -1. You type a message in Robrix -2. Robrix sends it to Palpo via the Matrix Client-Server API -3. Palpo sees the message is in a room where Octos is present, and pushes the event to Octos via the Appservice API -4. Octos receives the event, calls the configured LLM (e.g., DeepSeek), and gets a response -5. Octos posts the AI reply back through Palpo's Client-Server API -6. Palpo delivers the reply to Robrix, where you see the bot's response - -### Ports and Protocols - -| Connection | Protocol | Default Port | Notes | -|-----------|----------|-------------|-------| -| Robrix → Palpo | Client-Server API (Sliding Sync) | 8128 (host) → 8008 (container) | The only port Robrix needs | -| Palpo → Octos | Appservice API | 8009 (internal Docker network) | Palpo pushes events to Octos | -| Octos → Palpo | Client-Server API | 8008 (internal Docker network) | Octos replies through Palpo | -| Octos Dashboard | HTTP | 8010 (host) → 8080 (container) | Optional admin UI | -| Octos → LLM | HTTPS | 443 (outbound) | External API call | - ---- - -## 2. Quick Start - -Get everything running locally in 4 steps. - -### Prerequisites - -- **Docker** and **Docker Compose** (v2+) -- **Git** -- **An LLM API key** — e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available) -- **Robrix** — [download a pre-built release](https://github.com/Project-Robius-China/robrix2/releases), or build from source with `cargo run --release` - -### Step 1: Get the Example Configuration - -```bash -git clone https://github.com/Project-Robius-China/robrix2.git -cd robrix2/docs/examples -``` - -### Step 2: Run Setup - -```bash -./setup.sh -``` - -This clones the Palpo and Octos source repos and creates your `.env` file. Both are built from source to support all architectures (x86_64, ARM64/Apple Silicon, etc.). - -### Step 3: Set Your API Key - -Edit `.env` and replace `your-api-key-here` with your actual DeepSeek API key: - -``` -DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx -``` - -### Step 4: Start the Services - -```bash -docker compose up -d -``` - -> **Note:** The first run builds both Palpo and Octos from source, which can take **10–30 minutes** depending on your machine and network speed. Palpo compiles its Rust codebase; Octos additionally downloads runtime tools (Node.js, Chromium) for its skill plugins. Subsequent runs use cached images and start in seconds. - -Check that everything is running: - -```bash -docker compose ps -``` - -You should see three services (`palpo_postgres`, `palpo`, `octos`) all in `running` state. - -### Step 5: Connect with Robrix (after build completes) - -1. **Open Robrix** (don't have it yet? See [4.1 Getting Robrix](#41-getting-robrix)) - -2. **Set the homeserver**: In the login screen, enter `http://127.0.0.1:8128` in the **Homeserver URL** field (below the password field). - - - -3. **Register a new account**: Enter a username and password, then click **Sign up**. - - ![Register account — enter username, password, and homeserver URL](images/register-account.png) - -4. **Talk to the AI bot**: After logging in, join a room or create one, then invite the bot: - - Click the invite button in the room - - Enter `@octosbot:127.0.0.1:8128` - - Send a message — the AI bot should reply! - - - -**That's it!** You now have a working Robrix + Palpo + Octos setup. Read on for configuration details, or jump to [Troubleshooting](#6-troubleshooting) if something isn't working. - ---- - -## 3. Configuration Details - -This section explains every configuration file in the `examples/` directory. You already have a working setup from the Quick Start — come here when you want to customize. - -### 3.1 Directory Layout - -``` -examples/ -├── compose.yml # Docker Compose — orchestrates all services -├── .env.example # Environment variables template -├── palpo.toml # Palpo homeserver configuration -├── appservices/ -│ └── octos-registration.yaml # Appservice registration (links Palpo ↔ Octos) -├── config/ -│ ├── botfather.json # Octos bot profile (Matrix channel config) -│ └── octos.json # Octos global settings -├── data/ # Persistent data (created at runtime) -│ ├── pgsql/ # PostgreSQL database files -│ ├── octos/ # Octos runtime data -│ └── media/ # Palpo media storage -└── static/ - └── index.html # Palpo homepage (optional) -``` - -### 3.2 Token Generation - -The Appservice registration and the Octos bot profile share two secret tokens for mutual authentication. The example files come with pre-filled development tokens, but **you must generate new tokens for production**: - -```bash -openssl rand -hex 32 # → use as as_token -openssl rand -hex 32 # → use as hs_token -``` - -These two values must be identical in `appservices/octos-registration.yaml` and `config/botfather.json`. If they don't match, nothing works. See [Token Matching Checklist](#37-token-matching-checklist). - -### 3.3 Appservice Registration (`appservices/octos-registration.yaml`) - -This file tells Palpo about Octos — which user namespaces Octos manages and where to send events. - -```yaml -id: octos-matrix-appservice -url: "http://octos:8009" - -as_token: "" -hs_token: "" - -sender_localpart: octosbot -rate_limited: false - -namespaces: - users: - - exclusive: true - regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" - - exclusive: true - regex: "@octosbot:127\\.0\\.0\\.1:8128" - aliases: [] - rooms: [] -``` - -| Field | Description | -|-------|-------------| -| `id` | A unique identifier for this appservice registration. | -| `url` | Where Palpo sends events. Uses the Docker service name `octos` (not `localhost`), because both containers share the same Docker network. | -| `as_token` | Token that Octos uses when calling Palpo's API. Must match `botfather.json`. | -| `hs_token` | Token that Palpo uses when pushing events to Octos. Must match `botfather.json`. | -| `sender_localpart` | The bot's Matrix local username. Becomes `@octosbot:127.0.0.1:8128`. | -| `rate_limited` | Set to `false` so the bot can respond without rate limits. | -| `namespaces.users` | Regex patterns for user IDs that this appservice owns. Include the bot itself (`@octosbot:...`) and any dynamically-created bot users (`@octosbot_*:...`). | - -### 3.4 Palpo Configuration (`palpo.toml`) - -```toml -server_name = "127.0.0.1:8128" - -allow_registration = true -yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true -enable_admin_room = true - -appservice_registration_dir = "/var/palpo/appservices" - -# HTTP listener (Client-Server API) -[[listeners]] -address = "0.0.0.0:8008" - -[logger] -format = "pretty" - -[db] -url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" -pool_size = 10 - -[well_known] -server = "127.0.0.1:8128" -client = "http://127.0.0.1:8128" -``` - -| Field | Description | -|-------|-------------| -| `server_name` | The domain part of all Matrix IDs (e.g., `@user:127.0.0.1:8128`). For production, use your actual domain. | -| `allow_registration` | Whether new users can register. Set to `true` so Robrix users can create accounts. For production, consider setting to `false` after initial setup. | -| `yes_i_am_very_very_sure_...` | Required safety confirmation when `allow_registration = true`. The intentionally long name reminds you of the security implications. | -| `enable_admin_room` | Enables the server admin room for management. | -| `appservice_registration_dir` | Palpo loads all `.yaml` files from this directory on startup. This is how it discovers Octos. | -| `[[listeners]]` | Network listeners. Each entry defines an address Palpo listens on. | -| `[logger]` | Log format. `"pretty"` for development, `"json"` for production. | -| `[db]` | PostgreSQL connection. `palpo_postgres` is the Docker service name. The password must match `POSTGRES_PASSWORD` in `compose.yml`. | -| `[well_known]` | Used by clients for server discovery. Must match externally-reachable addresses. | - -> **Important:** In this local Docker example, the Matrix identity is `127.0.0.1:8128`, so `server_name`, the appservice regex, and bot user IDs must all use `127.0.0.1:8128`. Only container-to-container traffic should use Docker service names like `palpo:8008` or `octos:8009`. - -> **Further reading:** [Palpo on GitHub](https://github.com/palpo-im/palpo) for advanced configuration (federation, TLS, TURN, etc.). - -### 3.5 Octos Bot Profile (`config/botfather.json`) - -This file defines the bot's identity, LLM provider, and Matrix channel configuration. - -```json -{ - "id": "botfather", - "name": "BotFather", - "enabled": true, - "config": { - "provider": "deepseek", - "model": "deepseek-chat", - "api_key_env": "DEEPSEEK_API_KEY", - "channels": [ - { - "type": "matrix", - "homeserver": "http://palpo:8008", - "as_token": "", - "hs_token": "", - "server_name": "127.0.0.1:8128", - "sender_localpart": "octosbot", - "user_prefix": "octosbot_", - "port": 8009, - "allowed_senders": [] - } - ], - "gateway": { - "max_history": 50, - "queue_mode": "followup" - } - }, - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z" -} -``` - -> **Important:** The `created_at` and `updated_at` fields are **required** by Octos. If they are missing, Octos will skip this profile and the bot will never start. - -**LLM Provider settings:** - -| Field | Description | -|-------|-------------| -| `provider` | LLM provider name. Octos supports `deepseek`, `openai`, `anthropic`, and [12 more](https://octos-org.github.io/octos/). | -| `model` | Model identifier (e.g., `deepseek-chat`, `gpt-4o`, `claude-sonnet-4-20250514`). | -| `api_key_env` | Name of the environment variable holding your API key. | - -**Matrix channel settings:** - -| Field | Description | -|-------|-------------| -| `type` | Must be `"matrix"`. | -| `homeserver` | Palpo's internal URL. Uses Docker service name `palpo`, not `localhost`. | -| `as_token` / `hs_token` | Must match the appservice registration YAML. | -| `server_name` | The Matrix domain. Must match `server_name` in `palpo.toml`. | -| `sender_localpart` | Bot username. Must match the registration file. | -| `user_prefix` | Prefix for dynamically-created bot users (e.g., `octosbot_translator`). | -| `port` | Port Octos listens on for Appservice events from Palpo. | -| `allowed_senders` | Matrix user IDs allowed to talk to the bot. Empty `[]` = everyone can talk to it. | - -> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related, but they are not interchangeable. - -**Gateway settings:** - -| Field | Description | -|-------|-------------| -| `max_history` | Maximum number of messages to include as context for the LLM. | -| `queue_mode` | How Octos handles incoming messages. `followup` queues new messages and processes them sequentially. | - -> **Further reading:** [Octos Book — LLM Providers & Routing](https://octos-org.github.io/octos/) for all 14 supported providers, fallback chains, and adaptive routing. - -### 3.6 Docker Compose (`compose.yml`) - -The provided `compose.yml` starts three services: - -| Service | Image | Exposed Ports | Purpose | -|---------|-------|--------------|---------| -| `palpo_postgres` | `postgres:17` | *(none, internal only)* | Database for Palpo | -| `palpo` | Built from source | `8128:8008` | Matrix homeserver | -| `octos` | Built from source | `8009:8009`, `8010:8080` | AI bot appservice | - -**Port mapping explanation:** - -- `8128` → Robrix connects here (Client-Server API) -- `8009` → Palpo pushes events to Octos here (Appservice API) -- `8010` → Octos admin dashboard (optional, for monitoring) - -**Persistent volumes:** - -| Volume | Purpose | -|--------|---------| -| `./data/pgsql` | PostgreSQL data. Survives `docker compose down`. | -| `./data/octos` | Octos runtime data (sessions, memory). | -| `./data/media` | Media files uploaded through Matrix (images, files). | - -**Environment variables (`.env`):** - -| Variable | Required | Default | Description | -|----------|----------|---------|-------------| -| `DEEPSEEK_API_KEY` | **Yes** | — | Your LLM API key | -| `DB_PASSWORD` | No | `palpo_dev_password` | PostgreSQL password | -| `RUST_LOG` | No | `octos=debug,info` | Log verbosity | - -### 3.7 Token Matching Checklist - -The most common configuration error is a token mismatch. These values **must be identical** across both files: - -| Value | In `octos-registration.yaml` | In `botfather.json` | -|-------|------------------------------|---------------------| -| `as_token` | `as_token: "abc..."` | `"as_token": "abc..."` | -| `hs_token` | `hs_token: "def..."` | `"hs_token": "def..."` | -| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | -| `server_name` | `regex: "@octosbot:127\\.0\\.0\\.1:8128"` | `"server_name": "127.0.0.1:8128"` | - -If any of these don't match, the bot will not respond to messages. Double-check before filing a bug report! - ---- - -## 4. Using Robrix - -This section covers how to use Robrix as a client to connect to your Palpo server and interact with the Octos AI bot. - -### 4.1 Getting Robrix - -**Download a pre-built release (recommended):** - -Download from the [Robrix releases page](https://github.com/Project-Robius-China/robrix2/releases). Available for macOS, Linux, and Windows. - -**Or build from source:** - -1. [Install Rust](https://www.rust-lang.org/tools/install) -2. On Linux, install dependencies: - ```bash - sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev - ``` -3. Build and run: - ```bash - cargo run --release - ``` - -For mobile builds (Android/iOS) and packaging for distribution, see the [Robrix README](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop). - -### 4.2 Connecting to Palpo - -When you launch Robrix, you'll see the login screen: - - - -The **Homeserver URL** field is at the bottom of the login form. It defaults to `matrix.org` if left empty. To connect to your local Palpo instance: - -- **Local deployment:** Enter `http://127.0.0.1:8128` -- **Remote deployment:** Enter `https://your.server.name` (or `http://your-server-ip:8128`) - - - -> **Important:** Robrix requires [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync) support from the homeserver. Palpo supports this natively. - -### 4.3 Registration and Login - -**First time — Register a new account:** - -1. Enter your desired **username** and **password** -2. Enter the password again in the **Confirm password** field (appears for registration) -3. Enter the **Homeserver URL** (e.g., `http://127.0.0.1:8128`) -4. Click **Sign up** - -![Register account — enter username, password, and homeserver URL](images/register-account.png) - -**Returning — Log in:** - -1. Enter your **username** and **password** -2. Enter the **Homeserver URL** -3. Click **Log in** - -After successful login, you'll see your room list (empty if this is a fresh account). - - - -### 4.4 Interacting with the AI Bot - -There are two ways to start chatting with the bot: - -#### Method 1: Invite the bot to a room - -1. Create a new room or open an existing one -2. Click the **invite** button in the room -3. Enter the bot's Matrix ID: `@octosbot:127.0.0.1:8128` (replace `127.0.0.1:8128` with your `server_name`) -4. The bot will automatically join the room - - - -#### Method 2: Join a room where the bot is present - -1. Click the **Join Room** button (or use the room browser) -2. Enter a room alias or ID where the bot has been set up -3. Start chatting - - - -#### Chatting with the bot - -Once the bot is in the room, simply type a message and send it. The bot will process your message through the configured LLM and respond. - - - -### 4.5 Bot Management (Advanced) - -Robrix has built-in support for managing Matrix bots through the BotFather system. - -#### Enabling App Service support - -1. Open **Settings** in Robrix -2. Navigate to **Bot Settings** -3. Toggle **Enable App Service** on -4. Enter the **BotFather User ID** (e.g., `@octosbot:127.0.0.1:8128`) -5. Click **Save** - - - -#### Creating child bots - -With BotFather enabled, you can create specialized child bots: - -1. Use the **Create Bot** dialog -2. Fill in: - - **Username** — lowercase letters, digits, and underscores only (e.g., `translator_bot`) - - **Display Name** — human-readable name (e.g., "Translator Bot") - - **System Prompt** — initial instructions for the bot (e.g., "You are a translator. Translate all messages to English.") -3. Click **Create Bot** - -The bot will be created as `@octosbot_:127.0.0.1:8128`. - - - ---- - -## 5. End-to-End Verification - -After setting up, run through this checklist to confirm everything works: - -### Service Health - -```bash -# Check all containers are running -docker compose ps - -# Check Palpo logs for startup errors -docker compose logs palpo | tail -20 - -# Check Octos logs — look for "appservice listening" or similar -docker compose logs octos | tail -20 - -# Verify Palpo is responding -curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 -``` - -### Client Connectivity - -- [ ] Robrix can connect to `http://127.0.0.1:8128` -- [ ] You can register a new account -- [ ] After login, the room list loads (may be empty) -- [ ] You can create a new room - -### Bot Interaction - -- [ ] You can invite `@octosbot:127.0.0.1:8128` to a room -- [ ] The bot joins the room (check `docker compose logs octos` if it doesn't) -- [ ] Sending a message triggers a response from the bot -- [ ] The response content makes sense (confirms LLM connection works) - -### If something fails - -Check the logs in this order — they follow the data flow: - -```bash -# 1. Is Palpo receiving messages from Robrix? -docker compose logs palpo --since 1m - -# 2. Is Palpo forwarding events to Octos? -docker compose logs palpo --since 1m | grep -i appservice - -# 3. Is Octos receiving and processing events? -docker compose logs octos --since 1m - -# 4. Is Octos successfully calling the LLM? -docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" -``` - ---- - -## 6. Troubleshooting - -### 6.1 Service Startup Issues - -| Symptom | Cause | Fix | -|---------|-------|-----| -| `palpo_postgres` won't start | Port 5432 already in use, or corrupt data | Check `docker compose logs palpo_postgres`. Remove `data/pgsql/` to start fresh. | -| `palpo` build fails | Network issue or missing source | Ensure Docker can reach `github.com`. Check `docker compose logs palpo` for build errors. | -| `palpo` crashes on startup | Bad `palpo.toml` syntax or DB connection failure | Check logs. Ensure `palpo_postgres` is healthy first. Verify DB password matches. | -| `octos` build fails | Missing Dockerfile or network issue | Ensure Docker can reach `github.com`. Alternatively, build Octos locally and update `compose.yml` to use a local image. | -| `octos` starts but logs show errors | Invalid `botfather.json` or missing API key | Check JSON syntax. Verify `DEEPSEEK_API_KEY` is set in `.env`. | - -### 6.2 Robrix Connection Issues - -| Symptom | Cause | Fix | -|---------|-------|-----| -| "Cannot connect to server" | Wrong homeserver URL or Palpo not running | Verify Palpo is running (`docker compose ps`). Confirm URL is `http://127.0.0.1:8128`. | -| Login succeeds but no rooms appear | Normal for a fresh account | Create a new room. Rooms will appear as you join or create them. | -| Registration fails | `allow_registration = false` in `palpo.toml`, or server_name mismatch | Check `palpo.toml`. Ensure `allow_registration = true`. | -| "Homeserver does not support Sliding Sync" | Palpo version too old | Rebuild Palpo: `docker compose build --no-cache palpo`. | -| Connection times out | Firewall blocking port 8128 | Check firewall rules. On macOS, allow incoming connections in System Settings. | - -### 6.3 Bot Issues - -| Symptom | Cause | Fix | -|---------|-------|-----| -| Bot does not respond to messages | Token mismatch between registration and profile | Verify the [Token Matching Checklist](#37-token-matching-checklist). | -| `Connection refused` in Palpo logs | Octos not running, or wrong `url` in registration YAML | Ensure Octos is running. The `url` must use the Docker service name (`http://octos:8009`), not `localhost`. | -| `User ID not in namespace` | `sender_localpart` doesn't match `namespaces.users` regex | Update the regex in `octos-registration.yaml` to include the bot's full user ID pattern. | -| Bot joins room but gives empty replies | LLM API key invalid or quota exceeded | Check `docker compose logs octos` for API errors. Verify your API key and account balance. | -| Messages from some users are ignored | `allowed_senders` filtering in `botfather.json` | Add the user's Matrix ID to the `allowed_senders` array, or set it to `[]` to allow everyone. | - -### 6.4 Useful Debug Commands - -```bash -# View real-time logs for all services -docker compose logs -f - -# View logs for a specific service -docker compose logs -f palpo -docker compose logs -f octos - -# Restart a single service -docker compose restart octos - -# Check Palpo's client API -curl http://127.0.0.1:8128/_matrix/client/versions - -# Full reset (WARNING: deletes all data) -docker compose down -v -rm -rf data/ -docker compose up -d -``` - ---- - -## 7. Further Reading - -- **Octos Documentation (full):** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — covers all LLM providers, channels (Telegram, Slack, Discord, etc.), skills, memory system, and advanced configuration. -- **Octos Matrix Appservice Guide:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — the original guide this document is based on, with additional context. -- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo homeserver documentation. -- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix client, build instructions, and feature tracker. -- **Matrix Appservice Spec:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — the Matrix protocol specification for application services. - ---- - -*This guide covers the deployment as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/examples/static/index.html b/docs/examples/static/index.html deleted file mode 100644 index a71080f17..000000000 --- a/docs/examples/static/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - -Palpo Matrix Homeserver - -

Palpo Matrix Homeserver

-
-

Connect with Robrix or any Matrix client.

- - diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md new file mode 100644 index 000000000..70d7fa016 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md @@ -0,0 +1,500 @@ +# 部署指南:Robrix + Palpo + Octos + +[English Version](01-deploying-palpo-and-octos.md) + +> **目标:** 按照本指南操作后,你的机器上将通过 Docker Compose 运行 Palpo(Matrix 主服务器)、Octos(AI 机器人)和 PostgreSQL。Robrix 将能连接到你的 Palpo 服务器,你可以与 Octos AI 机器人对话。 + +本指南带你一步步部署后端服务:从克隆源码,到配置各组件,再到验证一切正常运行。 + +> **只想快速试试?** 跳到 [快速开始](#2-快速开始) — 5 步即可运行。 +> +> **想了解每个配置背后的原理?** 参阅 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md) 了解完整解释。 + +--- + +## 目录 + +1. [前提条件](#1-前提条件) +2. [快速开始](#2-快速开始) +3. [配置详解](#3-配置详解) +4. [端到端验证](#4-端到端验证) +5. [故障排除](#5-故障排除) +6. [延伸阅读](#6-延伸阅读) + +--- + +## 1. 前提条件 + +开始之前,请确保你已具备以下条件: + +| 需求 | 版本 | 备注 | +|------|------|------| +| **Docker** + **Docker Compose** | v2+ | 运行 `docker compose version` 检查。Docker Desktop 自带 Compose v2。 | +| **Git** | 任意 | 用于克隆源码仓库。 | +| **一个 LLM API Key** | -- | 如 [DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等。 | +| **Robrix** | 最新版 | 参阅 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) 了解下载或构建方式。 | + +> **注意:** Palpo 和 Octos 都在 Docker 内从源码构建。你不需要在宿主机上安装 Rust 或任何其他工具链。 + +--- + +## 2. 快速开始 + +5 步在本地跑通所有服务。 + +### 步骤 1:克隆仓库 + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/palpo-and-octos-deploy +``` + +### 步骤 2:运行初始化脚本 + +```bash +./setup.sh +``` + +此脚本会: +- 将 Palpo 源码仓库克隆到 `repos/palpo/`(从 GitHub 浅克隆) +- 将 Octos 源码仓库克隆到 `repos/octos/`(从 GitHub 浅克隆) +- 从 `.env.example` 创建 `.env` 文件 + +两个服务均在 Docker 内从源码构建,以支持所有架构(x86_64、ARM64/Apple Silicon 等)。 + +> **文件存放位置说明:** 运行 `setup.sh` 和 `docker compose up` 后,`palpo-and-octos-deploy/` 目录会包含: +> - `repos/` — Palpo 和 Octos 的源码(Docker 用来构建镜像) +> - `data/` — 运行时数据(PostgreSQL 数据库、Octos 会话、媒体文件) +> - `.env` — 你的环境变量(API Key 等) +> +> 这些目录已在 `.gitignore` 中列出,**不会**被提交到版本库。 + +### 步骤 3:设置 API Key + +编辑 `.env`,将 `your-api-key-here` 替换为你的实际 API Key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 步骤 4:启动服务 + +```bash +docker compose up -d +``` + +> **重要:** 首次运行会从源码编译 Palpo 和 Octos,这可能需要 **10--30 分钟**(取决于你的机器性能和网络速度)。Palpo 需要编译其 Rust 代码;Octos 还额外需要下载 Node.js、Chromium 等技能插件的运行时工具。后续启动会使用缓存镜像,几秒内完成。 + +检查运行状态: + +```bash +docker compose ps +``` + +你应该看到三个服务(`palpo_postgres`、`palpo`、`octos`)都处于 `running` 状态。 + +### 步骤 5:用 Robrix 连接 + +1. **打开 Robrix**(还没有?参阅 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md)) + +2. **设置服务器地址**:在登录界面,在 **Homeserver URL** 输入框中输入 `http://127.0.0.1:8128` + +3. **注册新账号**:输入用户名和密码,点击 **Sign up** + +4. **与 AI 机器人对话**:登录后,创建一个房间并邀请机器人: + - 点击房间中的邀请按钮 + - 输入 `@octosbot:127.0.0.1:8128` + - 等待机器人加入房间(你应该能看到加入事件) + - 发送一条消息——AI 机器人应该会回复! + +**完成!** 你现在拥有了一个可工作的 Robrix + Palpo + Octos 系统。继续阅读了解配置详情,或跳到 [故障排除](#5-故障排除) 解决问题。 + +--- + +## 3. 配置详解 + +本节解释 `palpo-and-octos-deploy/` 目录中的每个配置文件。快速开始已经让你跑起来了——当你需要自定义时再来这里查阅。 + +> **注意:** 想了解架构以及每个组件为何如此配置,请参阅 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md)。 + +### 3.1 目录结构 + +``` +palpo-and-octos-deploy/ +├── compose.yml # Docker Compose — 编排所有服务 +├── setup.sh # 一次性初始化脚本 +├── .env.example # 环境变量模板 +├── palpo.toml # Palpo 主服务器配置 +├── palpo.Dockerfile # Palpo 构建指令(多架构支持) +├── appservices/ +│ └── octos-registration.yaml # 应用服务注册文件(连接 Palpo <-> Octos) +├── config/ +│ ├── botfather.json # Octos 机器人配置(LLM + Matrix 通道) +│ └── octos.json # Octos 全局设置 +├── repos/ # 源码(由 setup.sh 创建,已 gitignore) +│ ├── palpo/ # Palpo 主服务器源码 +│ └── octos/ # Octos 机器人源码 +├── data/ # 持久化数据(运行时自动创建,已 gitignore) +│ ├── pgsql/ # PostgreSQL 数据库文件 +│ ├── octos/ # Octos 运行时数据 +│ └── media/ # Palpo 媒体存储 +``` + +### 3.2 令牌生成 + +应用服务注册文件和 Octos 机器人配置共享两个密钥令牌,用于双向认证。示例文件中已预填开发用令牌,但**在生产环境中你必须重新生成**: + +```bash +openssl rand -hex 32 # → 用作 as_token +openssl rand -hex 32 # → 用作 hs_token +``` + +这两个值必须在 `palpo-and-octos-deploy/appservices/octos-registration.yaml` 和 `palpo-and-octos-deploy/config/botfather.json` 中完全一致。如果不匹配,机器人将无法工作。详见 [3.8 令牌匹配检查清单](#38-令牌匹配检查清单)。 + +### 3.3 应用服务注册文件(`appservices/octos-registration.yaml`) + +此文件告诉 Palpo 关于 Octos 的信息——Octos 管理哪些用户命名空间,以及将事件发送到哪里。 + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4" +hs_token: "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| 字段 | 说明 | +|------|------| +| `id` | 此应用服务注册的唯一标识符。 | +| `url` | Palpo 发送事件的目标地址。使用 Docker 服务名 `octos`(不是 `localhost`),因为两个容器在同一个 Docker 网络中。 | +| `as_token` | Octos 调用 Palpo API 时使用的令牌。**必须**与 `botfather.json` 匹配。 | +| `hs_token` | Palpo 向 Octos 推送事件时使用的令牌。**必须**与 `botfather.json` 匹配。 | +| `sender_localpart` | 机器人的 Matrix 本地用户名。最终变为 `@octosbot:127.0.0.1:8128`。 | +| `rate_limited` | 设为 `false`,让机器人回复不受速率限制。 | +| `namespaces.users` | 此应用服务管理的用户 ID 正则匹配模式。包含机器人本身(`@octosbot:...`)和动态创建的子机器人(`@octosbot_*:...`)。 | + +### 3.4 Palpo 配置(`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP 监听器(Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| 字段 | 说明 | +|------|------| +| `server_name` | 所有 Matrix ID 的域名部分(如 `@user:127.0.0.1:8128`)。 | +| `allow_registration` | 是否允许新用户注册。设为 `true` 以便 Robrix 用户创建账号。 | +| `yes_i_am_very_very_sure_...` | 当 `allow_registration = true` 时必填的安全确认。 | +| `enable_admin_room` | 启用服务器管理员房间。 | +| `appservice_registration_dir` | Palpo 启动时自动加载此目录下所有 `.yaml` 文件。Octos 就是通过这种方式被发现的。 | +| `[[listeners]]` | 网络监听器。每个条目定义一个 Palpo 监听的地址。 | +| `[logger]` | 日志格式。`"pretty"` 用于开发,`"json"` 用于生产。 | +| `[db]` | PostgreSQL 连接配置。`palpo_postgres` 是 Docker 服务名。密码必须与 `compose.yml` 中的 `POSTGRES_PASSWORD` 匹配。 | +| `[well_known]` | 用于客户端发现服务器。必须与外部可访问的地址匹配。 | + +> **注意:** `server_name` 值 `"127.0.0.1:8128"` 仅用于本地开发。生产环境部署时,请替换为你的实际域名(如 `"chat.example.com"`)。更改 `server_name` 时,你还需要同步更新 `octos-registration.yaml`(正则表达式部分)和 `botfather.json`(`server_name` 字段)。 + +> **重要:** 在这个本地 Docker 示例里,Matrix 身份统一使用 `127.0.0.1:8128`。因此 `server_name`、应用服务正则和机器人用户 ID 都必须写成 `127.0.0.1:8128`。只有容器之间通信时才使用 `palpo:8008`、`octos:8009` 这类 Docker 服务名。 + +### 3.5 Octos 机器人配置(`config/botfather.json`) + +此文件定义机器人的身份、LLM 提供商和 Matrix 通道配置。 + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4", + "hs_token": "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **重要:** `created_at` 和 `updated_at` 字段是 Octos **必需的**。如果缺少这两个字段,Octos 会跳过该 profile,机器人将无法启动。 + +**LLM 提供商设置:** + +| 字段 | 说明 | +|------|------| +| `provider` | LLM 提供商名称。Octos 支持 `deepseek`、`openai`、`anthropic` 等[多种提供商](https://octos-org.github.io/octos/)。 | +| `model` | 模型标识符(如 `deepseek-chat`、`gpt-4o`、`claude-sonnet-4-20250514`)。 | +| `api_key_env` | 存放 API Key 的环境变量名称。 | + +**Matrix 通道设置:** + +| 字段 | 说明 | +|------|------| +| `type` | 必须为 `"matrix"`。 | +| `homeserver` | Palpo 的内部 URL。使用 Docker 服务名 `palpo`,不是 `localhost`。 | +| `as_token` / `hs_token` | 必须与应用服务注册 YAML 文件匹配。 | +| `server_name` | Matrix 域名。必须与 `palpo.toml` 中的 `server_name` 一致。 | +| `sender_localpart` | 机器人用户名。必须与注册文件一致。 | +| `user_prefix` | 动态创建的子机器人用户 ID 前缀(如 `octosbot_translator`)。 | +| `port` | Octos 监听 Palpo 应用服务事件的端口。 | +| `allowed_senders` | 允许与机器人对话的 Matrix 用户 ID。空数组 `[]` = 所有人都可以对话。 | + +> **重要:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关但不能混用。详见 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md)。 + +**Gateway 设置:** + +| 字段 | 说明 | +|------|------| +| `max_history` | 作为 LLM 上下文发送的最大历史消息数量。 | +| `queue_mode` | Octos 处理传入消息的方式。`followup` 将新消息排队并顺序处理。 | + +**切换 LLM 提供商(以 OpenAI 替代 DeepSeek 为例):** + +1. 在 `botfather.json` 中修改:`"provider": "openai"`、`"model": "gpt-4o"`、`"api_key_env": "OPENAI_API_KEY"` +2. 在 `.env` 中修改:`OPENAI_API_KEY=sk-xxxxxxxx` +3. 在 `compose.yml` 的 `octos` 服务 `environment` 中添加:`OPENAI_API_KEY: ${OPENAI_API_KEY}` + +Octos 支持 14+ 种提供商——完整列表见 [Octos Book](https://octos-org.github.io/octos/)。 + +### 3.6 Octos 全局设置(`config/octos.json`) + +此文件配置 Octos 的核心运行路径和日志级别。 + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +| 字段 | 说明 | +|------|------| +| `profiles_dir` | Octos 加载机器人配置文件(如 `botfather.json`)的目录。通过 Docker 卷映射自 `./config/`。 | +| `data_dir` | Octos 运行时数据(会话、记忆)的根目录。映射自 `./data/octos/`。 | +| `log_level` | Octos 日志详细程度。开发环境用 `debug`,生产环境用 `info`。 | + +> **注意:** 这些是容器内部路径。`compose.yml` 中的 Docker 卷映射会将它们连接到宿主机目录。 + +### 3.7 Docker Compose(`compose.yml`) + +提供的 `compose.yml` 启动三个服务: + +| 服务 | 镜像 | 暴露端口 | 用途 | +|------|------|----------|------| +| `palpo_postgres` | `postgres:17` | *(无,仅内部)* | Palpo 的数据库 | +| `palpo` | 从源码构建 | `8128:8008` | Matrix 主服务器 | +| `octos` | 从源码构建 | `8009:8009`、`8010:8080` | AI 机器人应用服务 | + +**端口映射说明:** + +- `8128` — Robrix 连接此端口(Client-Server API) +- `8009` — Palpo 向 Octos 推送事件(Appservice API,同时暴露到宿主机供调试) +- `8010` — Octos 管理控制面板(可选,用于监控) + +**持久化卷:** + +| 卷 | 用途 | +|----|------| +| `./data/pgsql` | PostgreSQL 数据。`docker compose down` 后保留。 | +| `./data/octos` | Octos 运行时数据(会话、记忆)。 | +| `./data/media` | 通过 Matrix 上传的媒体文件(图片、文件)。 | + +**环境变量(`.env`):** + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `DEEPSEEK_API_KEY` | **是** | -- | 你的 LLM API Key | +| `DB_PASSWORD` | 否 | `palpo_dev_password` | PostgreSQL 密码 | +| `RUST_LOG` | 否 | `octos=debug,info` | 日志详细程度 | + +### 3.8 令牌匹配检查清单 + +最常见的配置错误是令牌不匹配。以下值在两个文件中**必须完全一致**: + +| 值 | 在 `octos-registration.yaml` 中 | 在 `botfather.json` 中 | +|----|----------------------------------|------------------------| +| `as_token` | `as_token: "d1f4..."` | `"as_token": "d1f4..."` | +| `hs_token` | `hs_token: "e2a5..."` | `"hs_token": "e2a5..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | regex: `@octosbot:127\\.0\\.0\\.1:8128` | `"server_name": "127.0.0.1:8128"` | + +如果有任何不匹配,机器人将不会响应消息。提交 bug 报告前请先检查! + +--- + +## 4. 端到端验证 + +部署完成后,按照以下检查清单确认一切正常。 + +### 服务健康检查 + +```bash +# 检查所有容器是否运行 +docker compose ps + +# 检查 Palpo 日志是否有启动错误 +docker compose logs palpo | tail -20 + +# 检查 Octos 日志——寻找 "appservice listening" 或类似信息 +docker compose logs octos | tail -20 + +# 验证 Palpo 是否响应 Matrix API +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### 客户端连接检查清单 + +- [ ] Robrix 能连接到 `http://127.0.0.1:8128` +- [ ] 能注册新账号 +- [ ] 登录后房间列表能加载(新账号可能为空) +- [ ] 能创建新房间 + +### 机器人交互检查清单 + +- [ ] 能邀请 `@octosbot:127.0.0.1:8128` 到房间 +- [ ] 机器人加入房间(如果没有,检查 `docker compose logs octos`) +- [ ] 发送消息后机器人回复 +- [ ] 回复内容合理(确认 LLM 连接正常) + +### 日志检查顺序(跟随数据流) + +如果某步失败,按照数据在系统中流动的顺序检查日志: + +```bash +# 1. Palpo 是否收到了 Robrix 的消息? +docker compose logs palpo --since 1m + +# 2. Palpo 是否将事件转发给了 Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Octos 是否收到并处理了事件? +docker compose logs octos --since 1m + +# 4. Octos 是否成功调用了 LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 5. 故障排除 + +### 5.1 服务启动问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| `palpo_postgres` 无法启动 | 端口 5432 已被占用,或数据损坏 | 检查 `docker compose logs palpo_postgres`。删除 `data/pgsql/` 重新开始。 | +| `palpo` 构建失败 | 网络问题或源码获取失败 | 确保 Docker 能访问 `github.com`。检查 `docker compose logs palpo` 查看构建错误。 | +| `palpo` 启动时崩溃 | `palpo.toml` 语法错误或数据库连接失败 | 检查日志。确保 `palpo_postgres` 先正常运行。验证数据库密码一致。 | +| `octos` 构建失败 | 缺少 Dockerfile 或网络问题 | 确保 Docker 能访问 `github.com`。运行 `./setup.sh` 确认仓库已克隆。 | +| `octos` 启动但日志有错误 | `botfather.json` 无效或缺少 API Key | 检查 JSON 语法。验证 `.env` 中已设置 `DEEPSEEK_API_KEY`。 | + +### 5.2 Robrix 连接问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| "无法连接到服务器" | Homeserver URL 错误或 Palpo 未运行 | 确认 Palpo 正在运行(`docker compose ps`)。确认 URL 为 `http://127.0.0.1:8128`。 | +| 登录成功但没有房间 | 新账号的正常现象 | 创建一个新房间。加入或创建后房间会出现在列表中。 | +| 注册失败 | `palpo.toml` 中 `allow_registration = false` | 检查 `palpo.toml`。确保 `allow_registration = true`。 | +| "Homeserver 不支持 Sliding Sync" | Palpo 版本过旧 | 重新构建 Palpo:`docker compose build --no-cache palpo`。 | +| 连接超时 | 防火墙阻止了端口 8128 | 检查防火墙规则。macOS 上在系统设置中允许传入连接。 | + +### 5.3 机器人问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| 机器人不响应消息 | 注册文件和配置文件之间令牌不匹配 | 验证 [令牌匹配检查清单](#38-令牌匹配检查清单)。 | +| Palpo 日志中出现 `Connection refused` | Octos 未运行,或注册 YAML 中 `url` 错误 | 确保 Octos 正在运行。`url` 必须使用 Docker 服务名(`http://octos:8009`),不能用 `localhost`。 | +| `User ID not in namespace` | `sender_localpart` 与 `namespaces.users` 正则不匹配 | 更新 `octos-registration.yaml` 中的正则表达式,包含机器人的完整用户 ID 模式。 | +| 机器人加入房间但回复空消息 | LLM API Key 无效或额度不足 | 检查 `docker compose logs octos` 中的 API 错误。验证 API Key 和账户余额。 | +| 部分用户的消息被忽略 | `botfather.json` 中的 `allowed_senders` 过滤 | 设 `allowed_senders` 为 `[]` 允许所有人,或添加用户的 Matrix ID。 | +| 机器人配置未加载 | `botfather.json` 缺少 `created_at` / `updated_at` | 这两个字段是必需的。按 [3.5 节](#35-octos-机器人配置configbotfatherjson) 示例添加。 | + +### 5.4 常用调试命令 + +```bash +# 实时查看所有服务日志 +docker compose logs -f + +# 查看特定服务的日志 +docker compose logs -f palpo +docker compose logs -f octos + +# 重启单个服务(如修改 botfather.json 后) +docker compose restart octos + +# 重新构建单个服务(如更新源码后) +docker compose build --no-cache palpo +docker compose up -d palpo + +# 检查 Palpo 的 Client-Server API +curl http://127.0.0.1:8128/_matrix/client/versions + +# 完全重置(警告:删除所有数据,包括账号和消息) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +--- + +## 6. 延伸阅读 + +- **Octos 完整文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — 覆盖所有 LLM 提供商、通道、技能、记忆系统和高级配置。 +- **Octos Matrix Appservice 指南:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — 本文档参考的原始 Palpo + Octos 集成指南。 +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo 主服务器文档。 +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix 客户端、构建说明和功能追踪。 +- **Matrix Appservice 规范:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — 应用服务的 Matrix 协议规范。 +- **架构原理:** [02-how-robrix-palpo-octos-work-together-zh.md](02-how-robrix-palpo-octos-work-together-zh.md) — 应用服务机制如何运作、消息生命周期和 BotFather 系统。 + +--- + +*本指南内容截至 2026 年 4 月。最新更新请查看各项目的仓库。* diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md new file mode 100644 index 000000000..62521ae1f --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md @@ -0,0 +1,500 @@ +# Deployment Guide: Robrix + Palpo + Octos + +[中文版](01-deploying-palpo-and-octos-zh.md) + +> **Goal:** After following this guide, you will have Palpo (Matrix homeserver), Octos (AI bot), and PostgreSQL running via Docker Compose on your machine. Robrix will be able to connect to your Palpo server, and you can chat with the Octos AI bot. + +This guide walks you through deploying the backend services step by step: from cloning the source code, to configuring each component, to verifying everything works end-to-end. + +> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start) -- 5 steps to get running. +> +> **Want to understand WHY things are configured this way?** See [Architecture](02-how-robrix-palpo-octos-work-together.md) for the full explanation. + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Quick Start](#2-quick-start) +3. [Configuration Details](#3-configuration-details) +4. [End-to-End Verification](#4-end-to-end-verification) +5. [Troubleshooting](#5-troubleshooting) +6. [Further Reading](#6-further-reading) + +--- + +## 1. Prerequisites + +Before starting, make sure you have: + +| Requirement | Version | Notes | +|-------------|---------|-------| +| **Docker** + **Docker Compose** | v2+ | `docker compose version` to check. Docker Desktop includes Compose v2. | +| **Git** | Any | For cloning source repos. | +| **An LLM API key** | -- | e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | +| **Robrix** | Latest | See [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) for download or build instructions. | + +> **Note:** Palpo and Octos are both built from source inside Docker. You do not need to install Rust or any other toolchain on your host machine. + +--- + +## 2. Quick Start + +Get everything running locally in 5 steps. + +### Step 1: Clone the Repo + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/palpo-and-octos-deploy +``` + +### Step 2: Run the Setup Script + +```bash +./setup.sh +``` + +This script: +- Clones the Palpo source repo into `repos/palpo/` (shallow clone from GitHub) +- Clones the Octos source repo into `repos/octos/` (shallow clone from GitHub) +- Creates your `.env` file from `.env.example` + +Both services are built from source inside Docker to support all architectures (x86_64, ARM64/Apple Silicon, etc.). + +> **Where do files go?** After running `setup.sh` and `docker compose up`, the `palpo-and-octos-deploy/` directory will contain: +> - `repos/` — Palpo and Octos source code (used by Docker to build images) +> - `data/` — runtime data (PostgreSQL database, Octos sessions, media files) +> - `.env` — your environment variables (API key, etc.) +> +> These directories are listed in `.gitignore` and will **not** be committed to the repository. + +### Step 3: Set Your API Key + +Edit `.env` and replace `your-api-key-here` with your actual API key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +### Step 4: Start the Services + +```bash +docker compose up -d +``` + +> **Important:** The first run builds both Palpo and Octos from source, which can take **10--30 minutes** depending on your machine and network speed. Palpo compiles its Rust codebase; Octos additionally downloads runtime tools (Node.js, Chromium) for its skill plugins. Subsequent runs use cached images and start in seconds. + +Check that everything is running: + +```bash +docker compose ps +``` + +You should see three services (`palpo_postgres`, `palpo`, `octos`) all in `running` state. + +### Step 5: Connect with Robrix + +1. **Open Robrix** (see [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) if you don't have it yet) + +2. **Set the homeserver**: In the login screen, enter `http://127.0.0.1:8128` in the **Homeserver URL** field + +3. **Register a new account**: Enter a username and password, then click **Sign up** + +4. **Talk to the AI bot**: After logging in, create a room and invite the bot: + - Click the invite button in the room + - Enter `@octosbot:127.0.0.1:8128` + - Wait a moment for the bot to join the room (you should see a join event) + - Send a message -- the AI bot should reply! + +**That's it!** You now have a working Robrix + Palpo + Octos setup. Read on for configuration details, or jump to [Troubleshooting](#5-troubleshooting) if something isn't working. + +--- + +## 3. Configuration Details + +This section explains every configuration file in the `palpo-and-octos-deploy/` directory. You already have a working setup from the Quick Start -- come here when you want to customize. + +> **Note:** To understand the architecture and WHY each component is configured this way, see [Architecture](02-how-robrix-palpo-octos-work-together.md). + +### 3.1 Directory Layout + +``` +palpo-and-octos-deploy/ +├── compose.yml # Docker Compose -- orchestrates all services +├── setup.sh # One-time setup script +├── .env.example # Environment variables template +├── palpo.toml # Palpo homeserver configuration +├── palpo.Dockerfile # Palpo build instructions (multi-arch) +├── appservices/ +│ └── octos-registration.yaml # Appservice registration (links Palpo <-> Octos) +├── config/ +│ ├── botfather.json # Octos bot profile (LLM + Matrix channel config) +│ └── octos.json # Octos global settings +├── repos/ # Source code (created by setup.sh, gitignored) +│ ├── palpo/ # Palpo homeserver source +│ └── octos/ # Octos bot source +├── data/ # Persistent data (created at runtime, gitignored) +│ ├── pgsql/ # PostgreSQL database files +│ ├── octos/ # Octos runtime data +│ └── media/ # Palpo media storage +``` + +### 3.2 Token Generation + +The Appservice registration and the Octos bot profile share two secret tokens for mutual authentication. The example files come with pre-filled development tokens, but **you must generate new tokens for production**: + +```bash +openssl rand -hex 32 # -> use as as_token +openssl rand -hex 32 # -> use as hs_token +``` + +These two values must be identical in `palpo-and-octos-deploy/appservices/octos-registration.yaml` and `palpo-and-octos-deploy/config/botfather.json`. If they don't match, the bot will not work. See [3.8 Token Matching Checklist](#38-token-matching-checklist). + +### 3.3 Appservice Registration (`appservices/octos-registration.yaml`) + +This file tells Palpo about Octos -- which user namespaces Octos manages and where to send events. + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4" +hs_token: "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| Field | Description | +|-------|-------------| +| `id` | A unique identifier for this appservice registration. | +| `url` | Where Palpo sends events. Uses the Docker service name `octos` (not `localhost`), because both containers share the same Docker network. | +| `as_token` | Token that Octos uses when calling Palpo's API. **Must match** `botfather.json`. | +| `hs_token` | Token that Palpo uses when pushing events to Octos. **Must match** `botfather.json`. | +| `sender_localpart` | The bot's Matrix local username. Becomes `@octosbot:127.0.0.1:8128`. | +| `rate_limited` | Set to `false` so the bot can respond without rate limits. | +| `namespaces.users` | Regex patterns for user IDs that this appservice owns. Include the bot itself (`@octosbot:...`) and any dynamically-created bot users (`@octosbot_*:...`). | + +### 3.4 Palpo Configuration (`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP listener (Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| Field | Description | +|-------|-------------| +| `server_name` | The domain part of all Matrix IDs (e.g., `@user:127.0.0.1:8128`). | +| `allow_registration` | Whether new users can register. Set to `true` for Robrix users to create accounts. | +| `yes_i_am_very_very_sure_...` | Required safety confirmation when `allow_registration = true`. | +| `enable_admin_room` | Enables the server admin room for management. | +| `appservice_registration_dir` | Palpo loads all `.yaml` files from this directory on startup. This is how it discovers Octos. | +| `[[listeners]]` | Network listeners. Each entry defines an address Palpo listens on. | +| `[logger]` | Log format. `"pretty"` for development, `"json"` for production. | +| `[db]` | PostgreSQL connection. `palpo_postgres` is the Docker service name. The password must match `POSTGRES_PASSWORD` in `compose.yml`. | +| `[well_known]` | Used by clients for server discovery. Must match externally-reachable addresses. | + +> **Note:** The `server_name` `"127.0.0.1:8128"` is for local development only. For production deployment, replace it with your actual domain name (e.g., `"chat.example.com"`). When you change `server_name`, you must also update it in `octos-registration.yaml` (the regex patterns) and `botfather.json` (`server_name` field). + +> **Important:** In this local Docker setup, the Matrix identity is `127.0.0.1:8128`, so `server_name`, the appservice regex, and bot user IDs must all use `127.0.0.1:8128`. Only container-to-container traffic uses Docker service names like `palpo:8008` or `octos:8009`. + +### 3.5 Octos Bot Profile (`config/botfather.json`) + +This file defines the bot's identity, LLM provider, and Matrix channel configuration. + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4", + "hs_token": "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **Important:** The `created_at` and `updated_at` fields are **required** by Octos. If they are missing, Octos will skip this profile and the bot will never start. + +**LLM Provider settings:** + +| Field | Description | +|-------|-------------| +| `provider` | LLM provider name. Octos supports `deepseek`, `openai`, `anthropic`, and [more](https://octos-org.github.io/octos/). | +| `model` | Model identifier (e.g., `deepseek-chat`, `gpt-4o`, `claude-sonnet-4-20250514`). | +| `api_key_env` | Name of the environment variable holding your API key. | + +**Matrix channel settings:** + +| Field | Description | +|-------|-------------| +| `type` | Must be `"matrix"`. | +| `homeserver` | Palpo's internal URL. Uses Docker service name `palpo`, not `localhost`. | +| `as_token` / `hs_token` | Must match the appservice registration YAML. | +| `server_name` | The Matrix domain. Must match `server_name` in `palpo.toml`. | +| `sender_localpart` | Bot username. Must match the registration file. | +| `user_prefix` | Prefix for dynamically-created bot users (e.g., `octosbot_translator`). | +| `port` | Port Octos listens on for Appservice events from Palpo. | +| `allowed_senders` | Matrix user IDs allowed to talk to the bot. Empty `[]` = everyone. | + +> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related but not interchangeable. See [Architecture](02-how-robrix-palpo-octos-work-together.md) for why. + +**Gateway settings:** + +| Field | Description | +|-------|-------------| +| `max_history` | Maximum number of messages to include as context for the LLM. | +| `queue_mode` | How Octos handles incoming messages. `followup` queues new messages and processes them sequentially. | + +**Switching LLM Provider (example: OpenAI instead of DeepSeek):** + +1. In `botfather.json`, change: `"provider": "openai"`, `"model": "gpt-4o"`, `"api_key_env": "OPENAI_API_KEY"` +2. In `.env`, change: `OPENAI_API_KEY=sk-xxxxxxxx` +3. In `compose.yml`, add to the `octos` service's `environment`: `OPENAI_API_KEY: ${OPENAI_API_KEY}` + +Octos supports 14+ providers — see [Octos Book](https://octos-org.github.io/octos/) for the full list. + +### 3.6 Octos Global Settings (`config/octos.json`) + +This file configures Octos's core runtime paths and logging. + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +| Field | Description | +|-------|-------------| +| `profiles_dir` | Directory where Octos loads bot profiles (like `botfather.json`). Mapped via Docker volume from `./config/`. | +| `data_dir` | Root directory for Octos runtime data (sessions, memory). Mapped from `./data/octos/`. | +| `log_level` | Octos log verbosity. Use `debug` for development, `info` for production. | + +> **Note:** These are container-internal paths. The Docker volume mappings in `compose.yml` connect them to the host directories. + +### 3.7 Docker Compose (`compose.yml`) + +The provided `compose.yml` starts three services: + +| Service | Image | Exposed Ports | Purpose | +|---------|-------|--------------|---------| +| `palpo_postgres` | `postgres:17` | *(none, internal only)* | Database for Palpo | +| `palpo` | Built from source | `8128:8008` | Matrix homeserver | +| `octos` | Built from source | `8009:8009`, `8010:8080` | AI bot appservice | + +**Port mapping explanation:** + +- `8128` -- Robrix connects here (Client-Server API) +- `8009` -- Palpo pushes events to Octos here (Appservice API, also exposed to host for debugging) +- `8010` -- Octos admin dashboard (optional, for monitoring) + +**Persistent volumes:** + +| Volume | Purpose | +|--------|---------| +| `./data/pgsql` | PostgreSQL data. Survives `docker compose down`. | +| `./data/octos` | Octos runtime data (sessions, memory). | +| `./data/media` | Media files uploaded through Matrix (images, files). | + +**Environment variables (`.env`):** + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DEEPSEEK_API_KEY` | **Yes** | -- | Your LLM API key | +| `DB_PASSWORD` | No | `palpo_dev_password` | PostgreSQL password | +| `RUST_LOG` | No | `octos=debug,info` | Log verbosity | + +### 3.8 Token Matching Checklist + +The most common configuration error is a token mismatch. These values **must be identical** across files: + +| Value | In `octos-registration.yaml` | In `botfather.json` | +|-------|------------------------------|---------------------| +| `as_token` | `as_token: "d1f4..."` | `"as_token": "d1f4..."` | +| `hs_token` | `hs_token: "e2a5..."` | `"hs_token": "e2a5..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | regex: `@octosbot:127\\.0\\.0\\.1:8128` | `"server_name": "127.0.0.1:8128"` | + +If any of these don't match, the bot will not respond to messages. Double-check before filing a bug report! + +--- + +## 4. End-to-End Verification + +After setting up, run through this checklist to confirm everything works. + +### Service Health Check + +```bash +# Check all containers are running +docker compose ps + +# Check Palpo logs for startup errors +docker compose logs palpo | tail -20 + +# Check Octos logs -- look for "appservice listening" or similar +docker compose logs octos | tail -20 + +# Verify Palpo is responding to the Matrix API +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### Client Connectivity Checklist + +- [ ] Robrix can connect to `http://127.0.0.1:8128` +- [ ] You can register a new account +- [ ] After login, the room list loads (may be empty for a fresh account) +- [ ] You can create a new room + +### Bot Interaction Checklist + +- [ ] You can invite `@octosbot:127.0.0.1:8128` to a room +- [ ] The bot joins the room (check `docker compose logs octos` if it doesn't) +- [ ] Sending a message triggers a response from the bot +- [ ] The response content makes sense (confirms LLM connection works) + +### Log Checking Order (Follow the Data Flow) + +If something fails, check the logs in the order that data flows through the system: + +```bash +# 1. Is Palpo receiving messages from Robrix? +docker compose logs palpo --since 1m + +# 2. Is Palpo forwarding events to Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Is Octos receiving and processing events? +docker compose logs octos --since 1m + +# 4. Is Octos successfully calling the LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 5. Troubleshooting + +### 5.1 Service Startup Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `palpo_postgres` won't start | Port 5432 already in use, or corrupt data | Check `docker compose logs palpo_postgres`. Remove `data/pgsql/` to start fresh. | +| `palpo` build fails | Network issue or missing source | Ensure Docker can reach `github.com`. Check `docker compose logs palpo` for build errors. | +| `palpo` crashes on startup | Bad `palpo.toml` syntax or DB connection failure | Check logs. Ensure `palpo_postgres` is healthy first. Verify DB password matches. | +| `octos` build fails | Missing Dockerfile or network issue | Ensure Docker can reach `github.com`. Run `./setup.sh` to verify repos are cloned. | +| `octos` starts but logs show errors | Invalid `botfather.json` or missing API key | Check JSON syntax. Verify `DEEPSEEK_API_KEY` is set in `.env`. | + +### 5.2 Robrix Connection Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Cannot connect to server" | Wrong homeserver URL or Palpo not running | Verify Palpo is running (`docker compose ps`). Confirm URL is `http://127.0.0.1:8128`. | +| Login succeeds but no rooms appear | Normal for a fresh account | Create a new room. Rooms appear as you join or create them. | +| Registration fails | `allow_registration = false` in `palpo.toml` | Check `palpo.toml`. Ensure `allow_registration = true`. | +| "Homeserver does not support Sliding Sync" | Palpo version too old | Rebuild Palpo: `docker compose build --no-cache palpo`. | +| Connection times out | Firewall blocking port 8128 | Check firewall rules. On macOS, allow incoming connections in System Settings. | + +### 5.3 Bot Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Bot does not respond to messages | Token mismatch between registration and profile | Verify the [Token Matching Checklist](#38-token-matching-checklist). | +| `Connection refused` in Palpo logs | Octos not running, or wrong `url` in registration YAML | Ensure Octos is running. The `url` must use Docker service name (`http://octos:8009`), not `localhost`. | +| `User ID not in namespace` | `sender_localpart` doesn't match `namespaces.users` regex | Update the regex in `octos-registration.yaml` to include the bot's full user ID pattern. | +| Bot joins room but gives empty replies | LLM API key invalid or quota exceeded | Check `docker compose logs octos` for API errors. Verify your API key and account balance. | +| Messages from some users are ignored | `allowed_senders` filtering in `botfather.json` | Set `allowed_senders` to `[]` to allow everyone, or add the user's Matrix ID. | +| Bot profile not loading | Missing `created_at` / `updated_at` in `botfather.json` | These fields are required. Add them as shown in section [3.5](#35-octos-bot-profile-configbotfatherjson). | + +### 5.4 Useful Debug Commands + +```bash +# View real-time logs for all services +docker compose logs -f + +# View logs for a specific service +docker compose logs -f palpo +docker compose logs -f octos + +# Restart a single service (e.g., after editing botfather.json) +docker compose restart octos + +# Rebuild a single service (e.g., after updating source) +docker compose build --no-cache palpo +docker compose up -d palpo + +# Check Palpo's Client-Server API +curl http://127.0.0.1:8128/_matrix/client/versions + +# Full reset (WARNING: deletes all data including accounts and messages) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +--- + +## 6. Further Reading + +- **Octos Documentation (full):** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- covers all LLM providers, channels, skills, memory system, and advanced configuration. +- **Octos Matrix Appservice Guide:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) -- the original Palpo + Octos integration guide this document builds upon. +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver documentation. +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client, build instructions, and feature tracker. +- **Matrix Appservice Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the Matrix protocol specification for application services. +- **Architecture Guide:** [02-how-robrix-palpo-octos-work-together.md](02-how-robrix-palpo-octos-work-together.md) -- how the Appservice mechanism works, message lifecycle, and BotFather system. + +--- + +*This guide covers the deployment as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md b/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md new file mode 100644 index 000000000..dedc150ea --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md @@ -0,0 +1,321 @@ +# 架构原理:Robrix + Palpo + Octos 如何协同工作 + +[English Version](02-how-robrix-palpo-octos-work-together.md) + +> **目标:** 阅读本指南后,你将理解 Matrix Application Service(应用服务)机制如何运作,Octos 如何作为 App Service 注册到 Palpo 以接收和回复消息,以及消息从 Robrix 经过 Palpo 到达 AI 机器人再返回的完整生命周期。 + +本文档解释 Robrix + Palpo + Octos 系统背后的**工作机制**。如需部署请参阅 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md)。如需使用指南请参阅 [03-using-robrix-with-palpo-and-octos-zh.md](03-using-robrix-with-palpo-and-octos-zh.md)。 + +--- + +## 目录 + +1. [三个项目概览](#1-三个项目概览) +2. [Matrix 协议基础](#2-matrix-协议基础) +3. [Application Service 机制](#3-application-service-机制) +4. [消息生命周期](#4-消息生命周期) +5. [端口与协议](#5-端口与协议) +6. [BotFather 系统](#6-botfather-系统) +7. [延伸阅读](#7-延伸阅读) + +--- + +## 1. 三个项目概览 + +| 项目 | 角色 | 功能说明 | +|------|------|----------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 使用 Rust 和 [Makepad](https://github.com/makepad/makepad/) 编写的跨平台 Matrix 聊天客户端。支持 macOS、Linux、Windows、Android 和 iOS 原生运行。这是用户直接交互的应用程序——在这里阅读和发送消息。 | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix 服务器 | Rust 原生的 Matrix 主服务器(homeserver)。使用 PostgreSQL 存储用户账号、房间和消息。负责在客户端(Robrix)和应用服务(Octos)之间路由事件。可以把它理解为系统的"中央邮局"。 | +| [**Octos**](https://github.com/octos-org/octos) | AI 机器人(应用服务) | Rust 原生的 AI 代理平台,以 [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/) 的形式运行。从 Palpo 接收消息,将其转发给 LLM(DeepSeek、OpenAI、Anthropic 等),然后将 AI 的回复发布到房间中。 | + +三个项目各自独立且完全开源。组合在一起,它们构成一个完整的 AI 聊天系统:用户通过原生聊天界面与 AI 机器人交互,所有通信都通过符合标准的 Matrix 服务器进行路由。 + +--- + +## 2. Matrix 协议基础 + +在深入架构之前,先了解理解本系统所需的 Matrix 协议核心概念。 + +### 主服务器(Homeserver) + +主服务器是 Matrix 的骨干。它存储用户账号、房间状态和消息历史。每个用户恰好属于一个主服务器——例如,`@alice:example.com` 属于 `example.com` 上的主服务器。在我们的系统中,Palpo 就是主服务器。 + +### 房间(Room) + +房间是一个共享的对话空间。当你发送消息时,消息是发送到房间的,而不是直接发给另一个用户。房间中的所有参与者都能看到消息。房间中可以包含任意组合的真实用户和机器人。 + +### 事件(Event) + +Matrix 中的一切都是**事件**。一条消息是事件(`m.room.message`)。加入房间是事件(`m.room.member`)。修改房间名称也是事件。事件是最基本的数据单元——它们是不可变的、有序的,构成了房间的完整历史记录。 + +### 客户端-服务器 API(Client-Server API) + +这是客户端(如 Robrix)与其主服务器(Palpo)之间的通信方式。Client-Server API 用于: + +- 登录和注册账号 +- 发送消息(`PUT /_matrix/client/v3/rooms/{roomId}/send/...`) +- 同步房间状态和消息历史 +- 管理房间(创建、加入、邀请) + +Robrix 完全通过此 API 与 Palpo 通信。Octos 在发送机器人回复时也使用此 API。 + +### 服务器间 API(Federation) + +这是主服务器之间相互通信的方式。如果 `@alice:server-a.com` 在一个包含 `@bob:server-b.com` 的房间中发送消息,两个主服务器会通过联邦协议(Federation)通信来传递事件。这正是 Matrix 成为去中心化协议的关键所在。详见 [04-federation-with-palpo-zh.md](04-federation-with-palpo-zh.md)。 + +### 滑动同步(Sliding Sync) + +传统的 Matrix 同步会在启动时下载完整的房间状态,在移动设备或受限设备上可能很慢。**滑动同步(Sliding Sync)** 是 Matrix 规范中定义的一种优化同步机制,只发送客户端当前需要的数据——就像在房间列表上滑动一个窗口。Robrix 要求主服务器支持 Sliding Sync。Palpo 原生支持这一特性。 + +--- + +## 3. Application Service 机制 + +本节是架构文档的核心。理解 Application Service(应用服务)机制是理解 Octos 如何接入 Palpo 的关键。 + +### 3.1 什么是 Matrix Application Service? + +Matrix Application Service 是一种在主服务器上拥有**特殊权限**的程序。与使用用户名和密码登录的普通客户端不同,应用服务: + +- **通过 YAML 注册文件向主服务器注册**(而不是通过 Client-Server API) +- **声明独占的用户命名空间** -- 拥有一系列用户 ID,并可以代表其中任何一个用户行事 +- **从主服务器接收推送的事件** -- 无需轮询或同步 +- **不受速率限制** -- 可以按需要的任何速度发送消息 +- **可以动态创建虚拟用户**,无需经过常规注册流程 + +这是为桥接(将 Matrix 连接到 Telegram、Slack 等)和机器人设计的机制。Octos 使用它来运行 AI 机器人。 + +> Matrix 规范参考:[Application Service API](https://spec.matrix.org/latest/application-service-api/) + +### 3.2 注册文件:Palpo 如何发现 Octos + +启动时,Palpo 从 `palpo.toml` 中 `appservice_registration_dir` 指定的目录读取所有 `.yaml` 文件。每个文件代表一个已注册的应用服务。 + +注册文件(`appservices/octos-registration.yaml`)包含: + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "" +hs_token: "" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" +``` + +各字段说明: + +| 字段 | 用途 | +|------|------| +| `id` | 此应用服务的唯一名称。Palpo 用它来跟踪事件投递状态。 | +| `url` | Palpo 发送事件的 HTTP 端点。这是 Octos 在 Docker 网络内的地址。 | +| `as_token` | Octos 调用 Palpo API 时出示的令牌。证明"我是已注册的应用服务"。 | +| `hs_token` | Palpo 向 Octos 推送事件时出示的令牌。证明"我是你注册时对应的主服务器"。 | +| `sender_localpart` | 主机器人的用户名。与 `server_name` 组合后变成 `@octosbot:127.0.0.1:8128`。 | +| `namespaces.users` | 此应用服务独占的用户 ID 正则表达式模式。 | + +这是一种**双向信任关系**:Octos 用 `as_token` 向 Palpo 认证,Palpo 用 `hs_token` 向 Octos 认证。双方必须持有相同的令牌对,分别配置在两个文件中:注册 YAML 文件(给 Palpo 读取)和 `botfather.json`(给 Octos 读取)。如果不匹配,系统将无法工作。请参阅部署指南中的[令牌匹配检查清单](01-deploying-palpo-and-octos-zh.md#38-令牌匹配检查清单)。 + +### 3.3 用户命名空间:机器人身份 + +`namespaces.users` 部分告诉 Palpo 哪些用户 ID 属于 Octos。正则表达式模式声明了特定的范围: + +- **`@octosbot:127.0.0.1:8128`** -- 主机器人,也称为 **BotFather**。这是用户的入口点。 +- **`@octosbot_.*:127.0.0.1:8128`** -- 动态创建的子机器人(例如 `@octosbot_translator:127.0.0.1:8128`)。`.*` 通配符意味着 Octos 可以创建任何带 `octosbot_` 前缀的用户 ID。 + +设置 `exclusive: true` 意味着**没有其他实体可以创建或声明这些用户 ID**。如果普通用户尝试注册为 `@octosbot:127.0.0.1:8128`,Palpo 会拒绝该请求。 + +命名空间机制也是 Palpo 决定是否通知 Octos 的依据。当有人邀请 `@octosbot:127.0.0.1:8128` 加入房间时,Palpo 检查其已注册的应用服务,发现此用户 ID 匹配 Octos 的命名空间,于是将邀请事件推送给 Octos。 + +### 3.4 事件推送流程 + +应用服务协议是**推送式**的,不是拉取式的。应用服务不需要同步或轮询——主服务器主动向它发送事件。 + +当一条消息到达一个包含应用服务用户的房间时: + +1. **Palpo 检查其应用服务注册表。** 它查看哪些应用服务用户是该房间的成员。如果 `@octosbot:127.0.0.1:8128` 在房间中,Palpo 就知道需要通知 Octos。 + +2. **Palpo 向 Octos 发送 HTTP PUT 请求。** 请求发送到 `{url}/transactions/{txnId}`——在我们的场景中是 `http://octos:8009/transactions/{txnId}`。请求体包含事件数据(发送者、房间 ID、消息内容等),Palpo 附带 `hs_token` 进行认证。 + +3. **Octos 处理事件。** 它接收事件,识别房间和发送者,并决定如何响应。对于 AI 机器人来说,这意味着调用配置的 LLM。 + +4. **Octos 通过 Palpo 的 Client-Server API 发送回复。** Octos 没有直接连接到 Robrix 的通道。它以机器人用户的身份通过 Palpo 发送消息,就像其他任何客户端一样,使用 `as_token` 进行认证。 + +这种推送模型非常高效:Octos 不会浪费资源进行轮询,事件以最小延迟传递。 + +--- + +## 4. 消息生命周期 + +以下是一条消息在系统中的完整旅程,从你在 Robrix 中输入到 AI 机器人的回复出现在屏幕上。 + +### 逐步数据流 + +``` +用户在 Robrix 中输入 "Hello" + | + v ++-----------------+ +| 1. Robrix 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| (CS API) | -> http://127.0.0.1:8128 (Palpo) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo 存储 | 事件保存到 PostgreSQL +| 事件 | 房间状态更新 ++--------+--------+ + | + v ++-----------------+ +| 3. Palpo 推送 | PUT /transactions/{txnId} -> http://octos:8009 +| 到 Octos | (Appservice API,Docker 内部网络) ++--------+--------+ + | + v ++-----------------+ +| 4. Octos 调用 | POST /v1/chat/completions -> DeepSeek API +| LLM | (或其他配置的提供商) ++--------+--------+ + | + v ++-----------------+ +| 5. Octos 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 回复 | -> http://palpo:8008 (Docker 内部网络) +| (CS API) | Auth: Bearer {as_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo 存储 | 机器人回复事件已保存 +| 并投递 | Sliding Sync 推送到 Robrix ++--------+--------+ + | + v +用户在 Robrix 中看到 AI 回复 +``` + +### 每一步发生了什么 + +**步骤 1 -- Robrix 发送消息。** 当你点击发送时,Robrix 向 Palpo 的 Client-Server API 发起 HTTP PUT 请求。请求包含房间 ID、事件类型(`m.room.message`)和消息内容。Robrix 连接到 `http://127.0.0.1:8128`,即 Palpo 暴露在主机上的端口。 + +**步骤 2 -- Palpo 存储事件。** Palpo 接收消息,分配一个事件 ID,并将其持久化到 PostgreSQL。房间状态更新以反映新消息。 + +**步骤 3 -- Palpo 将事件推送给 Octos。** Palpo 检查其应用服务注册表,发现 `@octosbot:127.0.0.1:8128` 是该房间的成员。它通过 HTTP PUT 请求将事件发送到 Octos 的应用服务端点(`http://octos:8009`),路径为 `/transactions/{txnId}`。这使用 Docker 内部网络——流量不会离开主机。 + +**步骤 4 -- Octos 调用 LLM。** Octos 接收事件,提取消息内容,并调用配置的 LLM 提供商(例如 DeepSeek 的 `/v1/chat/completions` 端点)。它会包含对话历史作为上下文。 + +**步骤 5 -- Octos 发送回复。** LLM 响应后,Octos 以机器人用户(`@octosbot:127.0.0.1:8128`)的身份,通过 Palpo 的 Client-Server API 发送回复。它使用 `as_token` 进行认证。注意 Octos 连接的是 `http://palpo:8008`(Docker 内部),而不是 `127.0.0.1:8128`(主机)。 + +**步骤 6 -- Palpo 将回复投递给 Robrix。** Palpo 存储机器人的回复事件,并将其包含在 Robrix 的下一次 Sliding Sync 响应中。Robrix 接收事件并在对话中显示 AI 机器人的消息。 + +### 架构图 + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Appservice API | Octos | HTTPS | LLM | +| (客户端) | --------------------> | (服务器) | --------------------> | (机器人) | ------> | | +| | <-------------------- | | <------------------- | | <------ | | ++----------+ Sliding Sync +----------+ Client-Server API +----------+ +-----+ + 你的机器 Docker :8128 Docker :8009 外部服务 +``` + +关键观察: + +- **Robrix 从不直接与 Octos 通信。** 所有通信都通过 Palpo 中转。Robrix 甚至不知道 Octos 的存在——它只看到房间中的机器人用户。 +- **两条不同的路径,同一个 API。** Robrix 和 Octos 都使用 Client-Server API 与 Palpo 通信,但 Octos 使用 `as_token`(应用服务凭证)而非普通用户会话进行认证。 +- **内部流量与外部流量。** Robrix 通过主机端口(8128)连接。Palpo 和 Octos 在 Docker 内部网络上通信(使用服务名 `palpo:8008` 和 `octos:8009`)。只有 LLM API 调用会访问互联网。 + +--- + +## 5. 端口与协议 + +| 连接 | 协议 | 默认端口 | 方向 | 说明 | +|------|------|----------|------|------| +| Robrix -> Palpo | Client-Server API (Sliding Sync) | 8128 (主机) -> 8008 (容器) | 双向 | Robrix 唯一需要的端口。暴露在主机上。 | +| Palpo -> Octos | Appservice API | 8009 (主机) -> 8009 (容器) | Palpo 推送事件 | 同时暴露到主机用于调试。内部使用 Docker 服务名 `octos`。 | +| Octos -> Palpo | Client-Server API | 8008 (Docker 内部网络) | Octos 发送回复 | 使用 Docker 服务名 `palpo`。通过 `as_token` 认证。 | +| Octos 管理面板 | HTTP | 8010 (主机) -> 8080 (容器) | 入站 | 可选的管理 UI,用于监控 Octos。 | +| Octos -> LLM | HTTPS | 443 (出站) | 出站 | 对 LLM 提供商的外部 API 调用。 | + +**为什么 Palpo 有两个不同的端口(8008 vs. 8128)?** 在 Docker 网络内部,Palpo 监听 8008 端口(容器端口)。Docker 将主机端口 8128 映射到容器端口 8008。Octos 运行在同一 Docker 网络中,直接连接 `palpo:8008`。Robrix 运行在主机上,连接 `127.0.0.1:8128`。 + +--- + +## 6. BotFather 系统 + +Octos 实现了 **BotFather** 模式,通过单个应用服务管理多个 AI 机器人。 + +### 父机器人与子机器人 + +**BotFather** 是主机器人(`@octosbot:server_name`)。它是入口点——用户邀请 BotFather 加入房间即可开始交互。但 BotFather 还可以创建**子机器人**,每个子机器人拥有不同的个性和用途。 + +``` +BotFather (@octosbot:127.0.0.1:8128) + | + +-- 翻译机器人 (@octosbot_translator:127.0.0.1:8128) + | 系统提示词: "你是一个翻译。将所有消息翻译成中文。" + | + +-- 代码审查员 (@octosbot_reviewer:127.0.0.1:8128) + | 系统提示词: "你是一个代码审查员。检查代码中的错误和风格问题。" + | + +-- 写作助手 (@octosbot_writer:127.0.0.1:8128) + 系统提示词: "你是一个写作助手。帮助改善文字的清晰度和语气。" +``` + +### 子机器人的工作原理 + +每个子机器人都有自己的: + +- **显示名称** -- 在聊天中显示的可读名称(例如:"翻译机器人") +- **系统提示词(System Prompt)** -- 定义机器人个性和行为的指令 +- **用户 ID** -- 使用 `octosbot_` 前缀生成(例如:`@octosbot_translator:127.0.0.1:8128`) + +子机器人在运行时动态创建。它们不需要单独的注册文件或独立的进程。所有子机器人都在同一个 Octos 应用服务实例中运行。 + +### 为什么这能工作:命名空间的关联 + +还记得注册文件中的命名空间正则表达式吗? + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" +``` + +这个通配符模式正是使动态创建子机器人成为可能的原因。当 Octos 创建新的子机器人(如 `@octosbot_translator:127.0.0.1:8128`)时,Palpo 检查已注册的命名空间,确认该用户 ID 在 Octos 的独占范围内,然后允许创建。无需额外配置。 + +### 从 Robrix 管理机器人 + +Robrix 内置了通过 BotFather 系统创建和管理子机器人的 UI。在 Robrix 的**机器人设置**面板中,你可以: + +1. 启用应用服务支持并配置 BotFather 用户 ID +2. 创建新的子机器人,自定义用户名、显示名称和系统提示词 +3. 查看和管理现有机器人 + +详细的操作步骤请参阅使用指南中的[机器人管理](03-using-robrix-with-palpo-and-octos-zh.md)部分。 + +--- + +## 7. 延伸阅读 + +- **Matrix Application Service 规范:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- 应用服务的官方协议规范。 +- **Octos 文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- Octos 的完整文档,包括全部 14 种 LLM 提供商、频道、技能和记忆系统。 +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo 主服务器文档和源代码。 +- **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix 客户端源代码和功能跟踪。 +- **Matrix 规范 (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- 完整的 Client-Server API 规范,包括 Sliding Sync。 +- **部署指南:** [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) -- 如何部署和配置系统。 +- **使用指南:** [03-using-robrix-with-palpo-and-octos-zh.md](03-using-robrix-with-palpo-and-octos-zh.md) -- 如何使用 Robrix 与 AI 机器人交互的分步指南。 + +--- + +*本文档描述的是截至 2026 年 4 月的架构。如需最新更新,请参阅各项目的代码仓库。* diff --git a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md b/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md new file mode 100644 index 000000000..677cb216a --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md @@ -0,0 +1,321 @@ +# Architecture: How Robrix + Palpo + Octos Work Together + +[中文版](02-how-robrix-palpo-octos-work-together-zh.md) + +> **Goal:** After reading this guide, you will understand how the Matrix Application Service mechanism works, how Octos registers as an App Service on Palpo to receive and respond to messages, and how the complete message lifecycle flows from Robrix through Palpo to the AI bot and back. + +This document explains the **mechanisms** behind the Robrix + Palpo + Octos system. If you want to deploy it, see [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md). If you want to use it, see [03-using-robrix-with-palpo-and-octos.md](03-using-robrix-with-palpo-and-octos.md). + +--- + +## Table of Contents + +1. [Three Projects Overview](#1-three-projects-overview) +2. [Matrix Protocol Basics](#2-matrix-protocol-basics) +3. [Application Service Mechanism](#3-application-service-mechanism) +4. [Message Lifecycle](#4-message-lifecycle) +5. [Ports and Protocols](#5-ports-and-protocols) +6. [BotFather System](#6-botfather-system) +7. [Further Reading](#7-further-reading) + +--- + +## 1. Three Projects Overview + +| Project | Role | What it does | +|---------|------|--------------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client written in Rust using [Makepad](https://github.com/makepad/makepad/). Runs natively on macOS, Linux, Windows, Android, and iOS. This is the user-facing application -- where you read and send messages. | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix Homeserver | A Rust-native Matrix homeserver. Stores user accounts, rooms, and messages in PostgreSQL. Routes events between clients (Robrix) and application services (Octos). Think of it as the central post office. | +| [**Octos**](https://github.com/octos-org/octos) | AI Bot (Appservice) | A Rust-native AI agent platform that runs as a [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/). Receives messages from Palpo, forwards them to an LLM (DeepSeek, OpenAI, Anthropic, etc.), and posts the AI reply back into the room. | + +Each project is independent and open-source. Together, they form a complete AI chat system where users interact with AI bots through a native chat interface, with all communication routed through a standards-compliant Matrix homeserver. + +--- + +## 2. Matrix Protocol Basics + +Before diving into the architecture, here are the Matrix protocol concepts you need to understand. + +### Homeserver + +A homeserver is the backbone of Matrix. It stores user accounts, room state, and message history. Every user belongs to exactly one homeserver -- for example, `@alice:example.com` belongs to the homeserver at `example.com`. In our system, Palpo is the homeserver. + +### Room + +A room is a shared conversation space. When you send a message, it is sent to a room, not directly to another user. All participants in the room see the message. Rooms can contain any mix of human users and bots. + +### Event + +Everything in Matrix is an **event**. A message is an event (`m.room.message`). Joining a room is an event (`m.room.member`). Changing a room's name is an event. Events are the fundamental unit of data -- they are immutable, ordered, and form the room's history. + +### Client-Server API + +This is how clients (like Robrix) communicate with their homeserver (Palpo). The Client-Server API is used for: + +- Logging in and registering accounts +- Sending messages (`PUT /_matrix/client/v3/rooms/{roomId}/send/...`) +- Syncing room state and message history +- Managing rooms (creating, joining, inviting) + +Robrix talks to Palpo exclusively through this API. Octos also uses it when sending bot replies back through Palpo. + +### Server-Server API (Federation) + +This is how homeservers talk to each other. If `@alice:server-a.com` sends a message in a room that `@bob:server-b.com` is in, the two homeservers communicate via federation to deliver the event. This is what makes Matrix a decentralized protocol. See [04-federation-with-palpo.md](04-federation-with-palpo.md) for details. + +### Sliding Sync + +Traditional Matrix sync downloads the entire room state on startup, which can be slow on mobile or constrained devices. **Sliding Sync** is an optimized sync mechanism (defined in the Matrix spec) that only sends the data the client currently needs -- like a sliding window over your room list. Robrix requires Sliding Sync support from the homeserver. Palpo supports it natively. + +--- + +## 3. Application Service Mechanism + +This section is the core of the architecture. Understanding the Application Service (appservice) mechanism is the key to understanding how Octos connects to Palpo. + +### 3.1 What is a Matrix Application Service? + +A Matrix Application Service is a special kind of program that has **elevated privileges** on a homeserver. Unlike a regular client that logs in with a username and password, an appservice: + +- **Registers with the homeserver** via a YAML registration file (not through the Client-Server API) +- **Claims exclusive user namespaces** -- it owns a range of user IDs and can act as any of them +- **Receives pushed events** from the homeserver -- it does not need to poll or sync +- **Is not rate-limited** -- it can send messages at whatever speed it needs +- **Can create virtual users** dynamically, without going through the registration flow + +This is the mechanism designed for bridges (connecting Matrix to Telegram, Slack, etc.) and bots. Octos uses it to run AI bots. + +> Matrix spec reference: [Application Service API](https://spec.matrix.org/latest/application-service-api/) + +### 3.2 Registration File: How Palpo Discovers Octos + +On startup, Palpo reads all `.yaml` files from the directory specified by `appservice_registration_dir` in `palpo.toml`. Each file represents one registered appservice. + +The registration file (`appservices/octos-registration.yaml`) contains: + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "" +hs_token: "" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" +``` + +Here is what each field does: + +| Field | Purpose | +|-------|---------| +| `id` | A unique name for this appservice. Palpo uses it to track event delivery. | +| `url` | The HTTP endpoint where Palpo sends events. This is Octos's address inside the Docker network. | +| `as_token` | The token Octos presents when calling Palpo's API. Proves "I am the registered appservice." | +| `hs_token` | The token Palpo presents when pushing events to Octos. Proves "I am the homeserver you registered with." | +| `sender_localpart` | The main bot's username. Combined with `server_name`, it becomes `@octosbot:127.0.0.1:8128`. | +| `namespaces.users` | Regex patterns for user IDs that this appservice exclusively owns. | + +This is a **mutual trust relationship**: Octos authenticates to Palpo with `as_token`, and Palpo authenticates to Octos with `hs_token`. Both sides must have the same token pair, configured in two files: the registration YAML (for Palpo) and `botfather.json` (for Octos). If they do not match, nothing works. See the [Token Matching Checklist](01-deploying-palpo-and-octos.md#38-token-matching-checklist) in the deployment guide. + +### 3.3 User Namespaces: Bot Identity + +The `namespaces.users` section is how Palpo knows which user IDs belong to Octos. The regex patterns claim specific ranges: + +- **`@octosbot:127.0.0.1:8128`** -- The main bot, also called the **BotFather**. This is the entry point for users. +- **`@octosbot_.*:127.0.0.1:8128`** -- Child bots created dynamically (e.g., `@octosbot_translator:127.0.0.1:8128`). The `.*` wildcard means Octos can create any user ID with the `octosbot_` prefix. + +Setting `exclusive: true` means **no other entity can create or claim these user IDs**. If a regular user tries to register as `@octosbot:127.0.0.1:8128`, Palpo will reject the request. + +This namespace mechanism is also how Palpo decides to notify Octos. When someone invites `@octosbot:127.0.0.1:8128` to a room, Palpo checks its registered appservices, finds that this user ID matches Octos's namespace, and pushes the invite event to Octos. + +### 3.4 Event Push Flow + +The appservice protocol is **push-based**, not pull-based. The appservice does not sync or poll -- the homeserver sends events to it. + +When a message arrives in a room where an appservice user is present: + +1. **Palpo checks its appservice registry.** It looks at which appservice users are members of the room. If `@octosbot:127.0.0.1:8128` is in the room, Palpo knows Octos needs to be notified. + +2. **Palpo sends an HTTP PUT to Octos.** The request goes to `{url}/transactions/{txnId}` -- in our case, `http://octos:8009/transactions/{txnId}`. The body contains the event data (sender, room ID, message content, etc.), and Palpo includes `hs_token` for authentication. + +3. **Octos processes the event.** It receives the event, identifies the room and sender, and decides how to respond. For an AI bot, this means calling the configured LLM. + +4. **Octos sends its reply via Palpo's Client-Server API.** Octos does not have its own connection to Robrix. Instead, it acts as the bot user and sends a message through Palpo, just like any other client. It authenticates with `as_token`. + +This push model is efficient: Octos does not waste resources polling, and events are delivered with minimal latency. + +--- + +## 4. Message Lifecycle + +Here is the complete journey of a message through the system, from the moment you type it in Robrix to the moment the AI bot's reply appears on your screen. + +### Step-by-Step Data Flow + +``` +User types "Hello" in Robrix + | + v ++-----------------+ +| 1. Robrix sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| via CS API | -> http://127.0.0.1:8128 (Palpo) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo stores | Event saved to PostgreSQL +| the event | Room state updated ++--------+--------+ + | + v ++-----------------+ +| 3. Palpo pushes | PUT /transactions/{txnId} -> http://octos:8009 +| to Octos | (Appservice API, internal Docker network) ++--------+--------+ + | + v ++-----------------+ +| 4. Octos calls | POST /v1/chat/completions -> DeepSeek API +| the LLM | (or other configured provider) ++--------+--------+ + | + v ++-----------------+ +| 5. Octos sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| reply via | -> http://palpo:8008 (internal Docker network) +| CS API | Auth: Bearer {as_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo stores | Bot's reply event saved +| & delivers | Sliding Sync pushes to Robrix ++--------+--------+ + | + v +User sees AI reply in Robrix +``` + +### What Happens at Each Step + +**Step 1 -- Robrix sends the message.** When you hit send, Robrix makes an HTTP PUT request to Palpo's Client-Server API. The request includes the room ID, the event type (`m.room.message`), and the message content. Robrix connects to `http://127.0.0.1:8128`, which is Palpo's port exposed on the host machine. + +**Step 2 -- Palpo stores the event.** Palpo receives the message, assigns it an event ID, and persists it to PostgreSQL. The room's state is updated to reflect the new message. + +**Step 3 -- Palpo pushes the event to Octos.** Palpo checks its appservice registry and sees that `@octosbot:127.0.0.1:8128` is a member of this room. It sends the event to Octos's appservice endpoint (`http://octos:8009`) via an HTTP PUT to `/transactions/{txnId}`. This uses the internal Docker network -- no traffic leaves the host. + +**Step 4 -- Octos calls the LLM.** Octos receives the event, extracts the message content, and calls the configured LLM provider (e.g., DeepSeek's `/v1/chat/completions` endpoint). It includes conversation history for context. + +**Step 5 -- Octos sends the reply.** Once the LLM responds, Octos sends the reply back through Palpo's Client-Server API, acting as the bot user (`@octosbot:127.0.0.1:8128`). It authenticates with `as_token`. Note that Octos connects to Palpo at `http://palpo:8008` (Docker internal), not `127.0.0.1:8128` (host). + +**Step 6 -- Palpo delivers the reply to Robrix.** Palpo stores the bot's reply event and includes it in Robrix's next Sliding Sync response. Robrix receives the event and displays the AI bot's message in the conversation. + +### Architecture Diagram + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Appservice API | Octos | HTTPS | LLM | +| (Client) | --------------------> | (Server) | --------------------> | (Bot) | ------> | | +| | <-------------------- | | <------------------- | | <------ | | ++----------+ Sliding Sync +----------+ Client-Server API +----------+ +-----+ + Your machine Docker :8128 Docker :8009 External +``` + +Key observations: + +- **Robrix never talks directly to Octos.** All communication goes through Palpo. Robrix does not even know Octos exists -- it just sees bot users in rooms. +- **Two different paths, same API.** Both Robrix and Octos use the Client-Server API to talk to Palpo, but Octos authenticates with `as_token` (appservice credential) instead of a regular user session. +- **Internal vs. external traffic.** Robrix connects via the host port (8128). Palpo and Octos communicate on the Docker internal network (service names `palpo:8008` and `octos:8009`). Only the LLM API call goes to the internet. + +--- + +## 5. Ports and Protocols + +| Connection | Protocol | Default Port | Direction | Notes | +|-----------|----------|-------------|-----------|-------| +| Robrix -> Palpo | Client-Server API (Sliding Sync) | 8128 (host) -> 8008 (container) | Bidirectional | The only port Robrix needs. Exposed on the host machine. | +| Palpo -> Octos | Appservice API | 8009 (host) -> 8009 (container) | Palpo pushes events | Also exposed to host for debugging. Uses Docker service name `octos` internally. | +| Octos -> Palpo | Client-Server API | 8008 (internal Docker network) | Octos sends replies | Uses Docker service name `palpo`. Auth via `as_token`. | +| Octos Dashboard | HTTP | 8010 (host) -> 8080 (container) | Inbound | Optional admin UI for monitoring Octos. | +| Octos -> LLM | HTTPS | 443 (outbound) | Outbound | External API call to the LLM provider. | + +**Why two different ports for Palpo (8008 vs. 8128)?** Inside the Docker network, Palpo listens on port 8008 (its container port). Docker maps host port 8128 to container port 8008. Octos, running in the same Docker network, connects directly to `palpo:8008`. Robrix, running on the host machine, connects to `127.0.0.1:8128`. + +--- + +## 6. BotFather System + +Octos implements a **BotFather** pattern for managing multiple AI bots through a single appservice. + +### Parent and Child Bots + +The **BotFather** is the main bot (`@octosbot:server_name`). It is the entry point -- users invite BotFather to a room to start interacting. But BotFather can also create **child bots**, each with a different personality and purpose. + +``` +BotFather (@octosbot:127.0.0.1:8128) + | + +-- Translator Bot (@octosbot_translator:127.0.0.1:8128) + | System prompt: "You are a translator. Translate all messages to English." + | + +-- Code Reviewer (@octosbot_reviewer:127.0.0.1:8128) + | System prompt: "You are a code reviewer. Review code for bugs and style." + | + +-- Writing Assistant (@octosbot_writer:127.0.0.1:8128) + System prompt: "You are a writing assistant. Help improve clarity and tone." +``` + +### How Child Bots Work + +Each child bot has its own: + +- **Display name** -- A human-readable name shown in the chat (e.g., "Translator Bot") +- **System prompt** -- Instructions that define the bot's personality and behavior +- **User ID** -- Generated with the `octosbot_` prefix (e.g., `@octosbot_translator:127.0.0.1:8128`) + +Child bots are created dynamically at runtime. They do not need separate registration files or separate processes. They all run within the single Octos appservice instance. + +### Why This Works: The Namespace Connection + +Remember the namespace regex in the registration file? + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" +``` + +This wildcard pattern is what makes dynamic child bot creation possible. When Octos creates a new child bot like `@octosbot_translator:127.0.0.1:8128`, Palpo checks the registered namespaces, confirms that this user ID is within Octos's exclusive range, and allows it. No additional configuration is needed. + +### Managing Bots from Robrix + +Robrix has a built-in UI for creating and managing child bots through the BotFather system. From Robrix's **Bot Settings** panel, you can: + +1. Enable appservice support and configure the BotFather user ID +2. Create new child bots with a custom username, display name, and system prompt +3. View and manage existing bots + +For step-by-step instructions, see the [Bot Management](03-using-robrix-with-palpo-and-octos.md) section in the usage guide. + +--- + +## 7. Further Reading + +- **Matrix Application Service Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- The official protocol specification for appservices. +- **Octos Book:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- Full documentation for Octos, including all 14 LLM providers, channels, skills, and memory. +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver documentation and source. +- **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client source and feature tracker. +- **Matrix Spec (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- The full Client-Server API specification, including Sliding Sync. +- **Deployment Guide:** [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) -- How to deploy and configure the system. +- **Usage Guide:** [03-using-robrix-with-palpo-and-octos.md](03-using-robrix-with-palpo-and-octos.md) -- How to use Robrix with AI bots, step by step. + +--- + +*This document describes the architecture as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md new file mode 100644 index 000000000..059a7d070 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md @@ -0,0 +1,213 @@ +# 使用指南:Robrix + Palpo + Octos + +[English Version](03-using-robrix-with-palpo-and-octos.md) + +> **目标:** 按照本指南操作后,你将掌握如何使用 Robrix 连接 Palpo 服务器、注册账号、创建房间、邀请 AI 机器人、进行对话,以及通过 BotFather 系统管理机器人——全部配有分步截图演示。 + +本指南逐步介绍如何使用 Robrix 客户端连接 Palpo 服务器,并与 Octos AI 机器人进行对话。每个步骤都包含具体的操作说明。 + +**快速索引** + +| 你想做什么 | 跳转到 | +|---|---| +| 连接到服务器 | [第 2 节](#2-连接到-palpo) | +| 创建账号 | [第 3 节](#3-注册账号) | +| 与 AI 机器人聊天 | [第 5 节](#5-与-ai-机器人聊天) | +| 创建专业化机器人 | [第 6 节](#6-机器人管理高级功能) | + +--- + +## 1. 开始之前 + +请确认: + +- **Palpo 和 Octos 已经启动并运行。** 请参照 [部署指南](01-deploying-palpo-and-octos-zh.md) 完成所有服务的搭建。 +- **Robrix 已安装就绪。** 请参照 [快速入门](../robrix/getting-started-with-robrix-zh.md) 获取构建或下载说明。 + +> **注意:** 本指南假设使用本地部署,`server_name = 127.0.0.1:8128`。如果你使用远程部署,请将相关地址替换为你的实际服务器地址。 + +--- + +## 2. 连接到 Palpo + +打开 Robrix 后会显示登录界面。默认情况下,Robrix 连接到 `matrix.org`。你需要将其指向你自己的 Palpo 服务器。 + +1. 在登录界面的 **底部** 找到 **Homeserver URL** 输入框。 +2. 输入 `http://127.0.0.1:8128`(本地部署)。 +3. 如果是远程服务器,输入 `https://your.server.name` 或 `http://服务器IP:8128`。 + + + +> **注意:** 如果 Homeserver URL 留空,Robrix 会默认连接到 `matrix.org`。你必须填写此字段才能连接到自己的 Palpo 服务器。 + +--- + +## 3. 注册账号 + +在 Palpo 服务器上创建新账号: + +1. 输入你想要的 **用户名**(例如 `alice`)。 +2. 输入 **密码**。 +3. 在 **确认密码** 字段再次输入相同的密码。 +4. 输入 **Homeserver URL**:`http://127.0.0.1:8128`。 +5. 点击 **Sign up(注册)**。 + +![注册账号](../images/register-account.png) + +> **注意:** 服务器必须启用注册功能。请确保 `palpo.toml` 中设置了 `allow_registration = true`。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 + +--- + +## 4. 登录 + +如果你已有账号: + +1. 输入 **用户名** 和 **密码**。 +2. 输入 **Homeserver URL**:`http://127.0.0.1:8128`。 +3. 点击 **Log in(登录)**。 + +登录后会看到房间列表。新账号的房间列表是空的。 + + + +--- + +## 5. 与 AI 机器人聊天 + +这是主要的使用流程:创建房间、邀请机器人、开始对话。 + +### 5.1 创建新房间 + +1. 点击房间列表区域的 **创建房间** 按钮("+" 图标)。 +2. 为房间命名,例如 "AI Chat"。 +3. 房间创建完成,你会自动进入该房间。 + + + +### 5.2 邀请机器人 + +1. 在房间内点击 **邀请** 按钮(通常是一个人形加号图标)。 +2. 输入机器人的 Matrix ID:`@octosbot:127.0.0.1:8128`。 +3. 发送邀请。 +4. 机器人会自动加入房间。这是通过 Application Service 机制实现的,不需要在机器人端手动接受邀请。 + + + +> **提示:** 如果你使用了不同的 `server_name` 进行部署,请相应替换机器人 ID 中的 `127.0.0.1:8128`。例如,如果服务器的 `server_name = chat.example.com`,则机器人 ID 为 `@octosbot:chat.example.com`。 + +### 5.3 开始聊天 + +1. 在房间底部的输入框中输入消息。 +2. 按 **Enter** 或点击 **发送**。 +3. 机器人通过配置的 LLM 处理你的消息并回复。 +4. 你会看到流式动画效果,回复内容会实时逐步显示。 + + + +> 机器人的响应时间取决于 LLM 提供商和模型。DeepSeek 通常在几秒内响应,较大的模型可能需要更长时间。 + +**对话示例:** + +``` +你: 什么是 Matrix 协议? +机器人:Matrix 是一个去中心化实时通信的开放标准。它提供 HTTP API + 用于创建和管理聊天室、发送消息以及在联邦服务器之间同步状态…… +``` + +### 5.4 替代方式:加入已有的机器人房间 + +如果其他人已经创建了包含机器人的房间并邀请了你,或者存在公开房间: + +1. 点击 **加入房间**。 +2. 输入房间别名(例如 `#ai-chat:127.0.0.1:8128`)或房间 ID。 +3. 即可直接与机器人聊天。 + + + +--- + +## 6. 机器人管理(高级功能) + +Octos 支持"BotFather"模式:主机器人(`@octosbot`)可以创建**子机器人**,每个子机器人拥有自己的个性和系统提示词。这对于构建专业化的 AI 助手非常有用。 + +如需深入了解其工作原理,请参阅 [架构指南](02-how-robrix-palpo-octos-work-together-zh.md)。 + +### 6.1 在 Robrix 中启用 App Service 支持 + +在管理机器人之前,需要先在 Robrix 中启用该功能: + +1. 打开 Robrix 的 **设置**(齿轮图标)。 +2. 导航到 **Bot Settings(机器人设置)**。 +3. 将 **Enable App Service** 开关打开。 +4. 输入 **BotFather User ID**:`@octosbot:127.0.0.1:8128`。 +5. 点击 **Save(保存)**。 + + + +### 6.2 创建子机器人 + +启用 BotFather 后,你可以创建专业化的子机器人: + +1. 从机器人管理面板打开 **Create Bot(创建机器人)** 对话框。 +2. 填写以下字段: + - **Username(用户名)** -- 仅限小写字母、数字和下划线(例如 `translator_bot`)。 + - **Display Name(显示名称)** -- 在房间中显示的名称(例如 "翻译机器人")。 + - **System Prompt(系统提示词)** -- 定义机器人行为的指令。示例: + - `"你是一个翻译助手。将所有消息翻译成中文。"` + - `"你是一个编程助手。帮助用户编写和调试代码。"` + - `"你是一个写作教练。检查文本的清晰度和语法。"` +3. 点击 **Create Bot(创建机器人)**。 + +子机器人会以 `@octosbot_<用户名>:127.0.0.1:8128` 的格式注册。以上面的例子为例,ID 为 `@octosbot_translator_bot:127.0.0.1:8128`。 + + + +### 6.3 使用子机器人 + +创建子机器人后,使用方式与主机器人相同: + +1. 创建新房间或使用已有房间。 +2. 通过完整的 Matrix ID 邀请子机器人(例如 `@octosbot_translator_bot:127.0.0.1:8128`)。 +3. 与它对话。机器人会按照你定义的系统提示词来响应。 + + + +--- + +## 7. 使用技巧 + +- **在一个房间中使用多个机器人。** 你可以在同一个房间中邀请多个机器人,每个机器人根据自己的系统提示词独立响应。这对于对比不同模型的输出或构建多智能体工作流很有用。 + +- **私密对话。** 创建一个私密房间,只邀请一个机器人,进行不受其他用户或机器人干扰的一对一聊天。 + +- **更换 LLM 提供商。** LLM 后端在 `botfather.json` 中配置(或通过环境变量设置)。你可以在 DeepSeek、OpenAI、Anthropic 等提供商之间切换。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 + +- **机器人没有响应?** 常见原因: + - Octos 服务未运行。 + - LLM API 密钥缺失或无效。 + - 机器人未被正确邀请到房间。 + - 请查看部署指南中的 [故障排查部分](01-deploying-palpo-and-octos-zh.md#5-故障排除)。 + +- **Server name 不匹配。** 所有 Matrix ID(用户、机器人、房间)必须使用与 Palpo 配置相同的 `server_name`。如果机器人 ID 与服务器名称不匹配,邀请会失败。 + +--- + +## 8. 常用 Matrix ID 参考 + +本地部署(`server_name = 127.0.0.1:8128`)下的常用 ID: + +| 项目 | Matrix ID | +|---|---| +| 你的用户账号 | `@你的用户名:127.0.0.1:8128` | +| 主 AI 机器人(BotFather) | `@octosbot:127.0.0.1:8128` | +| 子机器人(例如翻译机器人) | `@octosbot_translator_bot:127.0.0.1:8128` | +| 房间别名 | `#房间名:127.0.0.1:8128` | + +远程部署时,将 `127.0.0.1:8128` 替换为你配置的 `server_name`。 + +--- + +## 接下来 + +- [部署指南](01-deploying-palpo-and-octos-zh.md) -- 搭建和配置服务 +- [架构指南](02-how-robrix-palpo-octos-work-together-zh.md) -- 了解各组件如何协同工作 diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md new file mode 100644 index 000000000..7629001a1 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md @@ -0,0 +1,214 @@ +# Usage Guide: Robrix + Palpo + Octos + +[中文版](03-using-robrix-with-palpo-and-octos-zh.md) + +> **Goal:** After following this guide, you will know how to use Robrix to connect to your Palpo server, register an account, create rooms, invite AI bots, have conversations, and manage bots through the BotFather system — all demonstrated with step-by-step screenshots. + +This guide walks you through using Robrix as a Matrix client connected to a Palpo homeserver with Octos AI bots. Every step includes what to click and what to type. + +**Quick Reference** + +| What you want to do | Go to | +|---|---| +| Connect to your server | [Section 2](#2-connecting-to-palpo) | +| Create an account | [Section 3](#3-registration) | +| Chat with the AI bot | [Section 5](#5-chatting-with-the-ai-bot) | +| Create specialized bots | [Section 6](#6-bot-management-advanced) | + +--- + +## 1. Before You Start + +Make sure: + +- **Palpo and Octos are running.** Follow the [Deployment Guide](01-deploying-palpo-and-octos.md) to set up all services. +- **Robrix is installed and ready.** See [Getting Started](../robrix/getting-started-with-robrix.md) for build or download instructions. + +> **Note:** This guide assumes a local deployment with `server_name = 127.0.0.1:8128`. Replace this with your actual server name if you deployed remotely. + +--- + +## 2. Connecting to Palpo + +When you open Robrix, the login screen appears. By default, Robrix connects to `matrix.org`. You need to point it to your Palpo server instead. + +1. Look at the **bottom** of the login screen for the **Homeserver URL** field. +2. Enter `http://127.0.0.1:8128` for a local deployment. +3. For a remote server, enter `https://your.server.name` or `http://server-ip:8128`. + + + +> **Note:** If the Homeserver URL field is left empty, Robrix connects to `matrix.org` by default. You must fill it in to reach your own Palpo server. + +--- + +## 3. Registration + +To create a new account on your Palpo server: + +1. Enter your desired **Username** (e.g., `alice`). +2. Enter a **Password**. +3. Enter the same password again in the **Confirm password** field. +4. Enter the **Homeserver URL**: `http://127.0.0.1:8128`. +5. Click **Sign up**. + +![Register account](../images/register-account.png) + +> **Note:** Registration must be enabled on the server. Make sure `allow_registration = true` is set in your `palpo.toml`. See [Deployment Guide -- Configuration](01-deploying-palpo-and-octos.md) for details. + +--- + +## 4. Login + +If you already have an account: + +1. Enter your **Username** and **Password**. +2. Enter the **Homeserver URL**: `http://127.0.0.1:8128`. +3. Click **Log in**. + +After logging in, you will see the room list. For a new account, this list is empty. + + + +--- + +## 5. Chatting with the AI Bot + +This is the main workflow: create a room, invite the bot, and start a conversation. + +### 5.1 Create a New Room + +1. Click the **create room** button (the "+" icon in the room list area). +2. Give the room a name, for example "AI Chat". +3. The room is created and you enter it automatically. + + + +### 5.2 Invite the Bot + +1. Click the **invite** button inside the room (usually a person-with-plus icon). +2. Enter the bot's Matrix ID: `@octosbot:127.0.0.1:8128`. +3. Send the invitation. +4. The bot joins automatically. This is handled by the Application Service mechanism -- no manual acceptance is needed on the bot side. + + + +> **Tip:** If you deployed with a different `server_name`, replace `127.0.0.1:8128` in the bot ID accordingly. For example, on a server with `server_name = chat.example.com`, the bot ID would be `@octosbot:chat.example.com`. + +### 5.3 Start Chatting + +1. Type a message in the input box at the bottom of the room. +2. Press **Enter** or click **Send**. +3. The bot processes your message through the configured LLM and replies. +4. You will see a streaming animation as the response arrives in real time. + + + +> The bot's response time depends on the LLM provider and model. DeepSeek typically responds within a few seconds. Larger models may take longer. + +**Example conversation:** + +``` +You: What is the Matrix protocol? +Bot: Matrix is an open standard for decentralized, real-time communication. + It provides HTTP APIs for creating and managing chat rooms, sending + messages, and synchronizing state across federated servers... +``` + +### 5.4 Alternative: Join an Existing Bot Room + +If someone else has already created a room with the bot and invited you, or if a public room exists: + +1. Click **Join Room**. +2. Enter the room alias (e.g., `#ai-chat:127.0.0.1:8128`) or the room ID. +3. You can start chatting with the bot right away. + + + +--- + +## 6. Bot Management (Advanced) + +Octos supports a "BotFather" pattern: the main bot (`@octosbot`) can create **child bots**, each with its own personality and system prompt. This is useful for building specialized assistants. + +For a deeper understanding of how this works, see the [Architecture Guide](02-how-robrix-palpo-octos-work-together.md). + +### 6.1 Enable App Service Support in Robrix + +Before managing bots, enable the feature in Robrix: + +1. Open **Settings** in Robrix (gear icon). +2. Navigate to **Bot Settings**. +3. Toggle **Enable App Service** to on. +4. Enter the **BotFather User ID**: `@octosbot:127.0.0.1:8128`. +5. Click **Save**. + + + +### 6.2 Create Child Bots + +With BotFather enabled, you can create specialized bots: + +1. Open the **Create Bot** dialog from the bot management panel. +2. Fill in the following fields: + - **Username** -- lowercase letters, digits, and underscores only (e.g., `translator_bot`). + - **Display Name** -- a human-readable name shown in rooms (e.g., "Translator Bot"). + - **System Prompt** -- instructions that define the bot's behavior. Examples: + - `"You are a translator. Translate all messages to English."` + - `"You are a coding assistant. Help users write and debug code."` + - `"You are a writing coach. Review text for clarity and grammar."` +3. Click **Create Bot**. + +The child bot is registered as `@octosbot_:127.0.0.1:8128`. For the example above, it would be `@octosbot_translator_bot:127.0.0.1:8128`. + + + +### 6.3 Using Child Bots + +After creating a child bot, use it like the main bot: + +1. Create a new room or use an existing one. +2. Invite the child bot by its full Matrix ID (e.g., `@octosbot_translator_bot:127.0.0.1:8128`). +3. Chat with it. The bot follows the system prompt you defined. + + + +--- + +## 7. Tips and Common Patterns + +- **Multiple bots in one room.** You can invite several bots into the same room. Each bot responds independently based on its own system prompt. This is useful for comparing outputs or building multi-agent workflows. + +- **Private conversations.** Create a private room and invite only one bot for focused 1-on-1 chats without noise from other users or bots. + +- **Change the LLM provider.** The LLM backend is configured in `botfather.json` (or via environment variables). You can switch between DeepSeek, OpenAI, Anthropic, and other providers. See the [Deployment Guide -- Configuration](01-deploying-palpo-and-octos.md) for details. + +- **Bot not responding?** Common causes: + - The Octos service is not running. + - The LLM API key is missing or invalid. + - The bot was not properly invited to the room. + - Check the [Troubleshooting section](01-deploying-palpo-and-octos.md#5-troubleshooting) in the Deployment Guide. + +- **Server name mismatch.** All Matrix IDs (users, bots, rooms) must use the same `server_name` that Palpo is configured with. If your bot ID does not match the server name, the invitation will fail. + +--- + +## 8. Common Matrix IDs Reference + +For a local deployment with `server_name = 127.0.0.1:8128`: + +| Item | Matrix ID | +|---|---| +| Your user account | `@yourusername:127.0.0.1:8128` | +| Main AI bot (BotFather) | `@octosbot:127.0.0.1:8128` | +| A child bot (e.g., translator) | `@octosbot_translator_bot:127.0.0.1:8128` | +| A room alias | `#room-name:127.0.0.1:8128` | + +For remote deployments, replace `127.0.0.1:8128` with your configured `server_name`. + +--- + +## What's Next + +- [Deployment Guide](01-deploying-palpo-and-octos.md) -- set up and configure services +- [Architecture Guide](02-how-robrix-palpo-octos-work-together.md) -- understand how the components work together diff --git a/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md new file mode 100644 index 000000000..bcb53fb03 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md @@ -0,0 +1,417 @@ +# 联邦功能:跨服务器通信 + +[English Version](04-federation-with-palpo.md) + +> **目标:** 按照本指南操作后,你的 Palpo 将配置好 Matrix 联邦功能,你服务器上的用户可以与其他 Matrix 服务器(如 matrix.org)上的用户通信,远程用户也可以访问你的 Octos AI 机器人。 + +本指南介绍 Matrix **联邦**(Federation)功能 -- 将你的 Palpo 服务器与其他 Matrix 服务器连接,使不同服务器上的用户能够互相通信。 + +> **前提条件:** 你应该已经完成了本地部署。如果还没有,请先参阅 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md)。 + +--- + +## 目录 + +1. [什么是 Matrix 联邦?](#1-什么是-matrix-联邦) +2. [联邦的前提条件](#2-联邦的前提条件) +3. [Palpo 联邦配置](#3-palpo-联邦配置) +4. [生产环境部署](#4-生产环境部署) +5. [使用联邦功能](#5-使用联邦功能) +6. [验证与故障排除](#6-验证与故障排除) +7. [延伸阅读](#7-延伸阅读) + +--- + +## 1. 什么是 Matrix 联邦? + +Matrix 是一个**去中心化**的通信协议。每个组织都可以运行自己的服务器,联邦功能允许不同服务器上的用户无缝通信。 + +可以类比电子邮件: + +- `@alice:server-a.com` 可以与 `@bob:server-b.com` 聊天 +- 每个服务器存储自己用户的数据 +- 消息在参与对话的所有服务器之间同步复制 +- 没有单点控制 -- 如果一台服务器宕机,其他服务器继续正常运行 + +在本地部署指南中,所有服务运行在 `127.0.0.1:8128` 上 -- 这是一个完全隔离的服务器。联邦功能将你的服务器接入更广阔的 Matrix 网络。 + +``` + 服务器 A 服务器 B +┌──────────┐ Federation API ┌──────────┐ +│ Palpo │ ◄────────────────► │ Synapse │ +│ + Octos │ (端口 8448) │ 或其他 │ +│ + Robrix│ │ Matrix │ +└──────────┘ └──────────┘ + @alice:server-a.com @bob:server-b.com + └─── 可以互相聊天 ───────────────┘ +``` + +--- + +## 2. 联邦的前提条件 + +与本地部署不同,联邦功能有额外的基础设施要求: + +| 需求 | 本地部署 | 联邦部署 | +|------|---------|---------| +| 域名 | 不需要(`127.0.0.1`) | 必需(如 `matrix.example.com`) | +| TLS 证书 | 不需要(HTTP) | 必需(HTTPS,推荐 Let's Encrypt) | +| 端口 443 | 不需要 | 开放(Client-Server API) | +| 端口 8448 | 不需要 | 开放(Server-Server 联邦 API) | +| 反向代理 | 不需要 | 推荐(Caddy 或 Nginx) | +| DNS 记录 | 不需要 | A 记录必需,SRV 记录可选 | + +> **自签名证书不能用于联邦。** 其他 Matrix 服务器会拒绝连接。请使用 [Let's Encrypt](https://letsencrypt.org/) 获取免费的受信证书。 + +--- + +## 3. Palpo 联邦配置 + +### 3.1 基本设置(`palpo.toml`) + +以下是支持联邦的 `palpo.toml` 配置。与本地部署的区别用注释标出: + +```toml +# 修改:使用真实域名代替 127.0.0.1:8128 +server_name = "matrix.example.com" + +# 修改:生产环境关闭开放注册 +# 先创建账号,再设为 false +allow_registration = false + +enable_admin_room = true +appservice_registration_dir = "/var/palpo/appservices" + +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "json" # 修改:生产环境使用 "json" 格式 + +[db] +url = "postgres://palpo:你的强密码@palpo_postgres:5432/palpo" +pool_size = 10 + +# 修改:使用真实域名进行服务发现 +[well_known] +server = "matrix.example.com:443" +client = "https://matrix.example.com" + +# --- 联邦设置(新增)--- +[federation] +enable = true +allow_inbound_profile_lookup = true + +# 可选:限制仅与特定服务器联邦 +# allowed_servers = ["matrix.org", "*.trusted.com"] +# denied_servers = ["evil.com"] + +[tls] +enable = true +cert = "/path/to/fullchain.pem" +key = "/path/to/privkey.pem" + +[presence] +allow_local = true +allow_incoming = true +allow_outgoing = true + +[typing] +allow_incoming = true +allow_outgoing = true +federation_timeout = 30000 + +trusted_servers = ["matrix.org"] +``` + +### 3.2 联邦设置参考 + +`[federation]` 部分控制你的服务器如何与其他 Matrix 服务器交互: + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `enable` | bool | `true` | 联邦总开关。设为 `false` 则运行完全隔离的服务器。 | +| `allow_loopback` | bool | `false` | 允许向自身发送联邦请求。仅用于开发。 | +| `allow_device_name` | bool | `false` | 向联邦用户暴露设备显示名称。出于隐私考虑,建议禁用。 | +| `allow_inbound_profile_lookup` | bool | `true` | 允许远程服务器查询本地用户资料。禁用后联邦用户将看不到显示名称。 | +| `allowed_servers` | list | 无 | 允许列表:仅这些服务器可以与你联邦。支持通配符(如 `*.trusted.com`)。未设置时允许所有服务器。 | +| `denied_servers` | list | `[]` | 拒绝列表:阻止特定服务器。**优先级高于** `allowed_servers`。支持通配符。 | + +### 3.3 服务发现(`[well_known]`) + +`[well_known]` 部分对联邦**至关重要**。它告诉其他服务器如何找到你的服务器。 + +Palpo 自动提供以下端点: + +| 端点 | 响应内容 | 使用者 | +|------|---------|--------| +| `/.well-known/matrix/server` | `{"m.server": "matrix.example.com:443"}` | 其他服务器(联邦) | +| `/.well-known/matrix/client` | `{"m.homeserver": {"base_url": "https://matrix.example.com"}}` | Matrix 客户端(Robrix、Element) | + +如果使用反向代理,需确保这些端点被正确转发到 Palpo。参见[第 4.2 节](#42-反向代理caddy-示例)的代理配置。 + +### 3.4 TLS 配置 + +```toml +[tls] +enable = true +cert = "/path/to/fullchain.pem" +key = "/path/to/privkey.pem" +dual_protocol = false # 生产环境不要同时允许 HTTP 和 HTTPS +``` + +如果使用反向代理终止 TLS(推荐方式),可以在 Palpo 中禁用 `[tls]`,让代理处理证书。参见[第 4.2 节](#42-反向代理caddy-示例)。 + +### 3.5 在线状态与输入提示(联邦功能) + +这些设置控制跨联邦服务器的实时状态指示器: + +```toml +[presence] +allow_local = true # 本地在线状态(仅你的服务器) +allow_incoming = true # 接收远程服务器的在线状态更新 +allow_outgoing = true # 发送在线状态更新到远程服务器 + +[typing] +allow_incoming = true # 接收远程用户的输入提示 +allow_outgoing = true # 发送输入提示到远程用户 +federation_timeout = 30000 # 毫秒 +``` + +> **注意:** `[presence]` 下的 `allow_outgoing` 需要 `allow_local` 为 `true` 才能生效。 + +### 3.6 受信服务器 + +```toml +trusted_servers = ["matrix.org"] +``` + +受信服务器充当**公证服务器** -- 帮助验证其他服务器的签名密钥。这是 [Perspectives 密钥验证](https://spec.matrix.org/latest/server-server-api/#querying-keys-through-another-server)机制的一部分。`matrix.org` 是最常用的选择。 + +### 3.7 DNS 配置 + +这些是 `palpo.toml` 中的顶层设置(不在任何 `[section]` 内): + +```toml +# 将这些添加到 palpo.toml 的顶层(与 server_name 等同级) +query_over_tcp_only = true # 使用 TCP 进行 DNS 查询(在容器中更可靠) +query_all_nameservers = true # 查询所有配置的域名服务器 +ip_lookup_strategy = 5 # 5 = 先查 IPv4,再查 IPv6 +``` + +> **提示:** 如果在 Docker 中运行,建议设置 `query_over_tcp_only = true` 以避免容器网络中的 UDP DNS 解析问题。 + +--- + +## 4. 生产环境部署 + +本节介绍从本地部署升级到联邦部署所需的基础设施变更。 + +### 4.1 域名和 DNS 配置 + +1. **注册域名**(如 `example.com`) + +2. **创建 DNS A 记录**,指向你的服务器: + ``` + matrix.example.com. IN A 203.0.113.10 + ``` + +3. **可选:创建 SRV 记录**(用于非标准端口的联邦): + ``` + _matrix-fed._tcp.example.com. IN SRV 10 0 8448 matrix.example.com. + ``` + > 如果在 443 端口提供联邦服务且 `/.well-known/matrix/server` 响应正确,则不需要 SRV 记录。 + +### 4.2 反向代理(Caddy 示例) + +生产环境推荐使用反向代理。Caddy 通过 Let's Encrypt 自动管理 TLS 证书。 + +``` +matrix.example.com { + # Well-known 端点(联邦发现) + handle /.well-known/matrix/server { + respond `{"m.server":"matrix.example.com:443"}` + } + + handle /.well-known/matrix/client { + header Access-Control-Allow-Origin "*" + respond `{"m.homeserver":{"base_url":"https://matrix.example.com"}}` + } + + # 其他请求代理到 Palpo + reverse_proxy localhost:8008 +} +``` + +使用 Caddy 处理 TLS 时,可以在 `palpo.toml` 中禁用 TLS,让 Palpo 在内部使用纯 HTTP: + +```toml +[tls] +enable = false # Caddy 终止 TLS +``` + +> **Nginx 替代方案:** 如果使用 Nginx,需要单独管理 Let's Encrypt 证书(如使用 certbot),并配置 `ssl_certificate` / `ssl_certificate_key` 指令。 + +### 4.3 更新 Docker Compose + +与本地 `compose.yml` 相比的关键变更: + +```yaml +services: + palpo: + # ...(构建部分与本地相同)... + ports: + - "8008:8008" # 修改:Caddy 代理到此端口 + # 不再直接暴露 8128 + volumes: + - ./palpo.toml:/var/palpo/palpo.toml:ro + - ./appservices:/var/palpo/appservices:ro + - ./data/media:/var/palpo/media + # 新增:挂载 TLS 证书(仅当 Palpo 直接处理 TLS 时) + # - /etc/letsencrypt/live/matrix.example.com:/certs:ro + # ... 其余与本地相同 ... +``` + +整体 Docker Compose 结构与本地部署保持一致 -- PostgreSQL、Palpo、Octos。主要区别是: + +- `palpo.toml` 中的 `server_name` 使用真实域名 +- 端口映射变更(Caddy 在 443 代理到 Palpo 的 8008) +- TLS 由 Caddy 处理(或在 Palpo 直接处理 TLS 时挂载证书) +- `allow_registration = false`(先创建账号,然后锁定注册) + +### 4.4 更新 Appservice 注册 + +从 `127.0.0.1:8128` 切换到真实域名时,需要更新以下文件: + +**`appservices/octos-registration.yaml`** -- 更新正则表达式: + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:matrix\\.example\\.com" # 已修改 + - exclusive: true + regex: "@octosbot:matrix\\.example\\.com" # 已修改 +``` + +**`config/botfather.json`** -- 更新 `server_name`: + +```json +{ + "config": { + "channels": [{ + "type": "matrix", + "homeserver": "http://palpo:8008", + "server_name": "matrix.example.com", + "sender_localpart": "octosbot", + ... + }] + } +} +``` + +> **重要:** `botfather.json` 中的 `homeserver` URL 保持为 Docker 内部地址(`http://palpo:8008`)。只有 `server_name` 需要改为真实域名。 + +--- + +## 5. 使用联邦功能 + +联邦配置完成后,你可以与其他 Matrix 服务器上的用户和房间进行交互。 + +### 5.1 加入其他服务器上的房间 + +在 Robrix 中: + +1. 点击 **Join Room**(加入房间) +2. 输入其他服务器上的房间别名,例如 `#general:matrix.org` +3. 你的服务器通过联邦与 `matrix.org` 连接并加入该房间 +4. 来自所有参与服务器的用户消息实时显示 + + + +### 5.2 邀请其他服务器上的用户 + +1. 打开你服务器上的一个房间 +2. 点击 **Invite**(邀请) +3. 输入其他服务器上的用户 ID:`@friend:other-server.com` +4. 邀请通过联邦传送到远程服务器 +5. 远程用户接受邀请后加入你的房间 + +### 5.3 跨服务器 AI 机器人 + +启用联邦后,**其他服务器**上的用户也可以与你的 Octos 机器人交互: + +1. `matrix.org` 上的用户邀请 `@octosbot:matrix.example.com` 到他们的房间 +2. 邀请通过联邦传送到你的服务器 +3. Octos 接受邀请并加入房间 +4. 机器人响应消息 -- 即使房间在不同的服务器上 + +> **注意:** 要使此功能正常工作,`botfather.json` 中的 `allowed_senders` 必须为空数组 `[]`(允许所有用户)或明确包含远程用户的 Matrix ID(如 `@remoteuser:matrix.org`)。 + +--- + +## 6. 验证与故障排除 + +### 6.1 测试联邦 + +**检查 well-known 端点是否可访问:** + +```bash +# 服务器发现(其他服务器使用) +curl https://matrix.example.com/.well-known/matrix/server + +# 客户端发现(Robrix/Element 使用) +curl https://matrix.example.com/.well-known/matrix/client +``` + +**使用 Matrix Federation Tester:** + +访问 [https://federationtester.matrix.org](https://federationtester.matrix.org) 并输入你的域名(如 `matrix.example.com`)。它会检查: + +- DNS 解析 +- TLS 证书有效性 +- Well-known 端点响应 +- Server-Server API 可达性 +- 签名密钥验证 + +### 6.2 常见问题 + +| 症状 | 原因 | 解决方法 | +|------|------|---------| +| 无法加入其他服务器的房间 | 联邦未启用或端口被阻止 | 检查 `palpo.toml` 中 `[federation] enable = true`。确保防火墙开放端口 443 和 8448。 | +| "Unable to find signing key" | TLS 或 DNS 配置错误 | 验证 TLS 证书有效(非自签名)。检查 DNS 解析是否正确。运行 Federation Tester。 | +| Well-known 返回 404 | 反向代理未转发 | 检查 Caddy/Nginx 配置是否将 `/.well-known/matrix/*` 转发到 Palpo(或直接响应)。 | +| 远程用户看不到资料 | 资料查询被禁用 | 在 `[federation]` 中设置 `allow_inbound_profile_lookup = true`。 | +| 连接远程服务器超时 | 防火墙或 DNS 问题 | 检查出站连接。尝试设置 `query_over_tcp_only = true`。验证服务器能否通过 8448 端口访问其他 Matrix 服务器。 | +| 机器人不响应联邦用户 | `allowed_senders` 过滤 | 在 `botfather.json` 中将 `allowed_senders` 设为 `[]` 以允许所有用户,或添加远程用户的完整 Matrix ID。 | + +### 6.3 调试命令 + +```bash +# 查看 Palpo 联邦相关日志 +docker compose logs palpo | grep -i federation + +# 检查 Palpo 是否能访问其他服务器 +docker compose exec palpo curl -sf https://matrix.org/.well-known/matrix/server + +# 从外部验证 well-known 端点 +curl -sf https://matrix.example.com/.well-known/matrix/server +curl -sf https://matrix.example.com/.well-known/matrix/client + +# 检查 TLS 证书 +openssl s_client -connect matrix.example.com:443 -servername matrix.example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates +``` + +--- + +## 7. 延伸阅读 + +- **Matrix 联邦规范:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- 服务器间通信的协议规范。 +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- 在线工具,验证你的联邦配置。 +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo 服务器源码和文档。 +- **Let's Encrypt:** [letsencrypt.org](https://letsencrypt.org/) -- 免费、自动化的 TLS 证书。 +- **Caddy:** [caddyserver.com](https://caddyserver.com/) -- 自动 HTTPS 的反向代理。 + +--- + +*本指南覆盖 2026 年 4 月的联邦配置。获取最新更新,请查看各项目仓库。* diff --git a/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md new file mode 100644 index 000000000..a14a961b9 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md @@ -0,0 +1,417 @@ +# Federation: Cross-Server Communication + +[中文版](04-federation-with-palpo-zh.md) + +> **Goal:** After following this guide, you will have Palpo configured for Matrix federation, enabling users on your server to communicate with users on other Matrix servers (like matrix.org), and allowing remote users to access your Octos AI bots. + +This guide covers Matrix **federation** -- connecting your Palpo homeserver with other Matrix servers so users on different servers can communicate with each other. + +> **Prerequisite:** You should already have a working local deployment. If not, see [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) first. + +--- + +## Table of Contents + +1. [What is Matrix Federation?](#1-what-is-matrix-federation) +2. [Prerequisites for Federation](#2-prerequisites-for-federation) +3. [Palpo Federation Configuration](#3-palpo-federation-configuration) +4. [Production Deployment](#4-production-deployment) +5. [Using Federation](#5-using-federation) +6. [Verification and Troubleshooting](#6-verification-and-troubleshooting) +7. [Further Reading](#7-further-reading) + +--- + +## 1. What is Matrix Federation? + +Matrix is a **decentralized** communication protocol. Each organization can run its own homeserver, and federation allows users on different homeservers to communicate seamlessly. + +Think of it like email: + +- `@alice:server-a.com` can chat with `@bob:server-b.com` +- Each server stores its own users' data +- Messages are replicated across all servers participating in a conversation +- No single point of control -- if one server goes down, others keep working + +In the local deployment guide, everything runs on `127.0.0.1:8128` -- a single isolated server. Federation opens your server to the wider Matrix network. + +``` + Server A Server B +┌──────────┐ Federation API ┌──────────┐ +│ Palpo │ ◄────────────────► │ Synapse │ +│ + Octos │ (port 8448) │ or any │ +│ + Robrix│ │ Matrix │ +└──────────┘ └──────────┘ + @alice:server-a.com @bob:server-b.com + └─── can chat with ─────────────┘ +``` + +--- + +## 2. Prerequisites for Federation + +Federation has requirements beyond a local deployment: + +| Requirement | Local Deployment | Federated Deployment | +|-------------|-----------------|---------------------| +| Domain name | Not needed (`127.0.0.1`) | Required (e.g., `matrix.example.com`) | +| TLS certificate | Not needed (HTTP) | Required (HTTPS, Let's Encrypt recommended) | +| Port 443 | Not needed | Open (Client-Server API) | +| Port 8448 | Not needed | Open (Server-Server Federation API) | +| Reverse proxy | Not needed | Recommended (Caddy or Nginx) | +| DNS records | Not needed | A record required, SRV record optional | + +> **Self-signed certificates will NOT work** for federation. Other Matrix servers will refuse to connect. Use [Let's Encrypt](https://letsencrypt.org/) for free, trusted certificates. + +--- + +## 3. Palpo Federation Configuration + +### 3.1 Basic Settings (`palpo.toml`) + +Here is a federation-ready `palpo.toml`. Changes from the local deployment are marked with comments: + +```toml +# CHANGED: Use your real domain instead of 127.0.0.1:8128 +server_name = "matrix.example.com" + +# CHANGED: Disable open registration in production. +# Create accounts first, then set to false. +allow_registration = false + +enable_admin_room = true +appservice_registration_dir = "/var/palpo/appservices" + +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "json" # CHANGED: Use "json" for production + +[db] +url = "postgres://palpo:YOUR_STRONG_PASSWORD@palpo_postgres:5432/palpo" +pool_size = 10 + +# CHANGED: Use real domain for discovery +[well_known] +server = "matrix.example.com:443" +client = "https://matrix.example.com" + +# --- Federation settings (NEW) --- +[federation] +enable = true +allow_inbound_profile_lookup = true + +# Optional: restrict federation to specific servers +# allowed_servers = ["matrix.org", "*.trusted.com"] +# denied_servers = ["evil.com"] + +[tls] +enable = true +cert = "/path/to/fullchain.pem" +key = "/path/to/privkey.pem" + +[presence] +allow_local = true +allow_incoming = true +allow_outgoing = true + +[typing] +allow_incoming = true +allow_outgoing = true +federation_timeout = 30000 + +trusted_servers = ["matrix.org"] +``` + +### 3.2 Federation Settings Reference + +The `[federation]` section controls how your server interacts with other Matrix servers: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enable` | bool | `true` | Master switch for federation. Set to `false` to run a completely isolated server. | +| `allow_loopback` | bool | `false` | Allow federation requests to self. For development only. | +| `allow_device_name` | bool | `false` | Expose device display names to federated users. Disabled for privacy. | +| `allow_inbound_profile_lookup` | bool | `true` | Allow remote servers to query local user profiles. Disabling hides display names from federated users. | +| `allowed_servers` | list | None | Allowlist: ONLY these servers can federate with yours. Supports wildcards (e.g., `*.trusted.com`). When unset, all servers are allowed. | +| `denied_servers` | list | `[]` | Denylist: block specific servers. **Takes precedence** over `allowed_servers`. Supports wildcards. | + +### 3.3 Server Discovery (`[well_known]`) + +The `[well_known]` section is **critical** for federation. It tells other homeservers how to find yours. + +Palpo automatically serves these endpoints: + +| Endpoint | Response | Used by | +|----------|----------|---------| +| `/.well-known/matrix/server` | `{"m.server": "matrix.example.com:443"}` | Other homeservers (federation) | +| `/.well-known/matrix/client` | `{"m.homeserver": {"base_url": "https://matrix.example.com"}}` | Matrix clients (Robrix, Element) | + +If you are behind a reverse proxy, ensure these endpoints are forwarded correctly to Palpo. See [Section 4.2](#42-reverse-proxy-caddy-example) for proxy configuration. + +### 3.4 TLS Configuration + +```toml +[tls] +enable = true +cert = "/path/to/fullchain.pem" +key = "/path/to/privkey.pem" +dual_protocol = false # Do NOT allow HTTP alongside HTTPS in production +``` + +If you use a reverse proxy that terminates TLS (recommended), you can leave `[tls]` disabled in Palpo and let the proxy handle certificates. See [Section 4.2](#42-reverse-proxy-caddy-example). + +### 3.5 Presence and Typing (Federated Features) + +These settings control real-time status indicators across federated servers: + +```toml +[presence] +allow_local = true # Local presence (your server only) +allow_incoming = true # Receive presence updates from remote servers +allow_outgoing = true # Send presence updates to remote servers + +[typing] +allow_incoming = true # Receive typing indicators from remote users +allow_outgoing = true # Send typing indicators to remote users +federation_timeout = 30000 # Milliseconds +``` + +> **Note:** `allow_outgoing` under `[presence]` requires `allow_local` to be `true`. + +### 3.6 Trusted Servers + +```toml +trusted_servers = ["matrix.org"] +``` + +Trusted servers act as **notary servers** -- they help verify signing keys from other homeservers. This is part of the [Perspectives key verification](https://spec.matrix.org/latest/server-server-api/#querying-keys-through-another-server) mechanism. `matrix.org` is the most common choice. + +### 3.7 DNS Configuration + +These are top-level settings in `palpo.toml` (not inside any `[section]`): + +```toml +# Add these at the top level of palpo.toml (alongside server_name, etc.) +query_over_tcp_only = true # Use TCP for DNS (more reliable in containers) +query_all_nameservers = true # Query all configured nameservers +ip_lookup_strategy = 5 # 5 = IPv4 first, then IPv6 +``` + +> **Tip:** If running in Docker, `query_over_tcp_only = true` is recommended to avoid UDP DNS resolution issues in container networks. + +--- + +## 4. Production Deployment + +This section covers the infrastructure changes needed to go from local to federated deployment. + +### 4.1 Domain and DNS Setup + +1. **Register a domain** (e.g., `example.com`) + +2. **Create a DNS A record** pointing to your server: + ``` + matrix.example.com. IN A 203.0.113.10 + ``` + +3. **Optional: Create an SRV record** for federation on a non-standard port: + ``` + _matrix-fed._tcp.example.com. IN SRV 10 0 8448 matrix.example.com. + ``` + > The SRV record is not needed if you serve federation on port 443 and have a proper `/.well-known/matrix/server` response. + +### 4.2 Reverse Proxy (Caddy Example) + +Using a reverse proxy is recommended for production. Caddy automatically manages TLS certificates via Let's Encrypt. + +``` +matrix.example.com { + # Well-known endpoints (federation discovery) + handle /.well-known/matrix/server { + respond `{"m.server":"matrix.example.com:443"}` + } + + handle /.well-known/matrix/client { + header Access-Control-Allow-Origin "*" + respond `{"m.homeserver":{"base_url":"https://matrix.example.com"}}` + } + + # Proxy everything else to Palpo + reverse_proxy localhost:8008 +} +``` + +With Caddy handling TLS, you can disable `[tls]` in `palpo.toml` and let Palpo listen on plain HTTP internally: + +```toml +[tls] +enable = false # Caddy terminates TLS +``` + +> **Nginx alternative:** If using Nginx, you need to manage Let's Encrypt certificates separately (e.g., with certbot) and configure `ssl_certificate` / `ssl_certificate_key` directives. + +### 4.3 Updated Docker Compose + +Key changes from the local `compose.yml`: + +```yaml +services: + palpo: + # ... (same build section as local) ... + ports: + - "8008:8008" # CHANGED: Caddy proxies to this port + # No longer exposing 8128 directly + volumes: + - ./palpo.toml:/var/palpo/palpo.toml:ro + - ./appservices:/var/palpo/appservices:ro + - ./data/media:/var/palpo/media + # ADDED: Mount TLS certs (only if Palpo handles TLS directly) + # - /etc/letsencrypt/live/matrix.example.com:/certs:ro + # ... rest same as local ... +``` + +The full Docker Compose structure remains the same as the local deployment -- PostgreSQL, Palpo, and Octos. The key differences are: + +- `server_name` in `palpo.toml` uses your real domain +- Port mapping changes (Caddy on 443 proxies to Palpo on 8008) +- TLS handled by Caddy (or mounted certificates if Palpo handles TLS) +- `allow_registration = false` (create accounts first, then lock down) + +### 4.4 Appservice Registration Updates + +When switching from `127.0.0.1:8128` to a real domain, update these files: + +**`appservices/octos-registration.yaml`** -- Update regex patterns: + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:matrix\\.example\\.com" # CHANGED + - exclusive: true + regex: "@octosbot:matrix\\.example\\.com" # CHANGED +``` + +**`config/botfather.json`** -- Update `server_name`: + +```json +{ + "config": { + "channels": [{ + "type": "matrix", + "homeserver": "http://palpo:8008", + "server_name": "matrix.example.com", + "sender_localpart": "octosbot", + ... + }] + } +} +``` + +> **Important:** The `homeserver` URL in `botfather.json` stays as the internal Docker address (`http://palpo:8008`). Only `server_name` changes to the real domain. + +--- + +## 5. Using Federation + +Once federation is configured, you can interact with users and rooms on other Matrix servers. + +### 5.1 Join Rooms on Other Servers + +From Robrix: + +1. Click **Join Room** +2. Enter a room alias from another server, e.g., `#general:matrix.org` +3. Your server federates with `matrix.org` to join the room +4. Messages from users on all participating servers appear in real time + + + +### 5.2 Invite Users from Other Servers + +1. Open a room on your server +2. Click **Invite** +3. Enter a user ID from another server: `@friend:other-server.com` +4. The invitation travels via federation to the remote server +5. When the remote user accepts, they join your room + +### 5.3 Cross-Server AI Bot + +With federation, users from **other servers** can also interact with your Octos bot: + +1. A user on `matrix.org` invites `@octosbot:matrix.example.com` to their room +2. The invitation federates to your server +3. Octos accepts the invitation and joins the room +4. The bot responds to messages -- even though the room lives on a different server + +> **Note:** For this to work, `allowed_senders` in `botfather.json` must either be empty `[]` (allow all users) or explicitly include the remote user's Matrix ID (e.g., `@remoteuser:matrix.org`). + +--- + +## 6. Verification and Troubleshooting + +### 6.1 Test Federation + +**Check that well-known endpoints are accessible:** + +```bash +# Server discovery (used by other homeservers) +curl https://matrix.example.com/.well-known/matrix/server + +# Client discovery (used by Robrix/Element) +curl https://matrix.example.com/.well-known/matrix/client +``` + +**Use the Matrix Federation Tester:** + +Visit [https://federationtester.matrix.org](https://federationtester.matrix.org) and enter your domain (e.g., `matrix.example.com`). It checks: + +- DNS resolution +- TLS certificate validity +- Well-known endpoint responses +- Server-Server API reachability +- Signing key verification + +### 6.2 Common Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Cannot join rooms on other servers | Federation disabled or ports blocked | Check `[federation] enable = true` in `palpo.toml`. Ensure ports 443 and 8448 are open in your firewall. | +| "Unable to find signing key" | TLS or DNS misconfigured | Verify your TLS certificate is valid (not self-signed). Check that DNS resolves correctly. Run the Federation Tester. | +| Well-known returns 404 | Reverse proxy not forwarding | Check your Caddy/Nginx config forwards `/.well-known/matrix/*` to Palpo (or responds directly). | +| Remote users cannot see profiles | Profile lookup disabled | Set `allow_inbound_profile_lookup = true` in `[federation]`. | +| Connection timeouts to remote servers | Firewall or DNS issues | Check outbound connectivity. Try setting `query_over_tcp_only = true`. Verify your server can reach other Matrix servers on port 8448. | +| Bot does not respond to federated users | `allowed_senders` filtering | Set `allowed_senders` to `[]` in `botfather.json` to allow all users, or add the remote user's full Matrix ID. | + +### 6.3 Debug Commands + +```bash +# Check Palpo logs for federation activity +docker compose logs palpo | grep -i federation + +# Check if Palpo can reach other servers +docker compose exec palpo curl -sf https://matrix.org/.well-known/matrix/server + +# Verify your well-known endpoints externally +curl -sf https://matrix.example.com/.well-known/matrix/server +curl -sf https://matrix.example.com/.well-known/matrix/client + +# Check TLS certificate +openssl s_client -connect matrix.example.com:443 -servername matrix.example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates +``` + +--- + +## 7. Further Reading + +- **Matrix Federation Specification:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- the protocol specification for server-to-server communication. +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- online tool to verify your federation setup. +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver source code and documentation. +- **Let's Encrypt:** [letsencrypt.org](https://letsencrypt.org/) -- free, automated TLS certificates. +- **Caddy:** [caddyserver.com](https://caddyserver.com/) -- reverse proxy with automatic HTTPS. + +--- + +*This guide covers federation configuration as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix/getting-started-with-robrix-zh.md b/docs/robrix/getting-started-with-robrix-zh.md new file mode 100644 index 000000000..c87c0deea --- /dev/null +++ b/docs/robrix/getting-started-with-robrix-zh.md @@ -0,0 +1,72 @@ +# Robrix 快速开始 + +[English Version](getting-started-with-robrix.md) + +> **目标:** 按照本指南操作后,你将完成 Robrix 的安装和运行,连接到 Matrix 服务器,并可以开始聊天。 + +Robrix 是一个用 Rust 编写的跨平台 Matrix 聊天客户端,基于 [Makepad](https://github.com/makepad/makepad/) UI 框架。原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 + +--- + +## 下载预编译版本(推荐) + +从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载最新版本。支持 macOS、Linux 和 Windows。 + +## 从源码构建 + +### 前提条件 + +- [Rust](https://www.rust-lang.org/tools/install)(最新稳定版) +- Linux 上需要安装系统依赖: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` + +### 桌面端(macOS / Linux / Windows) + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2 +cargo run --release +``` + +### 移动端 + +Android 和 iOS 构建方法请参考 [Robrix README — 构建与运行](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop)。 + +--- + +## 连接 Matrix 服务器 + +启动 Robrix 后,登录界面底部有一个 **Homeserver URL** 输入框。 + +- **留空** 默认连接 `matrix.org`(公共服务器) +- **输入自定义 URL** 连接其他 Matrix 兼容服务器: + - 本地 Palpo 实例:`http://127.0.0.1:8128` + - 远程服务器:`https://your.server.name` + +> **注意:** Robrix 要求主服务器支持 [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync)。Palpo 原生支持此功能;其他服务器请查阅其文档。 + +## 注册或登录 + +**新账号(服务器允许注册时):** + +1. 输入**用户名**和**密码** +2. 确认密码 +3. 设置 **Homeserver URL** +4. 点击 **Sign up** + +**已有账号:** + +1. 输入**用户名**和**密码** +2. 设置 **Homeserver URL** +3. 点击 **Log in** + +登录后你会看到房间列表。你可以加入房间、创建新房间并开始聊天。 + +--- + +## 下一步? + +- **只是聊天?** 你已经准备好了——加入房间,和 Matrix 网络上的人交流。 +- **想要 AI 机器人?** 查看 [Robrix + Palpo + Octos 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md),搭建你自己的 AI 聊天系统。 diff --git a/docs/robrix/getting-started-with-robrix.md b/docs/robrix/getting-started-with-robrix.md new file mode 100644 index 000000000..b6fa2380a --- /dev/null +++ b/docs/robrix/getting-started-with-robrix.md @@ -0,0 +1,72 @@ +# Getting Started with Robrix + +[中文版](getting-started-with-robrix-zh.md) + +> **Goal:** After following this guide, you will have Robrix installed and running, connected to a Matrix server, and ready to chat. + +Robrix is a cross-platform Matrix chat client written in Rust using the [Makepad](https://github.com/makepad/makepad/) UI framework. It runs natively on macOS, Linux, Windows, Android, and iOS. + +--- + +## Download a Pre-built Release (Recommended) + +Download the latest version from the [Robrix Releases page](https://github.com/Project-Robius-China/robrix2/releases). Available for macOS, Linux, and Windows. + +## Build from Source + +### Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (latest stable) +- On Linux, install system dependencies: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` + +### Desktop (macOS / Linux / Windows) + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2 +cargo run --release +``` + +### Mobile + +For Android and iOS builds, see the [Robrix README — Building & Running](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop). + +--- + +## Connect to a Matrix Server + +When you launch Robrix, you'll see the login screen with a **Homeserver URL** field at the bottom. + +- **Leave it empty** to connect to `matrix.org` (the default public server) +- **Enter a custom URL** to connect to any Matrix-compatible server: + - Local Palpo instance: `http://127.0.0.1:8128` + - Remote server: `https://your.server.name` + +> **Note:** Robrix requires the homeserver to support [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync). Palpo supports this natively; for other servers, check their documentation. + +## Register or Log In + +**New account (if the server allows registration):** + +1. Enter a **username** and **password** +2. Confirm the password +3. Set the **Homeserver URL** +4. Click **Sign up** + +**Existing account:** + +1. Enter your **username** and **password** +2. Set the **Homeserver URL** +3. Click **Log in** + +After login, you'll see your room list. From here you can join rooms, create new rooms, and start chatting. + +--- + +## What's Next? + +- **Just chatting?** You're all set — join rooms and talk to people on the Matrix network. +- **Want AI bots?** See the [Robrix + Palpo + Octos deployment guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) to set up your own AI chat system. diff --git a/docs/examples/.env.example b/palpo-and-octos-deploy/.env.example similarity index 100% rename from docs/examples/.env.example rename to palpo-and-octos-deploy/.env.example diff --git a/docs/examples/.gitignore b/palpo-and-octos-deploy/.gitignore similarity index 100% rename from docs/examples/.gitignore rename to palpo-and-octos-deploy/.gitignore diff --git a/docs/examples/appservices/octos-registration.yaml b/palpo-and-octos-deploy/appservices/octos-registration.yaml similarity index 100% rename from docs/examples/appservices/octos-registration.yaml rename to palpo-and-octos-deploy/appservices/octos-registration.yaml diff --git a/docs/examples/compose.yml b/palpo-and-octos-deploy/compose.yml similarity index 98% rename from docs/examples/compose.yml rename to palpo-and-octos-deploy/compose.yml index ed2154df9..a0a570ec1 100644 --- a/docs/examples/compose.yml +++ b/palpo-and-octos-deploy/compose.yml @@ -61,7 +61,6 @@ services: - ./palpo.toml:/var/palpo/palpo.toml:ro - ./appservices:/var/palpo/appservices:ro - ./data/media:/var/palpo/media - - ./static:/var/palpo/static:ro depends_on: palpo_postgres: condition: service_healthy diff --git a/docs/examples/config/botfather.json b/palpo-and-octos-deploy/config/botfather.json similarity index 100% rename from docs/examples/config/botfather.json rename to palpo-and-octos-deploy/config/botfather.json diff --git a/docs/examples/config/octos.json b/palpo-and-octos-deploy/config/octos.json similarity index 100% rename from docs/examples/config/octos.json rename to palpo-and-octos-deploy/config/octos.json diff --git a/docs/examples/palpo.toml b/palpo-and-octos-deploy/palpo.toml similarity index 100% rename from docs/examples/palpo.toml rename to palpo-and-octos-deploy/palpo.toml diff --git a/docs/examples/setup.sh b/palpo-and-octos-deploy/setup.sh similarity index 100% rename from docs/examples/setup.sh rename to palpo-and-octos-deploy/setup.sh From bee2a808be82cfb9f5bd6e8569a8dfd7ef627651 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 15:48:32 +0800 Subject: [PATCH 066/283] docs: add Octos bot commands, BotFather vs child bots, public/private visibility Add Section 7 to usage guides (EN/ZH) covering: - BotFather slash commands (/createbot, /deletebot, /listbots, /bothelp) - BotFather vs child bot comparison table - Public/private bot visibility model with examples - Owner-based delete permissions and operator override Based on octos-org/octos#69 (owner & visibility model). --- ...03-using-robrix-with-palpo-and-octos-zh.md | 63 ++++++++++++++++++- .../03-using-robrix-with-palpo-and-octos.md | 63 ++++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md index 059a7d070..d2412ebc8 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md @@ -14,6 +14,7 @@ | 创建账号 | [第 3 节](#3-注册账号) | | 与 AI 机器人聊天 | [第 5 节](#5-与-ai-机器人聊天) | | 创建专业化机器人 | [第 6 节](#6-机器人管理高级功能) | +| 机器人命令与公开/私有设置 | [第 7 节](#7-octos-机器人命令与行为) | --- @@ -174,7 +175,65 @@ Octos 支持"BotFather"模式:主机器人(`@octosbot`)可以创建**子 --- -## 7. 使用技巧 +## 7. Octos 机器人命令与行为 + +Octos 机器人支持在聊天房间中直接输入少量斜杠命令。本节只保留最主要的 BotFather 管理命令,以及子机器人的公开/私有可见性说明。 + +### 7.1 BotFather 管理命令 + +这些命令只对 BotFather 机器人(`@octosbot`)有效,子机器人不会响应。 + +| 命令 | 说明 | 示例 | +|------|------|------| +| `/createbot <用户名> <显示名> [选项]` | 创建子机器人。选项:`--public` 或 `--private`(默认),`--prompt "..."` 设置系统提示词。 | `/createbot weather Weather Bot --public --prompt "You are a weather assistant"` | +| `/deletebot ` | 删除子机器人。只有创建者(或管理员)可以删除。 | `/deletebot @octosbot_weather:127.0.0.1:8128` | +| `/listbots` | 列出所有公开机器人以及你自己创建的私有机器人。 | `/listbots` | +| `/bothelp` | 显示机器人管理命令的帮助信息。 | `/bothelp` | + +> **注意:** 你也可以通过 Robrix 的 UI 创建机器人(第 6.2 节),它提供了表单形式的替代方案。 + +### 7.2 BotFather 与子机器人的区别 + +BotFather 和子机器人的角色不同: + +| | BotFather(`@octosbot`) | 子机器人(`@octosbot_<名称>`) | +|---|---|---| +| **角色** | 管理入口 + 通用 AI 聊天 | 专业化 AI 助手 | +| **管理命令** | 支持(`/createbot`、`/deletebot`、`/listbots`) | 不支持 | +| **自定义系统提示词** | 使用默认提示词 | 拥有独立的专用提示词 | +| **能否创建其他机器人** | 能 | 不能 | +| **Matrix 用户 ID** | `@octosbot:server_name` | `@octosbot_<用户名>:server_name` | + +**何时使用哪个:** +- 使用 **BotFather** 进行通用 AI 对话,以及管理(创建/删除)其他机器人。 +- 使用**子机器人**来完成特定任务(翻译、编程辅助、文字审阅等),它们拥有固定的系统提示词。 + +### 7.3 公开与私有机器人 + +创建子机器人时,你可以设置其**可见性**: + +- **私有(默认):** 只有创建者可以邀请和使用此机器人。其他用户通过 `/listbots` 看不到它,如果尝试邀请它,机器人会短暂加入房间、发送拒绝消息,然后离开。 +- **公开:** 服务器上的任何用户都可以通过 `/listbots` 发现此机器人,将其邀请到房间并与之对话。 + +**创建私有机器人(默认):** +``` +/createbot myhelper My Helper --prompt "You are my personal assistant" +``` + +**创建公开机器人:** +``` +/createbot translator Translator Bot --public --prompt "Translate all messages to English" +``` + +**谁可以删除机器人:** +- 机器人的**创建者**(所有者)可以随时删除它。 +- **管理员**(`botfather.json` 中 `allowed_senders` 列表中的用户)可以删除任何机器人,作为紧急覆盖权限。 + +> **提示:** 建议先创建私有机器人供个人使用。只在你想让服务器上其他用户也能使用时,才将机器人设为公开。 + +--- + +## 8. 使用技巧 - **在一个房间中使用多个机器人。** 你可以在同一个房间中邀请多个机器人,每个机器人根据自己的系统提示词独立响应。这对于对比不同模型的输出或构建多智能体工作流很有用。 @@ -192,7 +251,7 @@ Octos 支持"BotFather"模式:主机器人(`@octosbot`)可以创建**子 --- -## 8. 常用 Matrix ID 参考 +## 9. 常用 Matrix ID 参考 本地部署(`server_name = 127.0.0.1:8128`)下的常用 ID: diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md index 7629001a1..f58e734ec 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md @@ -14,6 +14,7 @@ This guide walks you through using Robrix as a Matrix client connected to a Palp | Create an account | [Section 3](#3-registration) | | Chat with the AI bot | [Section 5](#5-chatting-with-the-ai-bot) | | Create specialized bots | [Section 6](#6-bot-management-advanced) | +| Bot commands and public/private bots | [Section 7](#7-octos-bot-commands-and-behavior) | --- @@ -175,7 +176,65 @@ After creating a child bot, use it like the main bot: --- -## 7. Tips and Common Patterns +## 7. Octos Bot Commands and Behavior + +Octos bots support a small set of slash commands that you type directly in the chat room. In this guide, we focus on the main BotFather management commands and the public/private visibility model for child bots. + +### 7.1 BotFather Management Commands + +These commands only work when sent to the BotFather bot (`@octosbot`). Child bots do not respond to them. + +| Command | Description | Example | +|---------|-------------|---------| +| `/createbot [flags]` | Create a new child bot. Flags: `--public` or `--private` (default), `--prompt "..."` for system prompt. | `/createbot weather Weather Bot --public --prompt "You are a weather assistant"` | +| `/deletebot ` | Delete a child bot. Only the bot's creator (or the operator) can delete it. | `/deletebot @octosbot_weather:127.0.0.1:8128` | +| `/listbots` | List all public bots plus your own private bots. | `/listbots` | +| `/bothelp` | Show help text for bot management commands. | `/bothelp` | + +> **Note:** You can also create bots through Robrix's UI (Section 6.2), which provides a form-based alternative to these slash commands. + +### 7.2 BotFather vs Child Bots + +BotFather and child bots serve different roles: + +| | BotFather (`@octosbot`) | Child Bot (`@octosbot_`) | +|---|---|---| +| **Role** | Management gateway + general AI chat | Specialized AI assistant | +| **Bot management commands** | Yes (`/createbot`, `/deletebot`, `/listbots`) | No | +| **Custom system prompt** | Uses default prompt | Has its own dedicated prompt | +| **Can create other bots** | Yes | No | +| **Matrix user ID** | `@octosbot:server_name` | `@octosbot_:server_name` | + +**When to use which:** +- Use **BotFather** for general-purpose AI chat and for managing (creating/deleting) other bots. +- Use **child bots** when you need a dedicated assistant for a specific task (translation, coding help, writing review, etc.) with a fixed system prompt. + +### 7.3 Public vs Private Bots + +When creating a child bot, you can set its **visibility**: + +- **Private (default):** Only the creator can invite and chat with this bot. Other users cannot discover it via `/listbots`, and if they try to invite it, the bot will join briefly, send a rejection message, then leave the room. +- **Public:** Any user on the server can discover the bot via `/listbots`, invite it to rooms, and chat with it. + +**Creating a private bot (default):** +``` +/createbot myhelper My Helper --prompt "You are my personal assistant" +``` + +**Creating a public bot:** +``` +/createbot translator Translator Bot --public --prompt "Translate all messages to English" +``` + +**Who can delete a bot:** +- The **creator** (owner) of the bot can always delete it. +- The **operator** (anyone in `allowed_senders` in `botfather.json`) can delete any bot as an override. + +> **Tip:** Start with private bots for personal use. Make a bot public only when you want other users on the server to use it. + +--- + +## 8. Tips - **Multiple bots in one room.** You can invite several bots into the same room. Each bot responds independently based on its own system prompt. This is useful for comparing outputs or building multi-agent workflows. @@ -193,7 +252,7 @@ After creating a child bot, use it like the main bot: --- -## 8. Common Matrix IDs Reference +## 9. Common Matrix IDs Reference For a local deployment with `server_name = 127.0.0.1:8128`: From c1ae1adf4f39167c3465b5476c3e614cf5b99480 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 16:45:03 +0800 Subject: [PATCH 067/283] docs: add screenshots, bot naming rules, extract palpo.Dockerfile - Add login screen and registration screenshots (compressed via pngquant) - Add bot naming explanation in usage guide Section 5.2 - Extract Palpo Dockerfile from compose.yml inline to standalone file - Update compose.yml to reference palpo.Dockerfile --- docs/images/register-account.png | Bin 61809 -> 10758 bytes .../01-deploying-palpo-and-octos-zh.md | 2 +- .../01-deploying-palpo-and-octos.md | 2 +- ...03-using-robrix-with-palpo-and-octos-zh.md | 74 +++++++++++------- .../03-using-robrix-with-palpo-and-octos.md | 17 +++- docs/robrix/getting-started-with-robrix-zh.md | 8 +- docs/robrix/getting-started-with-robrix.md | 2 + palpo-and-octos-deploy/compose.yml | 16 +--- palpo-and-octos-deploy/palpo.Dockerfile | 20 +++++ 9 files changed, 87 insertions(+), 54 deletions(-) create mode 100644 palpo-and-octos-deploy/palpo.Dockerfile diff --git a/docs/images/register-account.png b/docs/images/register-account.png index 1b7d0af56710fab1cb8059503271050504d4b93f..87241a8eba9e4448a866e5a0addd20de2d2f43e2 100644 GIT binary patch literal 10758 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 fYQB26LmzEq9)A=HdSW%OFhm literal 61809 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%0DVO_C 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 diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md index 70d7fa016..cdaabb7f5 100644 --- a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md @@ -125,7 +125,7 @@ palpo-and-octos-deploy/ ├── setup.sh # 一次性初始化脚本 ├── .env.example # 环境变量模板 ├── palpo.toml # Palpo 主服务器配置 -├── palpo.Dockerfile # Palpo 构建指令(多架构支持) +├── palpo.Dockerfile # Palpo Docker 构建(多阶段,release 模式) ├── appservices/ │ └── octos-registration.yaml # 应用服务注册文件(连接 Palpo <-> Octos) ├── config/ diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md index 62521ae1f..2d7b971ab 100644 --- a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md @@ -125,7 +125,7 @@ palpo-and-octos-deploy/ ├── setup.sh # One-time setup script ├── .env.example # Environment variables template ├── palpo.toml # Palpo homeserver configuration -├── palpo.Dockerfile # Palpo build instructions (multi-arch) +├── palpo.Dockerfile # Palpo Docker build (multi-stage, release) ├── appservices/ │ └── octos-registration.yaml # Appservice registration (links Palpo <-> Octos) ├── config/ diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md index d2412ebc8..84b64f9ed 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md @@ -8,12 +8,12 @@ **快速索引** -| 你想做什么 | 跳转到 | -|---|---| -| 连接到服务器 | [第 2 节](#2-连接到-palpo) | -| 创建账号 | [第 3 节](#3-注册账号) | -| 与 AI 机器人聊天 | [第 5 节](#5-与-ai-机器人聊天) | -| 创建专业化机器人 | [第 6 节](#6-机器人管理高级功能) | +| 你想做什么 | 跳转到 | +| ------------------------- | --------------------------------- | +| 连接到服务器 | [第 2 节](#2-连接到-palpo) | +| 创建账号 | [第 3 节](#3-注册账号) | +| 与 AI 机器人聊天 | [第 5 节](#5-与-ai-机器人聊天) | +| 创建专业化机器人 | [第 6 节](#6-机器人管理高级功能) | | 机器人命令与公开/私有设置 | [第 7 节](#7-octos-机器人命令与行为) | --- @@ -37,7 +37,7 @@ 2. 输入 `http://127.0.0.1:8128`(本地部署)。 3. 如果是远程服务器,输入 `https://your.server.name` 或 `http://服务器IP:8128`。 - +![Robrix 登录界面 — 在底部输入 Homeserver URL](../images/login-screen.png) > **注意:** 如果 Homeserver URL 留空,Robrix 会默认连接到 `matrix.org`。你必须填写此字段才能连接到自己的 Palpo 服务器。 @@ -64,7 +64,7 @@ 如果你已有账号: 1. 输入 **用户名** 和 **密码**。 -2. 输入 **Homeserver URL**:`http://127.0.0.1:8128`。 +2. 输入 **Homeserver URL**:`http://127.0.0.1:8128` 3. 点击 **Log in(登录)**。 登录后会看到房间列表。新账号的房间列表是空的。 @@ -94,7 +94,20 @@ -> **提示:** 如果你使用了不同的 `server_name` 进行部署,请相应替换机器人 ID 中的 `127.0.0.1:8128`。例如,如果服务器的 `server_name = chat.example.com`,则机器人 ID 为 `@octosbot:chat.example.com`。 +> **这个机器人名字是怎么来的?** BotFather 的 Matrix ID 由两个配置值组合而成: +> +> | 组成部分 | 值 | 配置位置 | +> | ------------------------ | -------------------------------------- | --------------------------------------------------------------------------- | +> | 用户名(localpart) | `octosbot` | `octos-registration.yaml` 和 `botfather.json` 中的 `sender_localpart` | +> | 服务器域名 | `127.0.0.1:8128` | `palpo.toml` 中的 `server_name` | +> | **完整 Matrix ID** | **`@octosbot:127.0.0.1:8128`** | | +> | 显示名称(房间内显示) | `BotFather` | `botfather.json` 中的 `name` | +> +> 通过 `/createbot` 创建的子机器人遵循类似规则。`botfather.json` 中的 `user_prefix` 字段(默认值:`octosbot_`)会自动拼接在你指定的用户名前面: +> +> `/createbot weather Weather Bot` → Matrix ID:`@octosbot_weather:127.0.0.1:8128` +> +> 如果你在生产环境中更改了 `server_name`,所有机器人 ID 都会随之改变。你还需要同步更新 `octos-registration.yaml` 中的命名空间正则表达式。 ### 5.3 开始聊天 @@ -183,12 +196,12 @@ Octos 机器人支持在聊天房间中直接输入少量斜杠命令。本节 这些命令只对 BotFather 机器人(`@octosbot`)有效,子机器人不会响应。 -| 命令 | 说明 | 示例 | -|------|------|------| +| 命令 | 说明 | 示例 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `/createbot <用户名> <显示名> [选项]` | 创建子机器人。选项:`--public` 或 `--private`(默认),`--prompt "..."` 设置系统提示词。 | `/createbot weather Weather Bot --public --prompt "You are a weather assistant"` | -| `/deletebot ` | 删除子机器人。只有创建者(或管理员)可以删除。 | `/deletebot @octosbot_weather:127.0.0.1:8128` | -| `/listbots` | 列出所有公开机器人以及你自己创建的私有机器人。 | `/listbots` | -| `/bothelp` | 显示机器人管理命令的帮助信息。 | `/bothelp` | +| `/deletebot ` | 删除子机器人。只有创建者(或管理员)可以删除。 | `/deletebot @octosbot_weather:127.0.0.1:8128` | +| `/listbots` | 列出所有公开机器人以及你自己创建的私有机器人。 | `/listbots` | +| `/bothelp` | 显示机器人管理命令的帮助信息。 | `/bothelp` | > **注意:** 你也可以通过 Robrix 的 UI 创建机器人(第 6.2 节),它提供了表单形式的替代方案。 @@ -196,15 +209,16 @@ Octos 机器人支持在聊天房间中直接输入少量斜杠命令。本节 BotFather 和子机器人的角色不同: -| | BotFather(`@octosbot`) | 子机器人(`@octosbot_<名称>`) | -|---|---|---| -| **角色** | 管理入口 + 通用 AI 聊天 | 专业化 AI 助手 | -| **管理命令** | 支持(`/createbot`、`/deletebot`、`/listbots`) | 不支持 | -| **自定义系统提示词** | 使用默认提示词 | 拥有独立的专用提示词 | -| **能否创建其他机器人** | 能 | 不能 | -| **Matrix 用户 ID** | `@octosbot:server_name` | `@octosbot_<用户名>:server_name` | +| | BotFather(`@octosbot`) | 子机器人(`@octosbot_<名称>`) | +| ---------------------------- | ----------------------------------------------------- | ---------------------------------- | +| **角色** | 管理入口 + 通用 AI 聊天 | 专业化 AI 助手 | +| **管理命令** | 支持(`/createbot`、`/deletebot`、`/listbots`) | 不支持 | +| **自定义系统提示词** | 使用默认提示词 | 拥有独立的专用提示词 | +| **能否创建其他机器人** | 能 | 不能 | +| **Matrix 用户 ID** | `@octosbot:server_name` | `@octosbot_<用户名>:server_name` | **何时使用哪个:** + - 使用 **BotFather** 进行通用 AI 对话,以及管理(创建/删除)其他机器人。 - 使用**子机器人**来完成特定任务(翻译、编程辅助、文字审阅等),它们拥有固定的系统提示词。 @@ -216,16 +230,19 @@ BotFather 和子机器人的角色不同: - **公开:** 服务器上的任何用户都可以通过 `/listbots` 发现此机器人,将其邀请到房间并与之对话。 **创建私有机器人(默认):** + ``` /createbot myhelper My Helper --prompt "You are my personal assistant" ``` **创建公开机器人:** + ``` /createbot translator Translator Bot --public --prompt "Translate all messages to English" ``` **谁可以删除机器人:** + - 机器人的**创建者**(所有者)可以随时删除它。 - **管理员**(`botfather.json` 中 `allowed_senders` 列表中的用户)可以删除任何机器人,作为紧急覆盖权限。 @@ -236,17 +253,14 @@ BotFather 和子机器人的角色不同: ## 8. 使用技巧 - **在一个房间中使用多个机器人。** 你可以在同一个房间中邀请多个机器人,每个机器人根据自己的系统提示词独立响应。这对于对比不同模型的输出或构建多智能体工作流很有用。 - - **私密对话。** 创建一个私密房间,只邀请一个机器人,进行不受其他用户或机器人干扰的一对一聊天。 - - **更换 LLM 提供商。** LLM 后端在 `botfather.json` 中配置(或通过环境变量设置)。你可以在 DeepSeek、OpenAI、Anthropic 等提供商之间切换。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 - - **机器人没有响应?** 常见原因: + - Octos 服务未运行。 - LLM API 密钥缺失或无效。 - 机器人未被正确邀请到房间。 - 请查看部署指南中的 [故障排查部分](01-deploying-palpo-and-octos-zh.md#5-故障排除)。 - - **Server name 不匹配。** 所有 Matrix ID(用户、机器人、房间)必须使用与 Palpo 配置相同的 `server_name`。如果机器人 ID 与服务器名称不匹配,邀请会失败。 --- @@ -255,12 +269,12 @@ BotFather 和子机器人的角色不同: 本地部署(`server_name = 127.0.0.1:8128`)下的常用 ID: -| 项目 | Matrix ID | -|---|---| -| 你的用户账号 | `@你的用户名:127.0.0.1:8128` | -| 主 AI 机器人(BotFather) | `@octosbot:127.0.0.1:8128` | +| 项目 | Matrix ID | +| -------------------------- | ------------------------------------------- | +| 你的用户账号 | `@你的用户名:127.0.0.1:8128` | +| 主 AI 机器人(BotFather) | `@octosbot:127.0.0.1:8128` | | 子机器人(例如翻译机器人) | `@octosbot_translator_bot:127.0.0.1:8128` | -| 房间别名 | `#房间名:127.0.0.1:8128` | +| 房间别名 | `#房间名:127.0.0.1:8128` | 远程部署时,将 `127.0.0.1:8128` 替换为你配置的 `server_name`。 diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md index f58e734ec..433b9e4d8 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md @@ -37,7 +37,7 @@ When you open Robrix, the login screen appears. By default, Robrix connects to ` 2. Enter `http://127.0.0.1:8128` for a local deployment. 3. For a remote server, enter `https://your.server.name` or `http://server-ip:8128`. - +![Robrix login screen — enter your Homeserver URL at the bottom](../images/login-screen.png) > **Note:** If the Homeserver URL field is left empty, Robrix connects to `matrix.org` by default. You must fill it in to reach your own Palpo server. @@ -94,7 +94,20 @@ This is the main workflow: create a room, invite the bot, and start a conversati -> **Tip:** If you deployed with a different `server_name`, replace `127.0.0.1:8128` in the bot ID accordingly. For example, on a server with `server_name = chat.example.com`, the bot ID would be `@octosbot:chat.example.com`. +> **How is this bot name determined?** The BotFather's Matrix ID is assembled from two config values: +> +> | Part | Value | Configured in | +> |------|-------|---------------| +> | Username (localpart) | `octosbot` | `sender_localpart` in `octos-registration.yaml` and `botfather.json` | +> | Server domain | `127.0.0.1:8128` | `server_name` in `palpo.toml` | +> | **Full Matrix ID** | **`@octosbot:127.0.0.1:8128`** | | +> | Display name (shown in rooms) | `BotFather` | `name` in `botfather.json` | +> +> Child bots created via `/createbot` follow a similar pattern. The `user_prefix` field in `botfather.json` (default: `octosbot_`) is automatically prepended to the username you specify: +> +> `/createbot weather Weather Bot` → Matrix ID: `@octosbot_weather:127.0.0.1:8128` +> +> If you change `server_name` in production, all bot IDs change accordingly. You must also update the namespace regex in `octos-registration.yaml` to match. ### 5.3 Start Chatting diff --git a/docs/robrix/getting-started-with-robrix-zh.md b/docs/robrix/getting-started-with-robrix-zh.md index c87c0deea..dd15097a7 100644 --- a/docs/robrix/getting-started-with-robrix-zh.md +++ b/docs/robrix/getting-started-with-robrix-zh.md @@ -6,12 +6,6 @@ Robrix 是一个用 Rust 编写的跨平台 Matrix 聊天客户端,基于 [Makepad](https://github.com/makepad/makepad/) UI 框架。原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 ---- - -## 下载预编译版本(推荐) - -从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载最新版本。支持 macOS、Linux 和 Windows。 - ## 从源码构建 ### 前提条件 @@ -40,6 +34,8 @@ Android 和 iOS 构建方法请参考 [Robrix README — 构建与运行](https: 启动 Robrix 后,登录界面底部有一个 **Homeserver URL** 输入框。 +![Robrix 登录界面](../images/login-screen.png) + - **留空** 默认连接 `matrix.org`(公共服务器) - **输入自定义 URL** 连接其他 Matrix 兼容服务器: - 本地 Palpo 实例:`http://127.0.0.1:8128` diff --git a/docs/robrix/getting-started-with-robrix.md b/docs/robrix/getting-started-with-robrix.md index b6fa2380a..1fc3cc44a 100644 --- a/docs/robrix/getting-started-with-robrix.md +++ b/docs/robrix/getting-started-with-robrix.md @@ -40,6 +40,8 @@ For Android and iOS builds, see the [Robrix README — Building & Running](https When you launch Robrix, you'll see the login screen with a **Homeserver URL** field at the bottom. +![Robrix login screen](../images/login-screen.png) + - **Leave it empty** to connect to `matrix.org` (the default public server) - **Enter a custom URL** to connect to any Matrix-compatible server: - Local Palpo instance: `http://127.0.0.1:8128` diff --git a/palpo-and-octos-deploy/compose.yml b/palpo-and-octos-deploy/compose.yml index a0a570ec1..8dace9827 100644 --- a/palpo-and-octos-deploy/compose.yml +++ b/palpo-and-octos-deploy/compose.yml @@ -38,20 +38,8 @@ services: # Built from source to support all architectures (x86_64, ARM64, etc.). palpo: build: - context: ./repos/palpo - dockerfile_inline: | - FROM rust:bookworm AS builder - WORKDIR /work - RUN apt-get update && apt-get install -y --no-install-recommends libclang-dev libpq-dev cmake && rm -rf /var/lib/apt/lists/* - COPY . . - RUN --mount=type=cache,target=/usr/local/cargo/registry --mount=type=cache,target=/work/target cargo build --release && cp target/release/palpo /usr/local/bin/palpo - FROM debian:bookworm-slim - RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl libpq5 && rm -rf /var/lib/apt/lists/* && mkdir -p /var/palpo/media - COPY --from=builder /usr/local/bin/palpo /usr/local/bin/palpo - ENV PALPO_CONFIG=/var/palpo/palpo.toml - EXPOSE 8008 - HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=15s CMD curl -sf http://localhost:8008/_matrix/client/versions || exit 1 - CMD ["/usr/local/bin/palpo"] + context: . + dockerfile: palpo.Dockerfile restart: unless-stopped ports: - "8128:8008" # Client-Server API (Robrix connects here) diff --git a/palpo-and-octos-deploy/palpo.Dockerfile b/palpo-and-octos-deploy/palpo.Dockerfile new file mode 100644 index 000000000..712eb7489 --- /dev/null +++ b/palpo-and-octos-deploy/palpo.Dockerfile @@ -0,0 +1,20 @@ +# Palpo Matrix Homeserver — Multi-stage Build +# Builds from source to support all architectures (x86_64, ARM64/Apple Silicon, etc.) + +FROM rust:bookworm AS builder +WORKDIR /work +RUN apt-get update && apt-get install -y --no-install-recommends libclang-dev libpq-dev cmake && rm -rf /var/lib/apt/lists/* +COPY ./repos/palpo . +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/work/target \ + cargo build --release && cp target/release/palpo /usr/local/bin/palpo + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl libpq5 && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /var/palpo/media +COPY --from=builder /usr/local/bin/palpo /usr/local/bin/palpo +ENV PALPO_CONFIG=/var/palpo/palpo.toml +EXPOSE 8008 +HEALTHCHECK --interval=10s --timeout=5s --retries=5 --start-period=15s \ + CMD curl -sf http://localhost:8008/_matrix/client/versions || exit 1 +CMD ["/usr/local/bin/palpo"] From 6fd25e5404d732321befc65c384aab5e5af83d58 Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 3 Apr 2026 17:37:25 +0800 Subject: [PATCH 068/283] docs: add search/invite bot screenshot and fix image sizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add compressed search-invite-bot.png showing the search → People workflow - Update Section 5.2 (EN/ZH) with step-by-step bot search instructions - Fix image tags to use for consistent display - Add "Download pre-built release" section to Chinese getting-started --- docs/images/search-invite-bot.png | Bin 0 -> 20200 bytes .../03-using-robrix-with-palpo-and-octos-zh.md | 15 ++++++++------- .../03-using-robrix-with-palpo-and-octos.md | 15 ++++++++------- docs/robrix/getting-started-with-robrix-zh.md | 6 +++++- docs/robrix/getting-started-with-robrix.md | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) create mode 100644 docs/images/search-invite-bot.png diff --git a/docs/images/search-invite-bot.png b/docs/images/search-invite-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..c37fe942d919c27b9f2a9465e68723730a02dcbe GIT binary patch literal 20200 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 > **注意:** 如果 Homeserver URL 留空,Robrix 会默认连接到 `matrix.org`。你必须填写此字段才能连接到自己的 Palpo 服务器。 @@ -53,7 +53,7 @@ 4. 输入 **Homeserver URL**:`http://127.0.0.1:8128`。 5. 点击 **Sign up(注册)**。 -![注册账号](../images/register-account.png) +注册账号 — 输入用户名、密码和 Homeserver URL > **注意:** 服务器必须启用注册功能。请确保 `palpo.toml` 中设置了 `allow_registration = true`。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 @@ -87,12 +87,13 @@ ### 5.2 邀请机器人 -1. 在房间内点击 **邀请** 按钮(通常是一个人形加号图标)。 -2. 输入机器人的 Matrix ID:`@octosbot:127.0.0.1:8128`。 -3. 发送邀请。 -4. 机器人会自动加入房间。这是通过 Application Service 机制实现的,不需要在机器人端手动接受邀请。 +1. 点击房间列表顶部的 **搜索图标**(下图中的 **①**)。 +2. 在搜索对话框中输入机器人的完整 Matrix ID:`@octosbot:127.0.0.1:8128`。 +3. 点击 **People** 标签页(**②**),将搜索结果过滤为用户和机器人(而非 Rooms 或 Spaces)。 +4. 从搜索结果中选择机器人,即可开始直接对话或邀请它加入房间。 +5. 机器人会自动加入。这是通过 Application Service 机制实现的,不需要在机器人端手动接受邀请。 - +搜索机器人:点击搜索图标(1),输入机器人 ID,然后点击 People(2)找到它 > **这个机器人名字是怎么来的?** BotFather 的 Matrix ID 由两个配置值组合而成: > diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md index 433b9e4d8..7bb020bcb 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md @@ -37,7 +37,7 @@ When you open Robrix, the login screen appears. By default, Robrix connects to ` 2. Enter `http://127.0.0.1:8128` for a local deployment. 3. For a remote server, enter `https://your.server.name` or `http://server-ip:8128`. -![Robrix login screen — enter your Homeserver URL at the bottom](../images/login-screen.png) +Robrix login screen — enter your Homeserver URL at the bottom > **Note:** If the Homeserver URL field is left empty, Robrix connects to `matrix.org` by default. You must fill it in to reach your own Palpo server. @@ -53,7 +53,7 @@ To create a new account on your Palpo server: 4. Enter the **Homeserver URL**: `http://127.0.0.1:8128`. 5. Click **Sign up**. -![Register account](../images/register-account.png) +Register account — enter username, password, and Homeserver URL > **Note:** Registration must be enabled on the server. Make sure `allow_registration = true` is set in your `palpo.toml`. See [Deployment Guide -- Configuration](01-deploying-palpo-and-octos.md) for details. @@ -87,12 +87,13 @@ This is the main workflow: create a room, invite the bot, and start a conversati ### 5.2 Invite the Bot -1. Click the **invite** button inside the room (usually a person-with-plus icon). -2. Enter the bot's Matrix ID: `@octosbot:127.0.0.1:8128`. -3. Send the invitation. -4. The bot joins automatically. This is handled by the Application Service mechanism -- no manual acceptance is needed on the bot side. +1. Click the **search icon** (**①** in the screenshot below) at the top of the room list. +2. In the search dialog, type the bot's full Matrix ID: `@octosbot:127.0.0.1:8128`. +3. Click the **People** tab (**②**) to filter results to users and bots (instead of Rooms or Spaces). +4. Select the bot from the search results to start a direct conversation or invite it to a room. +5. The bot joins automatically. This is handled by the Application Service mechanism -- no manual acceptance is needed on the bot side. - +Search for the bot: click the search icon (1), type the bot ID, then click People (2) to find it > **How is this bot name determined?** The BotFather's Matrix ID is assembled from two config values: > diff --git a/docs/robrix/getting-started-with-robrix-zh.md b/docs/robrix/getting-started-with-robrix-zh.md index dd15097a7..4035c6015 100644 --- a/docs/robrix/getting-started-with-robrix-zh.md +++ b/docs/robrix/getting-started-with-robrix-zh.md @@ -6,6 +6,10 @@ Robrix 是一个用 Rust 编写的跨平台 Matrix 聊天客户端,基于 [Makepad](https://github.com/makepad/makepad/) UI 框架。原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 +## 下载预编译版本(推荐) + +从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载最新版本。支持 macOS、Linux 和 Windows。 + ## 从源码构建 ### 前提条件 @@ -34,7 +38,7 @@ Android 和 iOS 构建方法请参考 [Robrix README — 构建与运行](https: 启动 Robrix 后,登录界面底部有一个 **Homeserver URL** 输入框。 -![Robrix 登录界面](../images/login-screen.png) +Robrix 登录界面 - **留空** 默认连接 `matrix.org`(公共服务器) - **输入自定义 URL** 连接其他 Matrix 兼容服务器: diff --git a/docs/robrix/getting-started-with-robrix.md b/docs/robrix/getting-started-with-robrix.md index 1fc3cc44a..745fb29e6 100644 --- a/docs/robrix/getting-started-with-robrix.md +++ b/docs/robrix/getting-started-with-robrix.md @@ -40,7 +40,7 @@ For Android and iOS builds, see the [Robrix README — Building & Running](https When you launch Robrix, you'll see the login screen with a **Homeserver URL** field at the bottom. -![Robrix login screen](../images/login-screen.png) +Robrix login screen - **Leave it empty** to connect to `matrix.org` (the default public server) - **Enter a custom URL** to connect to any Matrix-compatible server: From 5fd8e9c3501218c06367704653ade0e8df4fa984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Fri, 3 Apr 2026 21:02:43 +0800 Subject: [PATCH 069/283] Add room info pane and room reporting flow - add room info sliding pane with people list, topic preview, and member profile entry - add report room modal and leave-room confirmation modal in room screen - wire room info button in input bar and add MatrixRequest::ReportRoom handling --- src/home/room_screen.rs | 1355 ++++++++++++++++++++++++++++++++++-- src/room/room_input_bar.rs | 35 + src/sliding_sync.rs | 32 +- 3 files changed, 1381 insertions(+), 41 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 81e62106e..d5f22216b 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -8,7 +8,7 @@ use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ + OwnedServerName, media::{MediaFormat, MediaRequestParameters}, room::{RoomMember, RoomMemberRole}, ruma::{ EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ receipt::Receipt, room::{ @@ -32,7 +32,7 @@ use crate::{ }, room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, typing_notice::TypingNoticeWidgetExt}, shared::{ - avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt + avatar::{AvatarState, AvatarWidgetExt, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalAction, ConfirmationModalContent, ConfirmationModalWidgetExt}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, sliding_sync::{BackwardsPaginateUntilEventRequest, FetchedRoomThread, MatrixRequest, PaginationDirection, RoomThreadsAction, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} }; @@ -58,6 +58,9 @@ const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; /// Use a larger batch when we are trying to fill the initial viewport, /// otherwise many short messages can trigger a long chain of tiny paginations. const VIEWPORT_FILL_PAGINATION_SIZE: u16 = 150; +const TOPIC_PREVIEW_CHARS: usize = 140; +const ROOM_INFO_PANE_DESKTOP_WIDTH: f32 = 320.0; +const ROOM_INFO_PANE_MOBILE_BREAKPOINT: f32 = 700.0; /// #FFF4E5 @@ -960,6 +963,501 @@ script_mod! { } } + mod.widgets.RoomInfoPeopleEntry = #(RoomInfoPeopleEntry::register_widget(vm)) { + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + spacing: 9 + padding: Inset{left: 10, right: 10, top: 10, bottom: 10} + margin: Inset{left: 0, right: 0, top: 0, bottom: 6} + cursor: MouseCursor.Hand + + show_bg: true + draw_bg +: { + color: #F8FAFD + border_radius: 4.0 + border_size: 1.0 + border_color: #D8E0EA + } + + avatar := Avatar { + width: 34 + height: 34 + } + + display_name := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 11.2 } + color: #1F1F1F + } + text: "" + } + + level := Label { + width: Fit + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.2 } + color: #6D7682 + } + text: "" + } + } + + mod.widgets.RoomInfoSlidingPane = #(RoomInfoSlidingPane::register_widget(vm)) { + visible: false, + flow: Overlay, + width: Fill, + height: Fill, + align: Align{x: 1.0, y: 0} + + bg_view := SolidView { + width: Fill + height: Fill + visible: false, + show_bg: true + draw_bg.color: #000000BB + } + + main_content := SolidView { + width: 320, + height: Fill + flow: Down, + align: Align{x: 1.0} + + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) + + header := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + padding: Inset{top: 12, right: 10, bottom: 12, left: 15} + + back_button := RobrixNeutralIconButton { + visible: false + width: Fit, + height: Fit, + spacing: 0, + padding: 12, + icon_walk: Walk{width: 0, height: 0} + text: "Back" + } + + title := Label { + width: Fit + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 12.5 } + color: #000 + } + text: "Room Info" + } + + spacer := View { + width: Fill + height: Fit + } + + close_button := RobrixNeutralIconButton { + width: Fit, + height: Fit, + spacing: 0, + padding: 15, + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14} + text: "" + } + } + + content_scroll := ScrollYView { + width: Fill + height: Fill + flow: Down + + info_view := View { + width: Fill + height: Fit + flow: Down + spacing: 10 + padding: Inset{left: 12, right: 12, top: 12, bottom: 12} + + summary_card := RoundedView { + width: Fill + height: Fit + flow: Right + spacing: 10 + align: Align{y: 0.5} + padding: Inset{left: 10, right: 10, top: 10, bottom: 10} + + show_bg: true + draw_bg +: { + color: #F8FAFD + border_radius: 4.0 + border_size: 1.0 + border_color: #D8E0EA + } + + room_avatar := Avatar { + width: 40 + height: 40 + } + + room_meta := View { + width: Fill + height: Fit + flow: Down + spacing: 4 + + room_name_value := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 11.0 } + color: #1F1F1F + } + text: "" + } + + room_id_value := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 9.5 } + color: #6A6A6A + } + text: "" + } + } + } + + topic_card := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 5 + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + + show_bg: true + draw_bg +: { + color: #F8FAFD + border_radius: 4.0 + border_size: 1.0 + border_color: #D8E0EA + } + + topic_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 9.5 } + color: #4A4A4A + } + text: "Topic" + } + + topic_value := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.2 } + color: #6A6A6A + } + text: "" + } + + topic_toggle_button := RobrixNeutralIconButton { + visible: false + width: Fit + height: 30 + align: Align{x: 0.0, y: 0.5} + padding: Inset{left: 9, right: 9, top: 6, bottom: 6} + spacing: 0 + icon_walk: Walk{width: 0, height: 0} + text: "Expand" + } + } + + facts_card := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 6 + padding: Inset{left: 10, right: 10, top: 9, bottom: 9} + + show_bg: true + draw_bg +: { + color: #F8FAFD + border_radius: 4.0 + border_size: 1.0 + border_color: #D8E0EA + } + + visibility_row := View { + width: Fill + height: Fit + flow: Right + + visibility_label := Label { + width: 78 + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 9.5 } + color: #4A4A4A + } + text: "Visibility" + } + + visibility_value := Label { + width: Fill + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + color: (COLOR_TEXT) + } + text: "" + } + } + + encryption_row := View { + width: Fill + height: Fit + flow: Right + + encryption_label := Label { + width: 78 + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 9.5 } + color: #4A4A4A + } + text: "Encryption" + } + + encryption_value := Label { + width: Fill + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + color: (COLOR_TEXT) + } + text: "" + } + } + } + + actions_row := View { + width: Fill + height: Fit + flow: Down + spacing: 8 + + people_button := RobrixNeutralIconButton { + width: Fill + height: 40 + padding: 10 + draw_icon.svg: (ICON_ADD_USER) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "People" + } + + report_room_button := RobrixNeutralIconButton { + width: Fill + height: 40 + padding: 10 + draw_icon.svg: (ICON_INFO) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Report room" + } + + leave_room_button := RobrixNegativeIconButton { + width: Fill + height: 40 + padding: 10 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Leave Room" + } + } + } + + } + + people_view := View { + visible: false + width: Fill + height: Fill + flow: Down + spacing: 6 + padding: Inset{left: 12, right: 12, top: 12, bottom: 10} + + member_count := Label { + width: Fill + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 10.5 } + color: #4A4A4A + } + text: "" + } + + loading_label := Label { + visible: false + width: Fill + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.0 } + color: #6D7682 + } + text: "Loading members..." + } + + empty_label := Label { + visible: false + width: Fill + height: Fit + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 10.0 } + color: #6D7682 + } + text: "No members found." + } + + people_list := PortalList { + width: Fill + height: Fill + flow: Down + max_pull_down: 0.0 + + PersonEntry := mod.widgets.RoomInfoPeopleEntry {} + } + } + } + + slide: 1.0, + + animator: Animator { + panel: { + default: @hide + show: AnimatorState{ + redraw: true, + from: {all: Forward {duration: 0.5}} + ease: Ease.ExpDecay {d1: 0.80, d2: 0.97} + apply: { + slide: 0.0 + } + } + hide: AnimatorState{ + redraw: true, + from: {all: Forward {duration: 0.5}} + ease: Ease.ExpDecay {d1: 0.80, d2: 0.97} + apply: { + slide: 1.0 + } + } + } + } + } + + mod.widgets.ReportRoomModalLabel = Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #333 + } + text: "" + } + + mod.widgets.ReportRoomModal = #(ReportRoomModal::register_widget(vm)) { + width: Fit + height: Fit + + RoundedView { + width: 430 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 26, right: 22, bottom: 18, left: 22} + spacing: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT { font_size: 13 } + color: #000 + } + text: "Report Room" + } + + body := mod.widgets.ReportRoomModalLabel { + text: "" + } + + reason_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "Describe why you are reporting this room" + } + + status_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.2 } + color: #000 + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 16 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + report_button := RobrixNegativeIconButton { + width: 130 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_INFO) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Report room" + } + } + } + } + mod.widgets.AppServicePanel = #(AppServicePanel::register_widget(vm)) { width: Fill height: Fit @@ -1225,12 +1723,13 @@ script_mod! { // The top space should be displayed as an overlay at the top of the timeline. top_space := mod.widgets.TopSpace { } + threads_sliding_pane := mod.widgets.ThreadsSlidingPane { } + room_info_sliding_pane := mod.widgets.RoomInfoSlidingPane { } + // The user profile sliding pane should be displayed on top of other "static" subviews // (on top of all other views that are always visible). user_profile_sliding_pane := mod.widgets.UserProfileSlidingPane { } - threads_sliding_pane := mod.widgets.ThreadsSlidingPane { } - // The loading pane appears while the user is waiting for something in the room screen // to finish loading, e.g., when loading an older replied-to message. loading_pane := LoadingPane { } @@ -1247,6 +1746,18 @@ script_mod! { } } + report_room_modal := Modal { + content +: { + report_room_modal_inner := mod.widgets.ReportRoomModal {} + } + } + + leave_room_confirm_modal := Modal { + content +: { + leave_room_confirm_modal_inner := mod.widgets.NegativeConfirmationModal {} + } + } + /* * TODO: add the action bar back in as a series of floating buttons. @@ -1285,6 +1796,23 @@ impl ActionDefaultRef for ThreadsPaneAction { } } +#[derive(Clone, Default, Debug)] +pub enum RoomInfoPaneAction { + ShowPeoplePage, + OpenPeopleProfile(OwnedUserId), + ReportRoom, + LeaveRoom, + #[default] + None, +} + +impl ActionDefaultRef for RoomInfoPaneAction { + fn default_ref() -> &'static Self { + static DEFAULT: RoomInfoPaneAction = RoomInfoPaneAction::None; + &DEFAULT + } +} + #[derive(Clone, Debug)] struct ThreadsPaneEntryInfo { thread_root_event_id: OwnedEventId, @@ -1304,6 +1832,29 @@ struct ThreadsPaneInfo { show_loading: bool, } +#[derive(Clone, Debug)] +struct RoomInfoPaneInfo { + room_name: String, + room_id: String, + topic: String, + visibility: String, + encryption: String, + room_avatar_uri: Option, + room_avatar_fallback_text: String, + people_entries: Vec, + people_count_text: String, + show_people_loading: bool, +} + +#[derive(Clone, Debug)] +struct RoomInfoPeopleEntryInfo { + user_id: OwnedUserId, + display_name: String, + level: String, + avatar_uri: Option, + avatar_fallback_text: String, +} + #[derive(Default)] struct ThreadsPaneState { room_id: Option, @@ -1338,40 +1889,272 @@ impl Widget for ThreadsPaneEntry { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - self.view.draw_walk(cx, scope, walk) + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl ThreadsPaneEntry { + fn set_entry(&mut self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { + self.thread_root_event_id = Some(entry.thread_root_event_id.clone()); + self.label(cx, ids!(title)).set_text(cx, &entry.title); + self.label(cx, ids!(time)).set_text(cx, &entry.time); + self.label(cx, ids!(subtitle)).set_text(cx, &entry.subtitle); + self.label(cx, ids!(preview)).set_text(cx, &entry.preview); + } +} + +impl ThreadsPaneEntryRef { + fn set_entry(&self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_entry(cx, entry); + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct RoomInfoPeopleEntry { + #[source] source: ScriptObjectRef, + #[deref] view: View, + + #[rust] user_id: Option, +} + +impl Widget for RoomInfoPeopleEntry { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + let Some(user_id) = self.user_id.clone() else { return }; + match event.hits(cx, self.view.area()) { + Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { + cx.widget_action( + self.widget_uid(), + RoomInfoPaneAction::OpenPeopleProfile(user_id), + ); + } + _ => {} + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl RoomInfoPeopleEntry { + fn set_entry(&mut self, cx: &mut Cx, entry: &RoomInfoPeopleEntryInfo) { + self.user_id = Some(entry.user_id.clone()); + self.label(cx, ids!(display_name)).set_text(cx, &entry.display_name); + self.label(cx, ids!(level)).set_text(cx, &entry.level); + self.label(cx, ids!(level)).set_visible(cx, !entry.level.is_empty()); + + let avatar = self.avatar(cx, ids!(avatar)); + if let Some(uri) = entry.avatar_uri.as_ref() + && let avatar_cache::AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) + { + let res = avatar.show_image( + cx, + None, + |cx, img_ref| utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_err() { + avatar.show_text(cx, None, None, &entry.avatar_fallback_text); + } + } else { + avatar.show_text(cx, None, None, &entry.avatar_fallback_text); + } + } +} + +impl RoomInfoPeopleEntryRef { + fn set_entry(&self, cx: &mut Cx, entry: &RoomInfoPeopleEntryInfo) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_entry(cx, entry); + } +} + +#[derive(Script, ScriptHook, Widget, Animator)] +pub struct ThreadsSlidingPane { + #[source] source: ScriptObjectRef, + #[deref] view: View, + #[apply_default] animator: Animator, + #[live] slide: f32, + + #[rust] info: Option, + #[rust] is_animating_out: bool, +} + +impl Widget for ThreadsSlidingPane { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + if !self.visible { return; } + + let animator_action = self.animator_handle_event(cx, event); + if animator_action.must_redraw() { + self.redraw(cx); + } + + if self.is_animating_out && !self.animator.is_track_animating(id!(panel)) { + self.visible = false; + self.is_animating_out = false; + cx.revert_key_focus(); + self.view(cx, ids!(bg_view)).set_visible(cx, false); + self.redraw(cx); + return; + } + + let area = self.view.area(); + let close_pane = { + matches!( + event, + Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) + ) + || event.back_pressed() + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + Hit::FingerUp(fue) if fue.is_over => { + fue.mouse_button().is_some_and(|b| b.is_back()) + || !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + } + _ => false, + } + }; + if close_pane { + self.hide(cx); + } + + if let Event::Actions(actions) = event { + let threads_list = self.portal_list(cx, ids!(threads_list)); + if threads_list.scrolled(actions) + && threads_list.first_id() == 0 + && threads_list.scroll_position() >= -0.5 + { + cx.widget_action( + self.widget_uid(), + ThreadsPaneAction::LoadMoreRequested, + ); + } + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let Some(info) = self.info.as_ref() else { + self.visible = false; + return self.view.draw_walk(cx, scope, walk); + }; + + let container_width = self.view.area().rect(cx).size.x as f32; + let panel_width = if container_width > 1.0 && container_width < ROOM_INFO_PANE_MOBILE_BREAKPOINT { + container_width + } else { + ROOM_INFO_PANE_DESKTOP_WIDTH + }; + let right_margin = -(self.slide * panel_width); + let mut main_content = self.view(cx, ids!(main_content)); + script_apply_eval!(cx, main_content, { + width: #(panel_width) + margin.right: #(right_margin) + }); + let bg_alpha = (1.0 - self.slide) * 0.733; + let bg_color = vec4(0.0, 0.0, 0.0, bg_alpha); + let mut bg_view = self.view(cx, ids!(bg_view)); + script_apply_eval!(cx, bg_view, { + draw_bg +: { color: #(bg_color) } + }); + + self.label(cx, ids!(room_name)).set_text(cx, &info.room_name); + self.label(cx, ids!(loading_label)).set_text(cx, &info.loading_text); + self.view(cx, ids!(loading_indicator)).set_visible(cx, info.show_loading); + self.label(cx, ids!(empty_state)).set_text(cx, &info.status_text); + self.view(cx, ids!(empty_state)).set_visible(cx, !info.show_entries && !info.show_loading); + self.view(cx, ids!(threads_list)).set_visible(cx, info.show_entries); + + while let Some(widget) = self.view.draw_walk(cx, scope, walk).step() { + let portal_list_ref = widget.as_portal_list(); + let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + + list.set_item_range(cx, 0, info.entries.len()); + while let Some(item_id) = list.next_visible_item(cx) { + let Some(entry) = info.entries.get(item_id) else { continue }; + let item = list.item(cx, item_id, id!(ThreadEntry)); + item.as_threads_pane_entry().set_entry(cx, entry); + item.draw_all(cx, &mut Scope::empty()); + } + } + DrawStep::done() + } +} + +impl ThreadsSlidingPane { + pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { + self.visible + } + + fn set_info(&mut self, _cx: &mut Cx, info: ThreadsPaneInfo) { + self.info = Some(info); + } + + pub fn show(&mut self, cx: &mut Cx) { + self.visible = true; + self.is_animating_out = false; + cx.set_key_focus(self.view.area()); + self.animator_play(cx, ids!(panel.show)); + self.view(cx, ids!(bg_view)).set_visible(cx, true); + self.view.button(cx, ids!(close_button)).reset_hover(cx); + self.redraw(cx); + } + + pub fn hide(&mut self, cx: &mut Cx) { + if !self.visible { + return; + } + self.is_animating_out = true; + self.animator_play(cx, ids!(panel.hide)); + self.redraw(cx); } } -impl ThreadsPaneEntry { - fn set_entry(&mut self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { - self.thread_root_event_id = Some(entry.thread_root_event_id.clone()); - self.label(cx, ids!(title)).set_text(cx, &entry.title); - self.label(cx, ids!(time)).set_text(cx, &entry.time); - self.label(cx, ids!(subtitle)).set_text(cx, &entry.subtitle); - self.label(cx, ids!(preview)).set_text(cx, &entry.preview); +impl ThreadsSlidingPaneRef { + pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { + let Some(inner) = self.borrow() else { return false }; + inner.is_currently_shown(cx) } -} -impl ThreadsPaneEntryRef { - fn set_entry(&self, cx: &mut Cx, entry: &ThreadsPaneEntryInfo) { + fn set_info(&self, cx: &mut Cx, info: ThreadsPaneInfo) { let Some(mut inner) = self.borrow_mut() else { return }; - inner.set_entry(cx, entry); + inner.set_info(cx, info); + } + + pub fn show(&self, cx: &mut Cx) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.show(cx); + } + + pub fn hide(&self, cx: &mut Cx) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.hide(cx); } } #[derive(Script, ScriptHook, Widget, Animator)] -pub struct ThreadsSlidingPane { +pub struct RoomInfoSlidingPane { #[source] source: ScriptObjectRef, #[deref] view: View, #[apply_default] animator: Animator, #[live] slide: f32, - #[rust] info: Option, + #[rust] info: Option, #[rust] is_animating_out: bool, + #[rust] show_people_page: bool, + #[rust] topic_expanded: bool, + #[rust] people_display_count: usize, } -impl Widget for ThreadsSlidingPane { +impl Widget for RoomInfoSlidingPane { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -1416,16 +2199,51 @@ impl Widget for ThreadsSlidingPane { } if let Event::Actions(actions) = event { - let threads_list = self.portal_list(cx, ids!(threads_list)); - if threads_list.scrolled(actions) - && threads_list.first_id() == 0 - && threads_list.scroll_position() >= -0.5 - { + if self.button(cx, ids!(header.back_button)).clicked(actions) { + self.show_people_page = false; + self.redraw(cx); + } + if self.button(cx, ids!(content_scroll.info_view.topic_card.topic_toggle_button)).clicked(actions) { + self.topic_expanded = !self.topic_expanded; + self.redraw(cx); + } + if self.button(cx, ids!(content_scroll.info_view.actions_row.people_button)).clicked(actions) { + self.show_people_page = true; + self.people_display_count = self.info.as_ref() + .map(|info| info.people_entries.len().min(40)) + .unwrap_or(0); cx.widget_action( self.widget_uid(), - ThreadsPaneAction::LoadMoreRequested, + RoomInfoPaneAction::ShowPeoplePage, + ); + self.redraw(cx); + } + if self.button(cx, ids!(content_scroll.info_view.actions_row.report_room_button)).clicked(actions) { + cx.widget_action( + self.widget_uid(), + RoomInfoPaneAction::ReportRoom, + ); + } + if self.button(cx, ids!(content_scroll.info_view.actions_row.leave_room_button)).clicked(actions) { + cx.widget_action( + self.widget_uid(), + RoomInfoPaneAction::LeaveRoom, ); } + + if self.show_people_page + && let Some(info) = self.info.as_ref() + && self.people_display_count < info.people_entries.len() + { + let people_list = self.portal_list(cx, ids!(people_view.people_list)); + if people_list.scrolled(actions) { + let threshold = self.people_display_count.saturating_sub(5); + if people_list.first_id() + people_list.visible_items() >= threshold { + self.people_display_count = (self.people_display_count + 40).min(info.people_entries.len()); + self.redraw(cx); + } + } + } } } @@ -1448,22 +2266,67 @@ impl Widget for ThreadsSlidingPane { draw_bg +: { color: #(bg_color) } }); - self.label(cx, ids!(room_name)).set_text(cx, &info.room_name); - self.label(cx, ids!(loading_label)).set_text(cx, &info.loading_text); - self.view(cx, ids!(loading_indicator)).set_visible(cx, info.show_loading); - self.label(cx, ids!(empty_state)).set_text(cx, &info.status_text); - self.view(cx, ids!(empty_state)).set_visible(cx, !info.show_entries && !info.show_loading); - self.view(cx, ids!(threads_list)).set_visible(cx, info.show_entries); + self.button(cx, ids!(header.back_button)).set_visible(cx, self.show_people_page); + self.label(cx, ids!(header.title)).set_text(cx, if self.show_people_page { "People" } else { "Room Info" }); + self.view(cx, ids!(content_scroll)).set_visible(cx, !self.show_people_page); + self.view(cx, ids!(content_scroll.info_view)).set_visible(cx, !self.show_people_page); + self.view(cx, ids!(people_view)).set_visible(cx, self.show_people_page); + + self.label(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_name_value)).set_text(cx, &info.room_name); + self.label(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_id_value)).set_text(cx, &info.room_id); + self.label(cx, ids!(content_scroll.info_view.facts_card.visibility_row.visibility_value)).set_text(cx, &info.visibility); + self.label(cx, ids!(content_scroll.info_view.facts_card.encryption_row.encryption_value)).set_text(cx, &info.encryption); + + let topic_chars_len = info.topic.chars().count(); + let topic_has_more = topic_chars_len > TOPIC_PREVIEW_CHARS; + let topic_display_text = if topic_has_more && !self.topic_expanded { + let mut preview: String = info.topic.chars().take(TOPIC_PREVIEW_CHARS).collect(); + preview.push_str("..."); + preview + } else { + info.topic.clone() + }; + self.label(cx, ids!(content_scroll.info_view.topic_card.topic_value)).set_text(cx, &topic_display_text); + self.button(cx, ids!(content_scroll.info_view.topic_card.topic_toggle_button)).set_visible(cx, topic_has_more); + self.button(cx, ids!(content_scroll.info_view.topic_card.topic_toggle_button)).set_text( + cx, + if self.topic_expanded { "Collapse" } else { "Expand" }, + ); + + let room_avatar = self.avatar(cx, ids!(content_scroll.info_view.summary_card.room_avatar)); + if let Some(uri) = info.room_avatar_uri.as_ref() + && let avatar_cache::AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) + { + let res = room_avatar.show_image( + cx, + None, + |cx, img_ref| utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_err() { + room_avatar.show_text(cx, None, None, &info.room_avatar_fallback_text); + } + } else { + room_avatar.show_text(cx, None, None, &info.room_avatar_fallback_text); + } + + if self.show_people_page && self.people_display_count == 0 { + self.people_display_count = info.people_entries.len().min(40); + } + let visible_people_count = self.people_display_count.min(info.people_entries.len()); + self.label(cx, ids!(people_view.member_count)).set_text(cx, &info.people_count_text); + self.view(cx, ids!(people_view.loading_label)).set_visible(cx, info.show_people_loading); + self.view(cx, ids!(people_view.empty_label)).set_visible(cx, !info.show_people_loading && info.people_entries.is_empty()); + self.view(cx, ids!(people_view.people_list)).set_visible(cx, visible_people_count > 0); while let Some(widget) = self.view.draw_walk(cx, scope, walk).step() { let portal_list_ref = widget.as_portal_list(); let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; - list.set_item_range(cx, 0, info.entries.len()); + list.set_item_range(cx, 0, visible_people_count); while let Some(item_id) = list.next_visible_item(cx) { - let Some(entry) = info.entries.get(item_id) else { continue }; - let item = list.item(cx, item_id, id!(ThreadEntry)); - item.as_threads_pane_entry().set_entry(cx, entry); + let Some(entry) = info.people_entries.get(item_id) else { continue }; + let item = list.item(cx, item_id, id!(PersonEntry)); + item.as_room_info_people_entry().set_entry(cx, entry); item.draw_all(cx, &mut Scope::empty()); } } @@ -1471,18 +2334,29 @@ impl Widget for ThreadsSlidingPane { } } -impl ThreadsSlidingPane { +impl RoomInfoSlidingPane { pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { self.visible } - fn set_info(&mut self, _cx: &mut Cx, info: ThreadsPaneInfo) { + fn set_info(&mut self, cx: &mut Cx, info: RoomInfoPaneInfo) { self.info = Some(info); + if self.show_people_page { + if let Some(info) = self.info.as_ref() { + self.people_display_count = self.people_display_count + .max(40.min(info.people_entries.len())) + .min(info.people_entries.len()); + } + } + self.redraw(cx); } pub fn show(&mut self, cx: &mut Cx) { self.visible = true; self.is_animating_out = false; + self.show_people_page = false; + self.topic_expanded = false; + self.people_display_count = 0; cx.set_key_focus(self.view.area()); self.animator_play(cx, ids!(panel.show)); self.view(cx, ids!(bg_view)).set_visible(cx, true); @@ -1500,13 +2374,13 @@ impl ThreadsSlidingPane { } } -impl ThreadsSlidingPaneRef { +impl RoomInfoSlidingPaneRef { pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { let Some(inner) = self.borrow() else { return false }; inner.is_currently_shown(cx) } - fn set_info(&self, cx: &mut Cx, info: ThreadsPaneInfo) { + fn set_info(&self, cx: &mut Cx, info: RoomInfoPaneInfo) { let Some(mut inner) = self.borrow_mut() else { return }; inner.set_info(cx, info); } @@ -1522,6 +2396,113 @@ impl ThreadsSlidingPaneRef { } } +#[derive(Clone, Debug)] +pub enum ReportRoomModalAction { + Close, + Submit(String), +} + +#[derive(Script, ScriptHook, Widget)] +pub struct ReportRoomModal { + #[deref] + view: View, + #[rust] + is_showing_error: bool, +} + +impl Widget for ReportRoomModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_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 WidgetMatchEvent for ReportRoomModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let cancel_button = self.view.button(cx, ids!(buttons.cancel_button)); + let report_button = self.view.button(cx, ids!(buttons.report_button)); + let reason_input = self.view.text_input(cx, ids!(reason_input)); + let mut status_label = self.view.label(cx, ids!(status_label)); + + if cancel_button.clicked(actions) + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + { + cx.action(ReportRoomModalAction::Close); + return; + } + + if self.is_showing_error && reason_input.changed(actions).is_some() { + self.is_showing_error = false; + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if report_button.clicked(actions) || reason_input.returned(actions).is_some() { + let reason = reason_input.text().trim().to_string(); + if reason.is_empty() { + self.is_showing_error = true; + script_apply_eval!(cx, status_label, { + text: "Please enter a reason before reporting." + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + } + cx.action(ReportRoomModalAction::Submit(reason)); + } + } +} + +impl ReportRoomModal { + pub fn show(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { + self.is_showing_error = false; + self.view + .label(cx, ids!(title)) + .set_text(cx, "Report Room"); + self.view.label(cx, ids!(body)).set_text( + cx, + &format!( + "Report {} to your homeserver administrators. Please provide a reason.", + room_name_id + ), + ); + self.view + .text_input(cx, ids!(reason_input)) + .set_text(cx, ""); + self.view.label(cx, ids!(status_label)).set_text(cx, ""); + self.view + .button(cx, ids!(buttons.report_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.cancel_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.report_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(buttons.cancel_button)) + .reset_hover(cx); + self.view.redraw(cx); + } +} + +impl ReportRoomModalRef { + pub fn show(&self, cx: &mut Cx, room_name_id: &RoomNameId) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.show(cx, room_name_id); + } +} + /// The main widget that displays a single Matrix room. #[derive(Script, Widget)] pub struct RoomScreen { @@ -1591,6 +2572,7 @@ impl Widget for RoomScreen { let portal_list = self.portal_list(cx, ids!(timeline.list)); let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let threads_sliding_pane = self.threads_sliding_pane(cx, ids!(threads_sliding_pane)); + let room_info_sliding_pane = self.room_info_sliding_pane(cx, ids!(room_info_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); // Streaming animation frame handler @@ -1839,6 +2821,24 @@ impl Widget for RoomScreen { ); } } + if let Some(ReportRoomResultAction::Sent { room_id }) = action.downcast_ref() { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + enqueue_popup_notification( + "Room reported successfully.", + PopupKind::Success, + Some(4.0), + ); + } + } + if let Some(ReportRoomResultAction::Failed { room_id, error }) = action.downcast_ref() { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + enqueue_popup_notification( + format!("Failed to report room.\n\nError: {error}"), + PopupKind::Error, + Some(5.0), + ); + } + } match action.as_widget_action().cast_ref() { ThreadsPaneAction::OpenThread(thread_root_event_id) => { @@ -1858,6 +2858,56 @@ impl Widget for RoomScreen { ThreadsPaneAction::None => {} } + match action.as_widget_action().cast_ref() { + RoomInfoPaneAction::ShowPeoplePage => { + if let Some(tl) = self.tl_state.as_ref() + && tl.room_members.is_none() + { + submit_async_request(MatrixRequest::GetRoomMembers { + timeline_kind: tl.kind.clone(), + memberships: matrix_sdk::RoomMemberships::JOIN, + local_only: false, + }); + } + } + RoomInfoPaneAction::OpenPeopleProfile(user_id) => { + let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { continue }; + let room_member = self.tl_state.as_ref() + .and_then(|tl| tl.room_members.as_ref()) + .and_then(|members| members.iter().find(|member| member.user_id() == user_id).cloned()); + let username = room_member.as_ref() + .and_then(|member| member.display_name().map(ToOwned::to_owned)); + let avatar_state = AvatarState::Known( + room_member + .as_ref() + .and_then(|member| member.avatar_url().map(ToOwned::to_owned)) + ); + self.show_user_profile( + cx, + &user_profile_sliding_pane, + UserProfilePaneInfo { + profile_and_room_id: UserProfileAndRoomId { + user_profile: UserProfile { + user_id: user_id.clone(), + username, + avatar_state, + }, + room_id: room_name_id.room_id().clone(), + }, + room_name: room_name_id.to_string(), + room_member, + }, + ); + } + RoomInfoPaneAction::ReportRoom => { + self.open_report_room_modal(cx); + } + RoomInfoPaneAction::LeaveRoom => { + self.open_leave_room_confirm_modal(cx); + } + RoomInfoPaneAction::None => {} + } + if let Some(RoomThreadsAction::Loaded { room_id, from, threads, prev_batch_token }) = action.downcast_ref() { if self.threads_pane_state.room_id.as_ref().is_some_and(|current| current == room_id) { self.on_threads_loaded( @@ -1941,6 +2991,9 @@ impl Widget for RoomScreen { if threads_sliding_pane.is_currently_shown(cx) { self.refresh_threads_pane(cx); } + if room_info_sliding_pane.is_currently_shown(cx) { + self.refresh_room_info_pane(cx); + } // Ideally we would do this elsewhere on the main thread, because it's not room-specific, // but it doesn't hurt to do it here. @@ -1976,6 +3029,12 @@ impl Widget for RoomScreen { user_profile_sliding_pane.handle_event(cx, event, scope); } } + else if room_info_sliding_pane.is_currently_shown(cx) { + is_pane_shown = true; + if is_interactive_hit { + room_info_sliding_pane.handle_event(cx, event, scope); + } + } else { is_pane_shown = false; } @@ -2053,6 +3112,9 @@ impl Widget for RoomScreen { } }; let mut room_scope = Scope::with_props(&room_props); + let leave_room_confirm_modal_uid = self + .confirmation_modal(cx, ids!(leave_room_confirm_modal_inner)) + .widget_uid(); // Forward the event to the inner timeline view, but capture any actions it produces @@ -2295,6 +3357,42 @@ impl Widget for RoomScreen { None => {} } + match action.downcast_ref::() { + Some(ReportRoomModalAction::Close) => { + self.close_report_room_modal(cx); + return false; + } + Some(ReportRoomModalAction::Submit(reason)) => { + let Some(room_id) = self.room_id().cloned() else { + self.close_report_room_modal(cx); + return false; + }; + submit_async_request(MatrixRequest::ReportRoom { + room_id, + reason: reason.clone(), + }); + self.close_report_room_modal(cx); + return false; + } + None => {} + } + + if let ConfirmationModalAction::Close(accepted) = action + .as_widget_action() + .widget_uid_eq(leave_room_confirm_modal_uid) + .cast() + { + self.close_leave_room_confirm_modal(cx); + if accepted { + if let Some(room_id) = self.room_id().cloned() { + submit_async_request(MatrixRequest::LeaveRoom { + room_id, + }); + } + } + return false; + } + if let MessageAction::ToggleAppServiceActions = action .as_widget_action() .widget_uid_eq(room_screen_widget_uid) @@ -2657,6 +3755,14 @@ impl RoomScreen { self.view.modal(cx, ids!(delete_bot_modal)).close(cx); } + fn close_report_room_modal(&self, cx: &mut Cx) { + self.view.modal(cx, ids!(report_room_modal)).close(cx); + } + + fn close_leave_room_confirm_modal(&self, cx: &mut Cx) { + self.view.modal(cx, ids!(leave_room_confirm_modal)).close(cx); + } + fn open_create_bot_modal(&mut self, cx: &mut Cx) { let Some(room_name_id) = self.room_name_id.clone() else { return; @@ -2679,10 +3785,38 @@ impl RoomScreen { self.view.modal(cx, ids!(delete_bot_modal)).open(cx); } + fn open_report_room_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.as_ref() else { + return; + }; + self.view + .report_room_modal(cx, ids!(report_room_modal_inner)) + .show(cx, room_name_id); + self.view.modal(cx, ids!(report_room_modal)).open(cx); + } + + fn open_leave_room_confirm_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.as_ref() else { + return; + }; + self.view + .confirmation_modal(cx, ids!(leave_room_confirm_modal_inner)) + .show(cx, ConfirmationModalContent { + title_text: String::from("Leave Room").into(), + body_text: format!("Are you sure you want to leave {}?", room_name_id).into(), + accept_button_text: Some(String::from("Leave").into()), + cancel_button_text: Some(String::from("Cancel").into()), + ..Default::default() + }); + self.view.modal(cx, ids!(leave_room_confirm_modal)).open(cx); + } + fn reset_app_service_ui(&mut self, cx: &mut Cx) { self.set_app_service_actions_visible(cx, false); self.close_create_bot_modal(cx); self.close_delete_bot_modal(cx); + self.close_report_room_modal(cx); + self.close_leave_room_confirm_modal(cx); } fn is_app_service_room_bound(&self, app_state: &AppState, room_id: &OwnedRoomId) -> bool { @@ -3760,6 +4894,9 @@ impl RoomScreen { MessageAction::ShowThreadsPane => { self.show_threads_pane(cx); } + MessageAction::ShowRoomInfoPane => { + self.show_room_info_pane(cx); + } MessageAction::Redact { details, reason } => { let Some(tl) = self.tl_state.as_ref() else { return }; let timeline_event_id = details.timeline_event_id.clone(); @@ -3897,6 +5034,7 @@ impl RoomScreen { } fn show_threads_pane(&mut self, cx: &mut Cx) { + self.hide_room_info_pane(cx); self.ensure_threads_state_for_current_room(); if !self.threads_pane_state.initialized && !self.threads_pane_state.is_loading { self.request_more_threads(cx, false); @@ -3941,6 +5079,129 @@ impl RoomScreen { self.threads_sliding_pane(cx, ids!(threads_sliding_pane)).hide(cx); } + fn refresh_room_info_pane(&mut self, cx: &mut Cx) { + let Some(room_id) = self.room_id().cloned() else { return }; + let room_name = self.room_name_id.as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| room_id.to_string()); + let room_avatar_fallback_text = self.room_name_id.as_ref() + .and_then(|room_name_id| room_name_id.name_for_avatar().map(ToOwned::to_owned)) + .unwrap_or_else(|| String::from("?")); + let room_avatar_uri = self.room_avatar_url.clone(); + let (topic, visibility, encryption) = get_client() + .and_then(|client| client.get_room(&room_id)) + .map(|room| { + let topic = room.topic() + .unwrap_or_else(|| String::from("No topic")); + let visibility = match room.is_public() { + Some(true) => String::from("Public room"), + Some(false) => String::from("Private room"), + None => String::from("Unknown"), + }; + let encryption_state = room.encryption_state(); + let encryption = if encryption_state.is_unknown() { + String::from("Unknown") + } else if encryption_state.is_encrypted() { + String::from("Encrypted") + } else { + String::from("Unencrypted") + }; + (topic, visibility, encryption) + }) + .unwrap_or_else(|| ( + String::from("No topic"), + String::from("Unknown"), + String::from("Unknown"), + )); + + let (people_entries, people_count_text, show_people_loading) = self.tl_state.as_ref() + .map(|tl| { + let Some(room_members) = tl.room_members.as_ref() else { + return ( + Vec::new(), + String::from("People"), + true, + ); + }; + + let mut people_entries: Vec = room_members.iter() + .map(|member| { + let display_name = member.display_name() + .map(ToOwned::to_owned) + .unwrap_or_else(|| member.user_id().to_string()); + let level = match member.suggested_role_for_power_level() { + RoomMemberRole::Creator => String::from("Creator"), + RoomMemberRole::Administrator => String::from("Admin"), + RoomMemberRole::Moderator => String::from("Moderator"), + RoomMemberRole::User => String::new(), + }; + let avatar_fallback_text = utils::user_name_first_letter(&display_name) + .map(ToOwned::to_owned) + .unwrap_or_else(|| String::from("?")); + RoomInfoPeopleEntryInfo { + user_id: member.user_id().to_owned(), + display_name, + level, + avatar_uri: member.avatar_url().map(ToOwned::to_owned), + avatar_fallback_text, + } + }) + .collect(); + + let level_weight = |level: &str| -> u8 { + match level { + "Creator" => 0, + "Admin" => 1, + "Moderator" => 2, + _ => 3, + } + }; + people_entries.sort_by(|a, b| { + level_weight(&a.level) + .cmp(&level_weight(&b.level)) + .then_with(|| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase())) + }); + + ( + people_entries, + format!("{} Members", room_members.len()), + false, + ) + }) + .unwrap_or_else(|| ( + Vec::new(), + String::from("People"), + true, + )); + + self.room_info_sliding_pane(cx, ids!(room_info_sliding_pane)).set_info( + cx, + RoomInfoPaneInfo { + room_name, + room_id: room_id.to_string(), + topic, + visibility, + encryption, + room_avatar_uri, + room_avatar_fallback_text, + people_entries, + people_count_text, + show_people_loading, + }, + ); + } + + fn show_room_info_pane(&mut self, cx: &mut Cx) { + self.hide_threads_pane(cx); + self.refresh_room_info_pane(cx); + self.room_info_sliding_pane(cx, ids!(room_info_sliding_pane)).show(cx); + self.redraw(cx); + } + + fn hide_room_info_pane(&mut self, cx: &mut Cx) { + self.room_info_sliding_pane(cx, ids!(room_info_sliding_pane)).hide(cx); + } + fn ensure_threads_state_for_current_room(&mut self) { let Some(room_id) = self.room_id().cloned() else { return }; if self.threads_pane_state.room_id.as_ref().is_some_and(|current| current == &room_id) { @@ -4320,6 +5581,7 @@ impl RoomScreen { self.hide_timeline(); self.reset_app_service_ui(cx); self.hide_threads_pane(cx); + self.hide_room_info_pane(cx); self.threads_pane_state = Default::default(); // Reset the the state of the inner loading pane. self.loading_pane(cx, ids!(loading_pane)).take_state(); @@ -6482,6 +7744,18 @@ pub enum InviteResultAction { }, } +/// The result of reporting a room. +#[derive(Debug)] +pub enum ReportRoomResultAction { + Sent { + room_id: OwnedRoomId, + }, + Failed { + room_id: OwnedRoomId, + error: matrix_sdk::Error, + }, +} + /// Actions related to a specific message within a room timeline. #[derive(Clone, Default, Debug)] @@ -6550,6 +7824,7 @@ pub enum MessageAction { /// The user requested toggling the in-room app service quick actions card. ToggleAppServiceActions, ShowThreadsPane, + ShowRoomInfoPane, #[default] None, } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index a56b8eae2..160ebdf50 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -109,6 +109,33 @@ script_mod! { spacing: 6 align: Align{x: 0.0, y: 0.5} + room_info_card_button := RobrixIconButton { + width: Fit + align: Align{x: 0.0, y: 0.5} + margin: Inset{top: 1, bottom: 1} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 8 + draw_icon +: { + svg: (ICON_INFO) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + icon_walk: Walk{width: 20, height: 20} + text: "room info", + } + location_card_button := RobrixIconButton { width: Fit align: Align{x: 0.0, y: 0.5} @@ -454,6 +481,14 @@ impl RoomInputBar { self.redraw(cx); } + if self.button(cx, ids!(room_info_card_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::ShowRoomInfoPane, + ); + self.redraw(cx); + } + // Handle the send location button being clicked. if self.button(cx, ids!(location_preview.send_location_button)).clicked(actions) { let location_preview = self.location_preview(cx, ids!(location_preview)); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 039fd9587..0ee60d57b 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -44,7 +44,7 @@ use hashbrown::{HashMap, HashSet}; use crate::{ account_manager::{self, Account}, app::{AppStateAction, RoomFilterRemoteSearchAction}, app_data_dir, avatar_cache::AvatarUpdate, event_preview::{BeforeText, TextPreview, text_preview_of_raw_timeline_event, text_preview_of_timeline_item}, home::{ - add_room::{CreatableSpacesAction, CreateRoomAction, CreateRoomContext, KnockResultAction}, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, build_room_search_text, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, tombstone_footer::SuccessorRoomDetails + add_room::{CreatableSpacesAction, CreateRoomAction, CreateRoomContext, KnockResultAction}, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, ReportRoomResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, build_room_search_text, 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, take_skip_app_state_restore_once}, profile::{ user_profile::UserProfile, user_profile_cache::{UserProfileUpdate, enqueue_user_profile_update}, @@ -762,6 +762,11 @@ pub enum MatrixRequest { LeaveRoom { room_id: OwnedRoomId, }, + /// Request to report the given room. + ReportRoom { + room_id: OwnedRoomId, + reason: String, + }, /// Request to get the actual list of members in a room. /// /// This returns the list of members that can be displayed in the UI. @@ -1729,6 +1734,31 @@ async fn matrix_worker_task( }); } + MatrixRequest::ReportRoom { room_id, reason } => { + let Some(client) = get_client() else { continue }; + let _report_room_task = Handle::current().spawn(async move { + log!("Sending request to report room {room_id}..."); + let result_action = if let Some(room) = client.get_room(&room_id) { + match room.report_room(reason).await { + Ok(_) => { + ReportRoomResultAction::Sent { room_id } + } + Err(e) => { + error!("Error reporting room {room_id}: {e:?}"); + ReportRoomResultAction::Failed { room_id, error: e } + } + } + } else { + error!("BUG: client could not get room with ID {room_id}"); + ReportRoomResultAction::Failed { + room_id, + error: matrix_sdk::Error::UnknownError("Client couldn't locate room to report 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"); From 9a0488ba292ccf3367f76543e0d9341523c3294c Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 4 Apr 2026 02:15:36 +0800 Subject: [PATCH 070/283] Fix blank main page caused by Dock.load_state() DrawList corruption Dock.load_state() destroys DrawList2d objects by clearing tab_bars during event handling, but the rendering pipeline still holds stale DrawListId references from the previous frame. This causes massive "Drawlist id generation wrong" errors and a completely blank main content area. Replace dock.load_state() in load_dock_state_from() with programmatic tab recreation via close_all_tabs() + focus_or_create_tab(), which uses the Dock's normal widget API and avoids direct DrawList destruction. Fixes #45 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...001-dock-load-state-drawlist-corruption.md | 111 ++++++++++++++++++ src/home/main_desktop_ui.rs | 43 +++---- 2 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 issues/001-dock-load-state-drawlist-corruption.md diff --git a/issues/001-dock-load-state-drawlist-corruption.md b/issues/001-dock-load-state-drawlist-corruption.md new file mode 100644 index 000000000..4d72bda91 --- /dev/null +++ b/issues/001-dock-load-state-drawlist-corruption.md @@ -0,0 +1,111 @@ +# Issue #001: Dock.load_state() causes DrawList corruption and blank main page + +**Date:** 2026-04-04 +**Severity:** Critical (blocks all UI rendering) +**Status:** Fixed (workaround applied) +**Affected component:** `src/home/main_desktop_ui.rs` — `load_dock_state_from()` + +## Summary + +Restoring the Dock layout from persisted state via `Dock.load_state()` corrupts Makepad's internal DrawList references, causing the entire main content area (rooms list + room tabs) to render as a blank grey page. + +## Symptoms + +- Left navigation bar (NavigationTabBar) renders correctly +- Main content area (Dock with RoomsSideBar + room tabs) is completely blank/grey +- Console shows massive `Drawlist id generation wrong` errors: + ``` + [E] draw_list.rs:324: Drawlist id generation wrong index: 21 current gen:1 in pointer:0 + ``` +- Errors repeat continuously for draw list indices 21 and 22 + +## Root Cause + +`Dock.load_state()` in Makepad's `widgets/src/dock.rs:1310` destroys DrawList references during event handling: + +```rust +pub fn load_state(&mut self, cx: &mut Cx, dock_items: HashMap) { + self.dock_items = dock_items; + self.items.clear(); + self.tab_bars.clear(); // Drops TabBarWrap, freeing DrawList2d + self.splitters.clear(); + self.area.redraw(cx); // Marks redraw, but stale refs remain + self.create_all_items(cx); +} +``` + +The lifecycle issue: + +1. `tab_bars.clear()` drops `TabBarWrap` instances containing `contents_draw_list: DrawList2d` +2. Drop increments the DrawList pool entry generation (0 → 1) +3. Makepad's rendering pipeline still holds cached `DrawListId(index, gen=0)` from the previous frame +4. Next frame accesses stale references → generation mismatch → rendering failure + +This only triggers when the Dock already has live tab_bars (created during the first draw pass) and `load_state()` replaces them. On first startup with empty tab_bars, `clear()` is a no-op and causes no issue. + +## Reproduction + +1. Run the app, log in, open some room tabs +2. Close the app (state is persisted to `latest_app_state.json`) +3. Restart the app → blank main page + +**Verification:** Deleting `latest_app_state.json` before restart → UI renders correctly with 0 DrawList errors. + +## Fix Applied + +Modified `load_dock_state_from()` in `src/home/main_desktop_ui.rs` to avoid calling `dock.load_state()`. Instead, tabs are recreated programmatically: + +```rust +fn load_dock_state_from(&mut self, cx: &mut Cx, app_state: &mut AppState) { + // ... resolve which state to restore ... + + let room_order = to_restore.room_order.clone(); + let selected_room = to_restore.selected_room.clone(); + + // Close existing tabs using the Dock's normal API (safe) + self.close_all_tabs(cx); + + // Recreate each room tab in saved order (safe) + for room in &room_order { + self.focus_or_create_tab(cx, room.clone()); + } + + // Re-select the previously-selected room + let final_selected = selected_room.or_else(|| room_order.last().cloned()); + if let Some(selected) = final_selected.clone() { + self.focus_or_create_tab(cx, selected); + } + app_state.selected_room = final_selected; + self.redraw(cx); +} +``` + +This uses `close_all_tabs()` + `focus_or_create_tab()` which operate through the Dock's normal widget API, avoiding direct destruction of DrawList2d objects. + +## Remaining Issues + +1. **Splitter position not restored:** Custom sidebar width (if user dragged the splitter) resets to default 300px on restart. + +2. **Multi-pane layout not restored:** If the user created split-view arrangements by dragging tabs, those layouts are lost on restart. All tabs return to the single default tab bar. + +3. **Same issue exists in space switching:** `NavigationBarAction::TabSelected` also calls `load_dock_state_from()`, which previously used `dock.load_state()`. The fix applies to this path as well, but the same layout-loss trade-off exists. + +4. **Upstream Makepad bug:** `Dock.load_state()` should be fixed in Makepad to properly handle DrawList lifecycle when called during event handling. The fix should either: + - Defer the actual destruction to the next draw pass + - Properly invalidate cached DrawList references in the rendering pipeline + - Or use a two-phase approach: mark old DrawLists for cleanup, create new ones, then clean up + +5. **`SETTINGS_BUTTON_HEIGHT` undefined:** Unrelated but observed during debugging — `account_settings.rs:63,86` references `mod.widgets.SETTINGS_BUTTON_HEIGHT` which is never defined, causing DSL parse warnings at startup. + +## Files Changed + +- `src/home/main_desktop_ui.rs` — `load_dock_state_from()` rewritten + +## Test Verification + +| Scenario | Before Fix | After Fix | +|----------|-----------|-----------| +| Start with persisted state | Blank page, ~50+ DrawList errors | Rooms render, 0 DrawList errors | +| Start without persisted state | Works | Works | +| Room tabs restored | N/A (blank) | All saved tabs recreated correctly | +| Selected room restored | N/A (blank) | Correct room selected and loaded | diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 04c05ed21..3aef9e008 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -377,8 +377,11 @@ impl MainDesktopUI { /// /// If the saved state is empty (has no open rooms), we use the default dock layout /// defined in the DSL: one splitter with the RoomsList on the left and a Welcome tab on the right. + /// + /// Instead of calling `dock.load_state()` directly (which can corrupt Makepad's + /// internal DrawList references and cause blank rendering), we recreate each tab + /// programmatically via `focus_or_create_tab()`. fn load_dock_state_from(&mut self, cx: &mut Cx, app_state: &mut AppState) { - let dock = self.view.dock(cx, ids!(dock)); let to_restore_opt = if let Some(ss) = self.selected_space.as_ref() { app_state.saved_dock_state_per_space.get(ss) } else { @@ -389,32 +392,24 @@ impl MainDesktopUI { Some(sds) if sds.open_rooms.is_empty() => &self.default_layout, Some(sds) => sds, }; - let SavedDockState { dock_items, open_rooms, room_order, selected_room } = to_restore; - - self.room_order = room_order.clone(); - self.open_rooms = open_rooms.clone(); - - if let Some(mut dock) = dock.borrow_mut() { - dock.load_state(cx, dock_items.clone()); - // Only populate the currently-selected tab immediately. - // Background tabs will be initialized lazily when they are focused. - if let Some(selected_room) = selected_room.as_ref() { - if let Some((_, widget)) = dock.items().get(&selected_room.tab_id()) { - Self::sync_tab_widget(cx, widget, selected_room); - } - } - } else { - error!("BUG: failed to borrow dock widget to restore state upon LoadDockFromAppState action."); - return; + + let room_order = to_restore.room_order.clone(); + let selected_room = to_restore.selected_room.clone(); + + // Close any existing tabs first, starting from the default layout. + self.close_all_tabs(cx); + + // Recreate each room tab in the saved order. + for room in &room_order { + self.focus_or_create_tab(cx, room.clone()); } - // Note: the borrow of `dock` must end here *before* we call `self.focus_or_create_tab()`. - // Now that we've loaded the dock content, we can re-select the selected room. - let selected_room = selected_room.clone(); - if let Some(selected_room) = selected_room.clone() { - self.focus_or_create_tab(cx, selected_room); + // Re-select the previously-selected room (or the last one if not set). + let final_selected = selected_room.or_else(|| room_order.last().cloned()); + if let Some(selected) = final_selected.clone() { + self.focus_or_create_tab(cx, selected); } - app_state.selected_room = selected_room; + app_state.selected_room = final_selected; self.redraw(cx); } } From a0db454b214e44db08a7f1ea99bcf7c6927f5f9a Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 4 Apr 2026 02:25:03 +0800 Subject: [PATCH 071/283] Add file-issue skill for documenting bugs and creating GitHub issues Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/file-issue/SKILL.md | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .claude/skills/file-issue/SKILL.md diff --git a/.claude/skills/file-issue/SKILL.md b/.claude/skills/file-issue/SKILL.md new file mode 100644 index 000000000..ae70d188d --- /dev/null +++ b/.claude/skills/file-issue/SKILL.md @@ -0,0 +1,116 @@ +--- +name: file-issue +description: Document a bug/fix locally in issues/ and create a matching GitHub issue +allowed-tools: + - Bash(ls:*) + - Bash(mkdir:*) + - Bash(gh:*) + - Glob + - Grep + - Read + - Write +when_to_use: | + Use when the user wants to document a discovered bug, applied fix, and remaining issues + as both a local issue file and a GitHub issue. Typically invoked after a debugging/fix session. + Examples: "file an issue for this", "record this bug", "create issue", "file-issue" +--- + +# File Issue + +Document a bug discovery and fix as a local issue file in `issues/` and a matching GitHub issue. +All output is written in English regardless of conversation language. + +## Goal + +Produce two artifacts: +1. A detailed local issue document at `issues/NNN-slug.md` +2. A GitHub issue with a summary version + +## Steps + +### 1. Scan for next issue number + +Check if `issues/` directory exists in the project root. Create it if missing. +List existing files to determine the next sequential number (e.g., if `001-*` exists, next is `002`). + +**Success criteria**: Know the next issue number (zero-padded to 3 digits) and confirmed `issues/` dir exists. + +### 2. Gather context from conversation + +Extract from the current conversation: +- **Summary**: One-line description of the bug +- **Severity**: Critical / High / Medium / Low +- **Symptoms**: What the user observed (UI behavior, error messages, logs) +- **Root Cause**: Technical explanation of why it happens +- **Reproduction**: Steps to reproduce +- **Fix Applied**: What was changed and why (include code snippets if relevant) +- **Remaining Issues**: Known limitations, follow-up work, upstream bugs +- **Files Changed**: List of modified files +- **Test Verification**: Before/after comparison table + +Generate a kebab-case slug from the summary (e.g., `dock-load-state-drawlist-corruption`). + +**Success criteria**: All template sections populated with specific, accurate details from the session. + +### 3. Write local issue document + +Write to `issues/NNN-slug.md` using this template: + +```markdown +# Issue #NNN: {Summary} + +**Date:** {YYYY-MM-DD} +**Severity:** {Critical|High|Medium|Low} +**Status:** Fixed (workaround applied) | Fixed | Open +**Affected component:** {file path(s)} + +## Summary +{One paragraph} + +## Symptoms +{Bullet list of what the user observed} + +## Root Cause +{Technical explanation with code snippets} + +## Reproduction +{Numbered steps} + +## Fix Applied +{Description + key code changes} + +## Remaining Issues +{Numbered list of known limitations and follow-up work} + +## Files Changed +{Bullet list} + +## Test Verification +{Before/after table} +``` + +**Success criteria**: File written, all sections filled, no placeholder text remaining. + +### 4. Create GitHub issue + +Detect the repo with `gh repo view --json nameWithOwner`. +Create a GitHub issue via `gh issue create` with: +- Title: same as local doc summary (concise, under 80 chars) +- Label: `bug` +- Body: condensed version with Summary, Symptoms, Root Cause, Fix Applied, Remaining Issues (as checklist), and Environment section +- Reference the local doc path in the body + +**Rules**: +- Use a HEREDOC for the body to preserve formatting +- Remaining Issues should be `- [ ]` checklist items +- Include a link/reference to the local issue doc + +**Success criteria**: GitHub issue created, URL returned. + +### 5. Report results + +Tell the user: +- Local issue doc path +- GitHub issue URL (in `owner/repo#number` format for clickable link) + +**Success criteria**: Both paths reported in a concise summary. From d043f204331769fff59c41b96663b75df08f4ba5 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 4 Apr 2026 03:28:04 +0800 Subject: [PATCH 072/283] Migrate @mention user feature from Makepad 1.0 to 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the complete mention system (~4600 lines) from robrix (Makepad 1.0) to robrix2 (Makepad 2.0), including: - MentionableTextInput: state machine, autocomplete popup via CommandTextInput, mention insertion as markdown links, mention metadata extraction for sent messages - member_search.rs: background member search algorithm with streaming results, Unicode/grapheme-aware matching - cpu_worker.rs: background thread execution via cx.spawn_thread() - MatrixLinkPill: enable pill-style mention rendering in timeline Key 1.0 → 2.0 transformations: - live_design! → script_mod! - #[derive(Live, LiveHook)] → #[derive(Script, ScriptHook)] - WidgetRef::new_from_ptr() → widget_ref_from_live_ptr() - apply_over(cx, live!{}) → script_apply_eval!(cx, item, {}) - Widget accessors now require cx parameter Co-Authored-By: Claude Opus 4.6 (1M context) --- specs/task-mention-user.spec.md | 152 ++ src/cpu_worker.rs | 56 + src/lib.rs | 1 + src/room/member_search.rs | 1213 ++++++++++++++++ src/room/mod.rs | 1 + src/shared/html_or_plaintext.rs | 11 +- src/shared/mentionable_text_input.rs | 1934 +++++++++++++++++++++++++- 7 files changed, 3307 insertions(+), 61 deletions(-) create mode 100644 specs/task-mention-user.spec.md create mode 100644 src/cpu_worker.rs create mode 100644 src/room/member_search.rs diff --git a/specs/task-mention-user.spec.md b/specs/task-mention-user.spec.md new file mode 100644 index 000000000..db120b8c6 --- /dev/null +++ b/specs/task-mention-user.spec.md @@ -0,0 +1,152 @@ +spec: task +name: "Migrate @mention User Feature from Makepad 1.0 to 2.0" +tags: [feature, migration, mention, makepad-2.0] +estimate: 3d +--- + +## Intent + +Migrate the complete @mention user feature from the robrix (Makepad 1.0) project to robrix2 (Makepad 2.0). The 1.0 project at `/Users/zhangalex/Work/Projects/FW/robius/robrix` has a fully working mention system (~4600 lines) covering autocomplete popup, background member search, mention insertion as markdown links, mention metadata in sent messages, and pill-style rendering in the timeline. The robrix2 project currently has only a placeholder `MentionableTextInput` with no actual mention functionality. The existing `CommandTextInput` popup infrastructure in robrix2 provides the autocomplete foundation to build upon. + +## Decisions + +- Reuse `CommandTextInput` as the popup base widget for mention autocomplete — do not recreate popup from scratch +- Port `member_search.rs` algorithm as-is (pure Rust, no UI framework changes needed) +- Port `cpu_worker.rs` for background thread search execution via `cx.spawn_thread()` +- Mention insertion format: `[{username}](matrixUri)` markdown link with trailing space +- Track mentions via `possible_mentions: BTreeMap` and `possible_room_mention: bool` +- Extract real mentions from message text using `matrix_sdk::ruma::events::Mentions` before sending +- Enable existing `MatrixLinkPill` rendering in `html_or_plaintext.rs` by uncommenting Matrix URI parsing +- `@room` mention availability controlled by user's room power levels via `MentionableTextInputAction::PowerLevelsUpdated` +- Trigger character: `@` preceded by whitespace or start of text +- State machine: `Idle` → `WaitingForMembers` → `Searching` → `Idle` (also `JustCancelled` on ESC) +- Search results streamed in batches of 10 via MPSC channel for progressive UI updates +- Cancellation via `Arc` token for graceful background search abort +- Makepad 2.0 syntax: `script_mod!` DSL, `#[derive(Script, ScriptHook, Widget)]`, `script_apply_eval!` for runtime property updates + +## Boundaries + +### Allowed Changes +- src/shared/mentionable_text_input.rs +- src/shared/html_or_plaintext.rs +- src/room/member_search.rs (new) +- src/cpu_worker.rs (new) +- src/room/mod.rs +- src/lib.rs +- src/home/room_screen.rs +- src/home/editing_pane.rs + +### Forbidden +- Do not modify `CommandTextInput` internals — extend via composition only +- Do not add new cargo dependencies — use existing `matrix_sdk`, `unicode_segmentation`, `ruma` crates +- Do not change the message sending pipeline in `sliding_sync.rs` +- Do not modify the `RoomInputBar` DSL layout — the `mentionable_text_input` widget slot already exists + +## Out of Scope + +- Vertical alignment of inline MatrixLinkPill with surrounding text (known Makepad limitation) +- Mention extraction during message editing (editing_pane.rs has a TODO for this) +- Custom pill colors per user +- Mention notification sound/vibration +- Desktop vs mobile adaptive layout for popup items (use single layout initially) + +## Completion Criteria + +Scenario: Typing @ triggers mention popup + Test: manual_test_at_trigger_popup + Given a room with at least 3 members + When the user types "@" in the message input + Then a popup appears with header "Users in this Room" + And the popup lists room members with avatars and display names + +Scenario: Search filters members by typed text + Test: manual_test_mention_search_filter + Given a room with members "Alice", "Bob", "Alex" + When the user types "@al" + Then the popup shows "Alice" and "Alex" + And the popup does not show "Bob" + +Scenario: Selecting a mention inserts markdown link + Test: manual_test_mention_insert_markdown + Given the mention popup is showing results + When the user selects "Alice" from the popup + Then the input text contains "[Alice](matrix:u/alice:example.com) " + And the popup closes + And the cursor is positioned after the trailing space + +Scenario: ESC dismisses mention popup + Test: manual_test_esc_dismisses_popup + Given the mention popup is open + When the user presses ESC + Then the popup closes + And typing another "@" immediately does not re-open the popup (JustCancelled state) + +Scenario: Sent message includes Mentions metadata + Test: manual_test_mentions_metadata_in_sent_message + Given the user inserted a mention for "@alice:example.com" in the input + When the user sends the message + Then the `RoomMessageEventContent` includes `Mentions` with `user_ids` containing "alice:example.com" + +Scenario: @room mention requires power level + Test: manual_test_at_room_power_level + Given the user has room notification power level + When the user types "@room" and selects the @room item + Then the message includes `Mentions` with `room: true` + +Scenario: @room hidden when user lacks power level + Test: manual_test_at_room_hidden_without_power + Given the user does not have room notification power level + When the user types "@" + Then the popup does not show the @room option + +Scenario: Member search runs in background without UI freeze + Test: manual_test_background_search_responsive + Given a room with 1000+ members + When the user types "@a" + Then the UI remains responsive during search + And results appear progressively as they are found + +Scenario: MatrixLinkPill renders for received mentions + Test: manual_test_pill_renders_in_timeline + Given a message containing an HTML mention link `Alice` + When the message is displayed in the timeline + Then a pill-style widget renders with avatar and display name "Alice" + +Scenario: Current user mention renders with red background + Test: manual_test_current_user_pill_red + Given a message mentions the current logged-in user + When the message is displayed in the timeline + Then the mention pill renders with red background color `#d91b38` + +Scenario: Empty search shows no-matches indicator + Test: manual_test_no_matches_indicator + Given a room with members "Alice", "Bob" + When the user types "@zzzzzzz" + Then the popup shows "No matching users found" indicator + +Scenario: Member search handles Unicode names + Test: manual_test_unicode_member_search + Given a room with a member named "张三" (Zhang San) + When the user types "@张" + Then the popup shows "张三" in the results + +Scenario: Mention popup handles room with no loaded members gracefully + Test: manual_test_no_members_loaded_error + Given a room where members have not been fetched yet + When the user types "@" + Then the popup shows a loading indicator + And the popup does not crash or show empty results prematurely + +Scenario: Stale search results are discarded after cancellation + Test: manual_test_stale_search_discarded + Given a background member search is in progress for "@al" + When the user presses ESC to cancel and then types "@bo" + Then results from the first search "@al" are discarded + And only results matching "@bo" are displayed + +Scenario: Member search compiles and runs correctly in Makepad 2.0 + Test: manual_test_member_search_ported + Given the `member_search.rs` module is ported from robrix 1.0 + When `cargo build` is executed + Then the project compiles without errors + And `search_room_members_streaming_with_sort()` produces correct results diff --git a/src/cpu_worker.rs b/src/cpu_worker.rs new file mode 100644 index 000000000..95da8a361 --- /dev/null +++ b/src/cpu_worker.rs @@ -0,0 +1,56 @@ +//! Lightweight wrapper for CPU-bound tasks. +//! +//! Currently each job is handled by spawning a detached native thread via +//! Makepad's `cx.spawn_thread`. This keeps the implementation simple while +//! still moving CPU-heavy work off the UI thread. + +use makepad_widgets::{Cx, CxOsApi}; +use std::sync::{atomic::AtomicBool, mpsc::Sender, Arc}; +use crate::{ + room::member_search::{search_room_members_streaming_with_sort, PrecomputedMemberSort}, + shared::mentionable_text_input::SearchResult, +}; +use matrix_sdk::room::RoomMember; + +pub enum CpuJob { + SearchRoomMembers(SearchRoomMembersJob), +} + +pub struct SearchRoomMembersJob { + pub members: Arc>, + pub search_text: String, + pub max_results: usize, + pub sender: Sender, + pub search_id: u64, + pub precomputed_sort: Option>, + pub cancel_token: Option>, +} + +fn run_member_search(params: SearchRoomMembersJob) { + let SearchRoomMembersJob { + members, + search_text, + max_results, + sender, + search_id, + precomputed_sort, + cancel_token, + } = params; + + search_room_members_streaming_with_sort( + members, + search_text, + max_results, + sender, + search_id, + precomputed_sort, + cancel_token, + ); +} + +/// Spawns a CPU-bound job on a detached native thread. +pub fn spawn_cpu_job(cx: &mut Cx, job: CpuJob) { + cx.spawn_thread(move || match job { + CpuJob::SearchRoomMembers(params) => run_member_search(params), + }); +} diff --git a/src/lib.rs b/src/lib.rs index e730e3b75..4bcc78ecf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,6 +73,7 @@ pub mod tsp_dummy; // Matrix stuff +pub mod cpu_worker; pub mod sliding_sync; pub mod space_service_sync; pub mod avatar_cache; diff --git a/src/room/member_search.rs b/src/room/member_search.rs new file mode 100644 index 000000000..99db6911f --- /dev/null +++ b/src/room/member_search.rs @@ -0,0 +1,1213 @@ +//! Room member search functionality for @mentions +//! +//! This module provides efficient searching of room members with streaming results +//! to support responsive UI when users type @mentions. + +use std::collections::BinaryHeap; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::Sender, + Arc, +}; +use matrix_sdk::{room::{RoomMember, RoomMemberRole}, ruma::OwnedUserId}; +use unicode_segmentation::UnicodeSegmentation; +use crate::shared::mentionable_text_input::SearchResult; +use crate::sliding_sync::current_user_id; +use makepad_widgets::log; + +const BATCH_SIZE: usize = 10; // Number of results per streamed batch + +/// Pre-computed member sort key for fast empty search +#[derive(Debug, Clone)] +pub struct MemberSortKey { + /// Power level rank: 0=Admin, 1=Moderator, 2=User + pub power_rank: u8, + /// Name category: 0=Alphabetic, 1=Numeric, 2=Symbols + pub name_category: u8, + /// Normalized lowercase name for sorting + pub sort_key: String, +} + +/// Pre-computed sorted indices and keys for room members +#[derive(Debug, Clone)] +pub struct PrecomputedMemberSort { + /// Sorted indices into the members array + pub sorted_indices: Vec, + /// Pre-computed sort keys (parallel to original members array) + pub member_keys: Vec, +} + +/// Pre-compute sort keys and indices for room members +/// This is called once when members are fetched, avoiding repeated computation +pub fn precompute_member_sort(members: &[RoomMember]) -> PrecomputedMemberSort { + let current_user_id = current_user_id(); + let mut member_keys = Vec::with_capacity(members.len()); + let mut sortable_members = Vec::with_capacity(members.len()); + + for (index, member) in members.iter().enumerate() { + // Skip current user + if let Some(ref current_id) = current_user_id { + if member.user_id() == current_id { + // Add placeholder for current user to maintain index alignment + member_keys.push(MemberSortKey { + power_rank: 255, // Will be filtered out + name_category: 255, + sort_key: String::new(), + }); + continue; + } + } + + // Get power level rank + let power_rank = role_to_rank(member.suggested_role_for_power_level()); + + // Get normalized display name + let raw_name = member + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| member.user_id().localpart()); + + // Generate sort key by stripping leading non-alphanumeric + let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); + let sort_key = if stripped.is_empty() { + // Name is all symbols, use original + if raw_name.is_ascii() { + raw_name.to_ascii_lowercase() + } else { + raw_name.to_lowercase() + } + } else { + // Use stripped version for sorting + if stripped.is_ascii() { + stripped.to_ascii_lowercase() + } else { + stripped.to_lowercase() + } + }; + + // Determine name category based on stripped name for consistency + // This makes "!!!alice" categorized as alphabetic, not symbols + let name_category = if !stripped.is_empty() { + // Use first char of stripped name + match stripped.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + } + } else { + // Name is all symbols, use original first char + match raw_name.chars().next() { + Some(c) if c.is_alphabetic() => 0, // Shouldn't happen if stripped is empty + Some(c) if c.is_numeric() => 1, // Shouldn't happen if stripped is empty + _ => 2, // Symbols + } + }; + + let key = MemberSortKey { + power_rank, + name_category, + sort_key: sort_key.clone(), + }; + + member_keys.push(key.clone()); + sortable_members.push((power_rank, name_category, sort_key, index)); + } + + // Sort all valid members + sortable_members.sort_by(|a, b| match a.0.cmp(&b.0) { + std::cmp::Ordering::Equal => match a.1.cmp(&b.1) { + std::cmp::Ordering::Equal => a.2.cmp(&b.2), + other => other, + }, + other => other, + }); + + // Extract sorted indices + let sorted_indices: Vec = sortable_members + .into_iter() + .map(|(_, _, _, idx)| idx) + .collect(); + + PrecomputedMemberSort { + sorted_indices, + member_keys, + } +} + +/// Maps a member role to a sortable rank (lower value = higher priority) +fn role_to_rank(role: RoomMemberRole) -> u8 { + match role { + RoomMemberRole::Administrator | RoomMemberRole::Creator => 0, + RoomMemberRole::Moderator => 1, + RoomMemberRole::User => 2, + } +} + +fn is_cancelled(token: &Option>) -> bool { + token + .as_ref() + .map(|flag| flag.load(Ordering::Relaxed)) + .unwrap_or(false) +} + +fn send_search_update( + sender: &Sender, + cancel_token: &Option>, + search_id: u64, + search_text: &Arc, + results: Vec, + is_complete: bool, +) -> bool { + if is_cancelled(cancel_token) { + return false; + } + + let search_result = SearchResult { + search_id, + results, + is_complete, + search_text: Arc::clone(search_text), + }; + + if sender.send(search_result).is_err() { + log!("Failed to send search results - receiver dropped"); + return false; + } + + true +} + +fn stream_index_batches( + indices: &[usize], + sender: &Sender, + cancel_token: &Option>, + search_id: u64, + search_text: &Arc, +) -> bool { + if indices.is_empty() { + return send_search_update(sender, cancel_token, search_id, search_text, Vec::new(), true); + } + + let mut start = 0; + while start < indices.len() { + let end = (start + BATCH_SIZE).min(indices.len()); + let batch = indices[start..end].to_vec(); + start = end; + let is_last = start >= indices.len(); + + if !send_search_update(sender, cancel_token, search_id, search_text, batch, is_last) { + return false; + } + } + + true +} + +fn compute_empty_search_indices( + members: &[RoomMember], + max_results: usize, + current_user_id: Option<&OwnedUserId>, + precomputed_sort: Option<&PrecomputedMemberSort>, + cancel_token: &Option>, +) -> Option> { + if is_cancelled(cancel_token) { + return None; + } + + if let Some(sort_data) = precomputed_sort { + let mut indices: Vec = sort_data + .sorted_indices + .iter() + .take(max_results) + .copied() + .collect(); + + if max_results == 0 { + indices.clear(); + } + + return Some(indices); + } + + let mut valid_members: Vec<(u8, u8, usize)> = Vec::with_capacity(members.len()); + + for (index, member) in members.iter().enumerate() { + if is_cancelled(cancel_token) { + return None; + } + + if current_user_id.is_some_and(|id| member.user_id() == id) { + continue; + } + + let power_rank = role_to_rank(member.suggested_role_for_power_level()); + + let raw_name = member + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| member.user_id().localpart()); + + let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); + let name_category = if !stripped.is_empty() { + match stripped.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + } + } else { + 2 + }; + + valid_members.push((power_rank, name_category, index)); + } + + if is_cancelled(cancel_token) { + return None; + } + + valid_members.sort_by(|a, b| match a.0.cmp(&b.0) { + std::cmp::Ordering::Equal => match a.1.cmp(&b.1) { + std::cmp::Ordering::Equal => { + let name_a = members[a.2] + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| members[a.2].user_id().localpart()); + let name_b = members[b.2] + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| members[b.2].user_id().localpart()); + + name_a + .chars() + .map(|c| c.to_ascii_lowercase()) + .cmp(name_b.chars().map(|c| c.to_ascii_lowercase())) + } + other => other, + }, + other => other, + }); + + if is_cancelled(cancel_token) { + return None; + } + + valid_members.truncate(max_results); + + Some(valid_members.into_iter().map(|(_, _, idx)| idx).collect()) +} + +fn compute_non_empty_search_indices( + members: &[RoomMember], + search_text: &str, + max_results: usize, + current_user_id: Option<&OwnedUserId>, + precomputed_sort: Option<&PrecomputedMemberSort>, + cancel_token: &Option>, +) -> Option> { + if is_cancelled(cancel_token) { + return None; + } + + let mut top_matches: BinaryHeap<(u8, usize)> = BinaryHeap::with_capacity(max_results); + let mut high_priority_count = 0; + let mut best_priority_seen = u8::MAX; + + for (index, member) in members.iter().enumerate() { + if is_cancelled(cancel_token) { + return None; + } + + if current_user_id.is_some_and(|id| member.user_id() == id) { + continue; + } + + if let Some(priority) = match_member_with_priority(member, search_text) { + if priority <= 3 { + high_priority_count += 1; + } + best_priority_seen = best_priority_seen.min(priority); + + if top_matches.len() < max_results { + top_matches.push((priority, index)); + } else if let Some(&(worst_priority, _)) = top_matches.peek() { + if priority < worst_priority { + top_matches.pop(); + top_matches.push((priority, index)); + } + } + + if max_results > 0 + && high_priority_count >= max_results * 2 + && top_matches.len() == max_results + && best_priority_seen == 0 + { + break; + } + } + } + + if is_cancelled(cancel_token) { + return None; + } + + let mut all_matches: Vec<(u8, usize)> = top_matches.into_iter().collect(); + + all_matches.sort_by(|(priority_a, idx_a), (priority_b, idx_b)| { + match priority_a.cmp(priority_b) { + std::cmp::Ordering::Equal => { + if let Some(sort_data) = precomputed_sort { + let key_a = &sort_data.member_keys[*idx_a]; + let key_b = &sort_data.member_keys[*idx_b]; + + match key_a.power_rank.cmp(&key_b.power_rank) { + std::cmp::Ordering::Equal => match key_a.name_category.cmp(&key_b.name_category) { + std::cmp::Ordering::Equal => key_a.sort_key.cmp(&key_b.sort_key), + other => other, + }, + other => other, + } + } else { + let member_a = &members[*idx_a]; + let member_b = &members[*idx_b]; + + let power_a = role_to_rank(member_a.suggested_role_for_power_level()); + let power_b = role_to_rank(member_b.suggested_role_for_power_level()); + + match power_a.cmp(&power_b) { + std::cmp::Ordering::Equal => { + let name_a = member_a + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| member_a.user_id().localpart()); + let name_b = member_b + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| member_b.user_id().localpart()); + + if name_a.is_ascii() && name_b.is_ascii() { + name_a + .chars() + .map(|c| c.to_ascii_lowercase()) + .cmp(name_b.chars().map(|c| c.to_ascii_lowercase())) + } else { + name_a.to_lowercase().cmp(&name_b.to_lowercase()) + } + } + other => other, + } + } + } + other => other, + } + }); + + if is_cancelled(cancel_token) { + return None; + } + + Some(all_matches.into_iter().map(|(_, idx)| idx).collect()) +} + +/// Search room members with optional pre-computed sort data +pub fn search_room_members_streaming_with_sort( + members: Arc>, + search_text: String, + max_results: usize, + sender: Sender, + search_id: u64, + precomputed_sort: Option>, + cancel_token: Option>, +) { + let current_user_id = current_user_id(); + let search_text_arc = Arc::new(search_text); + + // Early exit if cancelled - send completion signal so UI doesn't wait indefinitely + if is_cancelled(&cancel_token) { + let _ = send_search_update(&sender, &cancel_token, search_id, &search_text_arc, Vec::new(), true); + return; + } + + let search_query = search_text_arc.as_str(); + let precomputed_ref = precomputed_sort.as_deref(); + let cancel_ref = &cancel_token; + let members_slice = members.as_ref(); + + let results = if search_query.is_empty() { + match compute_empty_search_indices( + members_slice, + max_results, + current_user_id.as_ref(), + precomputed_ref, + cancel_ref, + ) { + Some(indices) => indices, + None => { + // Cancelled during computation - send completion signal + let _ = send_search_update(&sender, &cancel_token, search_id, &search_text_arc, Vec::new(), true); + return; + } + } + } else { + match compute_non_empty_search_indices( + members_slice, + search_query, + max_results, + current_user_id.as_ref(), + precomputed_ref, + cancel_ref, + ) { + Some(indices) => indices, + None => { + // Cancelled during computation - send completion signal + let _ = send_search_update(&sender, &cancel_token, search_id, &search_text_arc, Vec::new(), true); + return; + } + } + }; + + let _ = stream_index_batches(&results, &sender, cancel_ref, search_id, &search_text_arc); +} + + +/// Check if search_text appears after a word boundary in text +/// Word boundaries include: punctuation, symbols, and other non-alphanumeric characters +/// For ASCII text, also supports case-insensitive matching +fn check_word_boundary_match(text: &str, search_text: &str, case_insensitive: bool) -> bool { + if search_text.is_empty() { + return false; + } + + if case_insensitive && search_text.is_ascii() { + let search_len = search_text.len(); + for (index, _) in text.char_indices() { + if index == 0 || index + search_len > text.len() { + continue; + } + if substring_eq_ignore_ascii_case(text, index, search_text) { + if let Some(prev_char) = text[..index].chars().last() { + if !prev_char.is_alphanumeric() { + return true; + } + } + } + } + false + } else { + for (index, _) in text.match_indices(search_text) { + if index == 0 { + continue; // Already handled by starts_with checks + } + + if let Some(prev_char) = text[..index].chars().last() { + if !prev_char.is_alphanumeric() { + return true; + } + } + } + false + } +} + +/// Check if a string starts with another string based on grapheme clusters +/// +/// ## What are Grapheme Clusters? +/// +/// A grapheme cluster is what users perceive as a single "character". This is NOT about +/// phonetics/pronunciation, but about visual representation. Examples: +/// +/// - "👨‍👩‍👧‍👦" (family emoji) looks like 1 character but is actually 7 Unicode code points +/// - "é" might be 1 precomposed character or 2 characters (e + ´ combining accent) +/// - "🇺🇸" (flag) is 2 regional indicator symbols that combine into 1 visual character +/// +/// ## Why is this needed? +/// +/// Standard string operations like `starts_with()` work on bytes or chars, which can +/// break these multi-codepoint characters. For @mentions, users expect: +/// - Typing "👨‍👩‍👧‍👦" should match a username starting with that family emoji +/// - Typing "é" should match whether the username uses precomposed or decomposed form +/// +/// ## When is this function called? +/// +/// This function is ONLY used when the search text contains complex Unicode characters +/// (when grapheme count != char count). For regular ASCII or simple Unicode, the +/// standard `starts_with()` is used for better performance. +/// +/// ## Performance Note +/// +/// This function is intentionally not called for common cases (ASCII usernames, +/// simple Chinese characters) to avoid the overhead of grapheme segmentation. +fn grapheme_starts_with(haystack: &str, needle: &str, case_insensitive: bool) -> bool { + if needle.is_empty() { + return true; + } + + let haystack_graphemes: Vec<&str> = haystack.graphemes(true).collect(); + let needle_graphemes: Vec<&str> = needle.graphemes(true).collect(); + + if needle_graphemes.len() > haystack_graphemes.len() { + return false; + } + + for i in 0..needle_graphemes.len() { + let h_grapheme = haystack_graphemes[i]; + let n_grapheme = needle_graphemes[i]; + + let grapheme_matches = if case_insensitive && h_grapheme.is_ascii() && n_grapheme.is_ascii() + { + h_grapheme.to_lowercase() == n_grapheme.to_lowercase() + } else { + h_grapheme == n_grapheme + }; + + if !grapheme_matches { + return false; + } + } + + true +} + +/// Match a member against search text and return priority if matched +/// Returns None if no match, Some(priority) if matched (lower priority = better match) +/// +/// Follows Matrix official recommendations for matching order: +/// 1. Exact display name match +/// 2. Exact user ID match +/// 3. Display name starts with search text +/// 4. User ID starts with search text +/// 5. Display name contains search text (at word boundary) +/// 6. User ID contains search text +fn match_member_with_priority(member: &RoomMember, search_text: &str) -> Option { + if search_text.is_empty() { + return Some(10); + } + + let display_name = member.display_name(); + let user_id = member.user_id().as_str(); + let localpart = member.user_id().localpart(); + let case_insensitive = search_text.is_ascii(); + let search_without_at = search_text.strip_prefix('@').unwrap_or(search_text); + let search_has_at = search_without_at.len() != search_text.len(); + + for matcher in MATCHERS { + if let Some(priority) = reducer( + matcher, + search_text, + display_name, + user_id, + localpart, + case_insensitive, + search_has_at, + ) { + return Some(priority); + } + } + + if !case_insensitive && search_text.graphemes(true).count() != search_text.chars().count() { + if let Some(display) = display_name { + if grapheme_starts_with(display, search_text, false) { + return Some(8); + } + } + } + + None +} + +#[derive(Copy, Clone)] +struct Matcher { + priority: u8, + func: fn( + search_text: &str, + display_name: Option<&str>, + user_id: &str, + localpart: &str, + case_insensitive: bool, + search_has_at: bool, + ) -> bool, +} + +const MATCHERS: &[Matcher] = &[ + Matcher { + priority: 0, + func: |search_text, display_name, _, _, _, _| display_name == Some(search_text), + }, + Matcher { + priority: 1, + func: |search_text, display_name, _, _, case_insensitive, _| { + case_insensitive && display_name.is_some_and(|d| d.eq_ignore_ascii_case(search_text)) + }, + }, + Matcher { + priority: 2, + func: |search_text, _, user_id, _, _, search_has_at| { + user_id == search_text + || (!search_has_at + && user_id.starts_with('@') + && user_id.strip_prefix('@') == Some(search_text)) + }, + }, + Matcher { + priority: 3, + func: |search_text, _, user_id, _, case_insensitive, search_has_at| { + case_insensitive + && (user_id.eq_ignore_ascii_case(search_text) + || (!search_has_at + && user_id.starts_with('@') + && user_id.strip_prefix('@').is_some_and(|id| { + id.eq_ignore_ascii_case(search_text) + }))) + }, + }, + Matcher { + priority: 4, + func: |search_text, display_name, _, _, _, _| { + display_name.is_some_and(|d| d.starts_with(search_text)) + }, + }, + Matcher { + priority: 5, + func: |search_text, display_name, _, _, case_insensitive, _| { + case_insensitive + && display_name.is_some_and(|d| starts_with_ignore_ascii_case(d, search_text)) + }, + }, + Matcher { + priority: 6, + func: |search_text, _, user_id, localpart, _, search_has_at| { + user_id.starts_with(search_text) + || (!search_has_at + && user_id.starts_with('@') + && user_id.strip_prefix('@').is_some_and(|id| id.starts_with(search_text))) + || localpart.starts_with(search_text) + }, + }, + Matcher { + priority: 7, + func: |search_text, _, user_id, localpart, case_insensitive, search_has_at| { + case_insensitive + && (starts_with_ignore_ascii_case(user_id, search_text) + || starts_with_ignore_ascii_case(localpart, search_text) + || (!search_has_at + && user_id.starts_with('@') + && user_id.strip_prefix('@').is_some_and(|id| { + starts_with_ignore_ascii_case(id, search_text) + }))) + }, + }, + Matcher { + priority: 8, + func: |search_text, display_name, _, _, case_insensitive, _| { + display_name.is_some_and(|display| { + check_word_boundary_match(display, search_text, case_insensitive) + || display.contains(search_text) + || (case_insensitive + && contains_ignore_ascii_case(display, search_text)) + }) + }, + }, + Matcher { + priority: 9, + func: |search_text, _, user_id, localpart, case_insensitive, _| { + if case_insensitive { + contains_ignore_ascii_case(user_id, search_text) + || contains_ignore_ascii_case(localpart, search_text) + } else { + user_id.contains(search_text) || localpart.contains(search_text) + } + }, + }, +]; + +fn reducer( + matcher: &Matcher, + search_text: &str, + display_name: Option<&str>, + user_id: &str, + localpart: &str, + case_insensitive: bool, + search_has_at: bool, +) -> Option { + if (matcher.func)( + search_text, + display_name, + user_id, + localpart, + case_insensitive, + search_has_at, + ) { + Some(matcher.priority) + } else { + None + } +} + +/// Returns true if the `haystack` starts with `needle` ignoring ASCII case. +fn starts_with_ignore_ascii_case(haystack: &str, needle: &str) -> bool { + haystack + .get(..needle.len()) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(needle)) +} + +/// Returns true if the `haystack` contains `needle` ignoring ASCII case. +fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool { + if needle.is_empty() { + return true; + } + if !needle.is_ascii() { + return haystack.contains(needle); + } + let needle_len = needle.len(); + for (index, _) in haystack.char_indices() { + if index + needle_len > haystack.len() { + break; + } + if substring_eq_ignore_ascii_case(haystack, index, needle) { + return true; + } + } + false +} + +fn substring_eq_ignore_ascii_case(haystack: &str, start: usize, needle: &str) -> bool { + haystack + .get(start..start.saturating_add(needle.len())) + .is_some_and(|segment| segment.eq_ignore_ascii_case(needle)) +} + +// typos:disable +#[cfg(test)] +mod tests { + use super::*; + use matrix_sdk::room::RoomMemberRole; + use std::sync::mpsc::channel; + + #[test] + fn test_send_search_update_respects_cancellation() { + let (tx, rx) = channel(); + let cancel = Some(Arc::new(AtomicBool::new(true))); + let query = Arc::new("query".to_owned()); + + let result = send_search_update(&tx, &cancel, 1, &query, vec![1], false); + + assert!(!result); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn test_stream_index_batches_emits_completion() { + let (tx, rx) = channel(); + let cancel = None; + let query = Arc::new("abc".to_owned()); + + assert!(stream_index_batches(&[1, 2], &tx, &cancel, 7, &query)); + + let message = rx.recv().expect("expected batched result"); + assert_eq!(message.results, vec![1, 2]); + assert!(message.is_complete); + assert_eq!(message.search_id, 7); + assert_eq!(message.search_text.as_str(), "abc"); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn test_stream_index_batches_cancelled_before_send() { + let (tx, rx) = channel(); + let cancel = Some(Arc::new(AtomicBool::new(true))); + let query = Arc::new("abc".to_owned()); + + assert!(!stream_index_batches(&[1, 2], &tx, &cancel, 3, &query)); + assert!(rx.try_recv().is_err()); + } + + #[test] + fn test_role_to_rank() { + // Verify that admin < moderator < user in terms of rank + assert_eq!(role_to_rank(RoomMemberRole::Administrator), 0); + assert_eq!(role_to_rank(RoomMemberRole::Moderator), 1); + assert_eq!(role_to_rank(RoomMemberRole::User), 2); + + // Verify ordering + assert!( + role_to_rank(RoomMemberRole::Administrator) < role_to_rank(RoomMemberRole::Moderator) + ); + assert!(role_to_rank(RoomMemberRole::Moderator) < role_to_rank(RoomMemberRole::User)); + } + + #[test] + fn test_top_k_selection_correctness() { + use std::collections::BinaryHeap; + + // Simulate Top-K selection with mixed priorities + let test_data = vec![ + (5, "user5"), // priority 5 + (1, "user1"), // priority 1 (better) + (3, "user3"), // priority 3 + (0, "user0"), // priority 0 (best) + (8, "user8"), // priority 8 (worst) + (2, "user2"), // priority 2 + (4, "user4"), // priority 4 + (1, "user1b"), // priority 1 (tie) + ]; + + let max_results = 3; + let mut top_matches: BinaryHeap<(u8, &str)> = BinaryHeap::with_capacity(max_results); + + // Apply the same algorithm as in search + for (priority, name) in test_data { + if top_matches.len() < max_results { + top_matches.push((priority, name)); + } else if let Some(&(worst_priority, _)) = top_matches.peek() { + if priority < worst_priority { + top_matches.pop(); + top_matches.push((priority, name)); + } + } + } + + // Extract and sort results + let mut results: Vec<(u8, &str)> = top_matches.into_iter().collect(); + results.sort_by_key(|&(priority, _)| priority); + + // Verify we got the top 3 with lowest priorities + assert_eq!(results.len(), 3); + assert_eq!(results[0].0, 0); // Best priority + assert_eq!(results[1].0, 1); // Second best + assert_eq!(results[2].0, 1); // Tied second best + + // Verify the worst candidates were excluded + assert!(!results.iter().any(|&(p, _)| p >= 4)); + } + + #[test] + fn test_word_boundary_case_insensitive() { + // Test case-insensitive word boundary matching for ASCII + assert!(check_word_boundary_match("Hello, Alice", "alice", true)); + assert!(check_word_boundary_match("@BOB is here", "bob", true)); + assert!(check_word_boundary_match("Meet CHARLIE!", "charlie", true)); + assert!(check_word_boundary_match("user:David", "david", true)); + + // Should not match in middle of word (case-insensitive) + assert!(!check_word_boundary_match("AliceSmith", "lice", true)); + assert!(!check_word_boundary_match("BOBCAT", "cat", true)); + + // Test case-sensitive mode + assert!(check_word_boundary_match("Hello, alice", "alice", false)); + assert!(!check_word_boundary_match("Hello, Alice", "alice", false)); + + // Test with mixed case in search text + assert!(check_word_boundary_match("Hello, Alice", "Alice", true)); + assert!(check_word_boundary_match("Hello, Alice", "Alice", false)); + } + + #[test] + fn test_name_category_with_stripped_prefix() { + // Helper to determine name category (matching the actual implementation) + fn get_name_category(raw_name: &str) -> u8 { + let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); + if !stripped.is_empty() { + match stripped.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + } + } else { + 2 // All symbols + } + } + + // Test normal names + assert_eq!(get_name_category("alice"), 0); // Alphabetic + assert_eq!(get_name_category("123user"), 1); // Numeric + assert_eq!(get_name_category("@#$%"), 2); // All symbols + + // Test names with symbol prefixes + assert_eq!(get_name_category("!!!alice"), 0); // Should be alphabetic after stripping + assert_eq!(get_name_category("@bob"), 0); // Should be alphabetic after stripping + assert_eq!(get_name_category("___123"), 1); // Should be numeric after stripping + assert_eq!(get_name_category("#$%alice"), 0); // Should be alphabetic after stripping + + // Test edge cases + assert_eq!(get_name_category(""), 2); // Empty -> symbols + assert_eq!(get_name_category("!!!"), 2); // All symbols -> symbols + } + + #[test] + fn test_grapheme_starts_with_basic() { + // Basic ASCII cases + assert!(grapheme_starts_with("hello", "hel", false)); + assert!(grapheme_starts_with("hello", "hello", false)); + assert!(!grapheme_starts_with("hello", "llo", false)); + assert!(grapheme_starts_with("hello", "", false)); + assert!(!grapheme_starts_with("hi", "hello", false)); + } + + #[test] + fn test_grapheme_starts_with_case_sensitivity() { + // Case-insensitive for ASCII + assert!(grapheme_starts_with("Hello", "hel", true)); + assert!(grapheme_starts_with("HELLO", "hel", true)); + assert!(!grapheme_starts_with("Hello", "hel", false)); + + // Case-insensitive only works for ASCII + assert!(!grapheme_starts_with("Привет", "прив", true)); // Russian + } + + #[test] + fn test_grapheme_starts_with_emojis() { + // Family emoji (multiple code points appearing as single character) + let family = "👨‍👩‍👧‍👦"; // 7 code points, 1 grapheme + assert!(grapheme_starts_with("👨‍👩‍👧‍👦 Smith Family", "👨‍👩‍👧‍👦", false)); + assert!(grapheme_starts_with(family, family, false)); + + // Flag emojis (regional indicators) + assert!(grapheme_starts_with("🇺🇸 USA", "🇺🇸", false)); + assert!(grapheme_starts_with("🇯🇵 Japan", "🇯🇵", false)); + + // Skin tone modifiers + assert!(grapheme_starts_with("👋🏽 Hello", "👋🏽", false)); + assert!(!grapheme_starts_with("👋🏽 Hello", "👋", false)); // Different without modifier + + // Complex emoji sequences + assert!(grapheme_starts_with("🧑‍💻 Developer", "🧑‍💻", false)); + } + + #[test] + fn test_grapheme_starts_with_combining_characters() { + // Precomposed vs decomposed forms + let precomposed = "café"; // é as single character (U+00E9) + let decomposed = "cafe\u{0301}"; // e + combining acute accent (U+0065 + U+0301) + + // Both should work + assert!(grapheme_starts_with(precomposed, "caf", false)); + assert!(grapheme_starts_with(decomposed, "caf", false)); + + // Other combining characters + assert!(grapheme_starts_with("naïve", "naï", false)); // ï with diaeresis + assert!(grapheme_starts_with("piñata", "piñ", false)); // ñ with tilde + } + + #[test] + fn test_grapheme_starts_with_various_scripts() { + // Chinese + assert!(grapheme_starts_with("张三", "张", false)); + + // Japanese (Hiragana + Kanji) + assert!(grapheme_starts_with("こんにちは", "こん", false)); + assert!(grapheme_starts_with("日本語", "日本", false)); + + // Korean + assert!(grapheme_starts_with("안녕하세요", "안녕", false)); + + // Arabic (RTL) + assert!(grapheme_starts_with("مرحبا", "مر", false)); + + // Hindi with complex ligatures + assert!(grapheme_starts_with("नमस्ते", "नम", false)); + + // Thai with combining marks + assert!(grapheme_starts_with("สวัสดี", "สวั", false)); + } + + #[test] + fn test_grapheme_starts_with_zero_width_joiners() { + // Zero-width joiner sequences + let zwj_sequence = "👨‍⚕️"; // Man + ZWJ + Medical symbol + assert!(grapheme_starts_with("👨‍⚕️ Dr. Smith", zwj_sequence, false)); + + // Gender-neutral sequences + assert!(grapheme_starts_with("🧑‍🎓 Student", "🧑‍🎓", false)); + } + + #[test] + fn test_grapheme_starts_with_edge_cases() { + // Empty strings + assert!(grapheme_starts_with("", "", false)); + assert!(!grapheme_starts_with("", "a", false)); + + // Single grapheme vs multiple + assert!(grapheme_starts_with("a", "a", false)); + assert!(!grapheme_starts_with("a", "ab", false)); + + // Whitespace handling + assert!(grapheme_starts_with(" hello", " ", false)); + assert!(grapheme_starts_with("\nhello", "\n", false)); + } + + #[test] + fn test_word_boundary_match() { + // Test case-sensitive word boundary scenarios + assert!(check_word_boundary_match("Hello,alice", "alice", false)); + assert!(check_word_boundary_match("(bob) is here", "bob", false)); + assert!(check_word_boundary_match("user:charlie", "charlie", false)); + assert!(check_word_boundary_match("@david!", "david", false)); + assert!(check_word_boundary_match("eve.smith", "smith", false)); + assert!(check_word_boundary_match("frank-jones", "jones", false)); + + // Test case-insensitive matching (ASCII) + assert!(check_word_boundary_match("Hello,Alice", "alice", true)); + assert!(check_word_boundary_match("(Bob) is here", "bob", true)); + assert!(check_word_boundary_match("USER:Charlie", "charlie", true)); + assert!(check_word_boundary_match("@DAVID!", "david", true)); + + // Should not match in the middle of a word + assert!(!check_word_boundary_match("alice123", "lice", false)); + assert!(!check_word_boundary_match("bobcat", "cat", false)); + assert!(!check_word_boundary_match("Alice123", "lice", true)); + assert!(!check_word_boundary_match("BobCat", "cat", true)); + + // Edge cases + assert!(!check_word_boundary_match("test", "test", false)); // Starts with (handled elsewhere) + assert!(!check_word_boundary_match("", "test", false)); // Empty text + } + + #[test] + fn test_smart_sort_key_generation() { + // Helper function to simulate sort key generation + fn generate_sort_key(raw_name: &str) -> (u8, String) { + let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); + let sort_key = if stripped.is_empty() { + raw_name.to_lowercase() + } else { + stripped.to_lowercase() + }; + + // Three-tier ranking: alphabetic (0), numeric (1), symbols (2) + let rank = match raw_name.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + }; + + (rank, sort_key) + } + + // Test alphabetic names get rank 0 + assert_eq!(generate_sort_key("alice"), (0, "alice".to_string())); + assert_eq!(generate_sort_key("Bob"), (0, "bob".to_string())); + assert_eq!(generate_sort_key("张三"), (0, "张三".to_string())); + + // Test numeric names get rank 1 + assert_eq!(generate_sort_key("0user"), (1, "0user".to_string())); + assert_eq!(generate_sort_key("123abc"), (1, "123abc".to_string())); + assert_eq!(generate_sort_key("999test"), (1, "999test".to_string())); + + // Test symbol-prefixed names get rank 2 but sort by stripped version + assert_eq!(generate_sort_key("!!!alice"), (2, "alice".to_string())); + assert_eq!(generate_sort_key("@bob"), (2, "bob".to_string())); + assert_eq!(generate_sort_key("___charlie"), (2, "charlie".to_string())); + + // Test pure symbol names + assert_eq!(generate_sort_key("!!!"), (2, "!!!".to_string())); + assert_eq!(generate_sort_key("@@@"), (2, "@@@".to_string())); + + // Test ordering: alphabetic -> numeric -> symbols + let mut names = vec![ + ("!!!alice", generate_sort_key("!!!alice")), + ("0user", generate_sort_key("0user")), + ("alice", generate_sort_key("alice")), + ("123test", generate_sort_key("123test")), + ("@bob", generate_sort_key("@bob")), + ("bob", generate_sort_key("bob")), + ]; + + // Sort by (rank, sort_key) + names.sort_by(|a, b| match a.1.0.cmp(&b.1.0) { + std::cmp::Ordering::Equal => a.1.1.cmp(&b.1.1), + other => other, + }); + + // Verify order: alice, bob, 0user, 123test, !!!alice, @bob + assert_eq!(names[0].0, "alice"); + assert_eq!(names[1].0, "bob"); + assert_eq!(names[2].0, "0user"); + assert_eq!(names[3].0, "123test"); + assert_eq!(names[4].0, "!!!alice"); + assert_eq!(names[5].0, "@bob"); + } + + #[test] + fn test_role_to_rank_mapping() { + assert_eq!(role_to_rank(RoomMemberRole::Administrator), 0); + assert_eq!(role_to_rank(RoomMemberRole::Moderator), 1); + assert_eq!(role_to_rank(RoomMemberRole::User), 2); + } + + #[test] + fn test_top_k_heap_selection_priorities() { + // Simulate the heap logic used in non-empty search: keep K smallest priorities + fn top_k(items: &[(u8, usize)], k: usize) -> Vec<(u8, usize)> { + use std::collections::BinaryHeap; + let mut heap: BinaryHeap<(u8, usize)> = BinaryHeap::with_capacity(k); + for &(p, idx) in items { + if heap.len() < k { + heap.push((p, idx)); + } else if let Some(&(worst_p, _)) = heap.peek() { + if p < worst_p { + let _ = heap.pop(); + heap.push((p, idx)); + } + } + } + let mut out: Vec<(u8, usize)> = heap.into_iter().collect(); + out.sort_by_key(|(p, _)| *p); + out + } + + let items = vec![ + (9, 0), + (3, 1), + (5, 2), + (1, 3), + (2, 4), + (7, 5), + (0, 6), + (4, 7), + (6, 8), + (8, 9), + ]; + + // K = 3 should return priorities [0, 1, 2] + let k3 = top_k(&items, 3); + let priorities: Vec = k3.into_iter().map(|(p, _)| p).collect(); + assert_eq!(priorities, vec![0, 1, 2]); + + // K = 5 should return priorities [0, 1, 2, 3, 4] + let k5 = top_k(&items, 5); + let priorities: Vec = k5.into_iter().map(|(p, _)| p).collect(); + assert_eq!(priorities, vec![0, 1, 2, 3, 4]); + } + + #[test] + fn test_when_grapheme_search_is_used() { + // This test demonstrates when grapheme_starts_with is actually called + // in the user_matches_search function + + // Regular ASCII - grapheme count == char count + assert_eq!("hello".graphemes(true).count(), "hello".chars().count()); + + // Family emoji - grapheme count != char count + assert_ne!("👨‍👩‍👧‍👦".graphemes(true).count(), "👨‍👩‍👧‍👦".chars().count()); + assert_eq!("👨‍👩‍👧‍👦".graphemes(true).count(), 1); + assert_eq!("👨‍👩‍👧‍👦".chars().count(), 7); + + // Combining character - grapheme count != char count + // Using actual decomposed form: e (U+0065) + combining acute accent (U+0301) + let decomposed = "e\u{0301}"; // e + combining acute accent + assert_ne!( + decomposed.graphemes(true).count(), + decomposed.chars().count() + ); + assert_eq!(decomposed.graphemes(true).count(), 1); // Shows as 1 grapheme + assert_eq!(decomposed.chars().count(), 2); // But is 2 chars + + // Simple Chinese - grapheme count == char count + assert_eq!("你好".graphemes(true).count(), "你好".chars().count()); + } +} diff --git a/src/room/mod.rs b/src/room/mod.rs index 68b20bae9..01e4b84aa 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -7,6 +7,7 @@ use ruma::{OwnedRoomAliasId, OwnedRoomId, room::{JoinRuleSummary, RoomType}}; use crate::utils::RoomNameId; +pub mod member_search; pub mod reply_preview; pub mod room_input_bar; pub mod room_display_filter; diff --git a/src/shared/html_or_plaintext.rs b/src/shared/html_or_plaintext.rs index c86eac05d..dee4772dc 100644 --- a/src/shared/html_or_plaintext.rs +++ b/src/shared/html_or_plaintext.rs @@ -1,7 +1,7 @@ //! A `HtmlOrPlaintext` view can display either plaintext or rich HTML content. use makepad_widgets::*; -use matrix_sdk::{ruma::{matrix_uri::MatrixId, OwnedMxcUri}, OwnedServerName}; +use matrix_sdk::{ruma::{matrix_uri::{MatrixId, MatrixToUri, MatrixUri}, OwnedMxcUri}, OwnedServerName}; use crate::{avatar_cache::{self, AvatarCacheEntry}, profile::user_profile_cache, sliding_sync::{current_user_id, submit_async_request, MatrixRequest}, utils}; @@ -242,10 +242,9 @@ impl Widget for RobrixHtmlLink { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - // TODO: this is currently disabled because Makepad doesn't yet support - // partial vertical alignment of inline Html subwidgets with the surrounding text. - // Once makepad supports that, we can re-enable this to show the Pill widgets. - /* + // Try to render Matrix mention pills for matrix:// URIs. + // Note: vertical alignment with surrounding text may not be perfect + // (known Makepad limitation with inline Html subwidgets). if let Ok(matrix_to_uri) = MatrixToUri::parse(&self.url) { self.draw_matrix_pill(cx, matrix_to_uri.id(), matrix_to_uri.via()); } else if let Ok(matrix_uri) = MatrixUri::parse(&self.url) { @@ -253,8 +252,6 @@ impl Widget for RobrixHtmlLink { } else { self.draw_html_link(cx); } - */ - self.draw_html_link(cx); self.view.draw_walk(cx, scope, walk) } diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 31c422935..02f9fd9b9 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -1,139 +1,1965 @@ -//! A temporary mock/placeholder for MentionableTextInput that uses a simple TextInput -//! instead of the full @mention popup system (CommandTextInput). +//! MentionableTextInput component provides text input with @mention capabilities. //! -//! This preserves the same external-facing API so that the real MentionableTextInput -//! can be slotted back in later without changing the code that depends on it. +//! Can be used in any context where user mentions are needed (message input, editing). +//! +//! # Architecture Overview +//! +//! This component uses a **state machine** pattern combined with **background thread execution** +//! to provide responsive @mention search functionality even in large rooms. +//! +//! ## State Machine +//! +//! The search functionality is driven by [`MentionSearchState`], which has four states: +//! +//! ```text +//! ┌──────────────────────────────────────────────────────────────────────────┐ +//! │ State Transitions │ +//! │ │ +//! │ ┌──────┐ user types @ ┌───────────────────┐ │ +//! │ │ Idle │ ─────────────────► │ WaitingForMembers │ (if no cached data) │ +//! │ └──────┘ └─────────┬─────────┘ │ +//! │ ▲ │ │ +//! │ │ │ members loaded │ +//! │ │ ▼ │ +//! │ │ ┌─────────────────────────────────────┐ │ +//! │ │ │ Searching │ │ +//! │ │ │ - receiver: channel for results │ │ +//! │ │ │ - accumulated_results: Vec │ │ +//! │ │ │ - cancel_token: Arc │ │ +//! │ │ └──────────────┬──────────────────────┘ │ +//! │ │ │ │ +//! │ │ ┌───────────────────┼───────────────────┐ │ +//! │ │ │ │ │ │ +//! │ │ ▼ ▼ ▼ │ +//! │ │ search user selects user presses │ +//! │ │ completes a mention ESC │ +//! │ │ │ │ │ │ +//! │ │ │ │ ▼ │ +//! │ │ │ │ ┌───────────────┐ │ +//! │ │ │ │ │ JustCancelled │ │ +//! │ │ │ │ └───────┬───────┘ │ +//! │ │ │ │ │ │ +//! │ └────┴───────────────────┴───────────────────┘ │ +//! │ reset to Idle │ +//! └──────────────────────────────────────────────────────────────────────────┘ +//! ``` +//! +//! - **Idle**: Default state, no active search +//! - **WaitingForMembers**: Triggered @ detected, waiting for room member data to load +//! - **Searching**: Background search task running, receiving streaming results via channel +//! - **JustCancelled**: ESC pressed, prevents immediate re-trigger on next keystroke +//! +//! ## Background Thread Execution +//! +//! To keep the UI responsive during searches in large rooms, the actual member search +//! is offloaded to a background thread via [`cpu_worker::spawn_cpu_job`]: +//! +//! ```text +//! ┌─────────────────────┐ ┌─────────────────────┐ +//! │ UI Thread │ │ Background Thread │ +//! │ │ │ │ +//! │ update_user_list() │ │ │ +//! │ │ │ │ │ +//! │ ▼ │ │ │ +//! │ spawn_cpu_job() ───┼────────►│ SearchRoomMembers │ +//! │ │ │ │ │ │ +//! │ ▼ │ │ ▼ │ +//! │ cx.new_next_frame()│ │ search members... │ +//! │ │ │ │ │ │ +//! │ ▼ │ MPSC │ ▼ │ +//! │ check_search_ │◄────────┼─ send batch (10) │ +//! │ channel() │ Channel │ │ │ +//! │ │ │ │ ▼ │ +//! │ ▼ │ │ send batch (10) │ +//! │ update UI with │◄────────┼─ │ │ +//! │ streaming results │ │ ▼ │ +//! │ │ │ send completion │ +//! └─────────────────────┘ └─────────────────────┘ +//! ``` +//! +//! Key features: +//! - Results are streamed in batches of 10 for progressive UI updates +//! - Cancellation is supported via `Arc` token +//! - Each search has a unique `search_id` to ignore stale results +//! +//! ## Focus Management +//! +//! The component handles complex focus scenarios: +//! - `pending_popup_cleanup`: Defers popup closure when focus is lost during search +//! - `pending_draw_focus_restore`: Retries focus restoration in draw_walk until successful +//! +//! ## Key Components +//! +//! - [`SearchResult`]: Result type sent through the channel from background thread +//! - [`MentionSearchState`]: State machine enum managing search lifecycle +//! - [`MentionableTextInputAction`]: Actions for external communication (power levels, member updates) +//! +use crate::avatar_cache::*; +use crate::shared::avatar::AvatarWidgetRefExt; +use crate::shared::bouncing_dots::BouncingDotsWidgetRefExt; +use crate::shared::styles::COLOR_UNKNOWN_ROOM_AVATAR; +use crate::utils; +use crate::cpu_worker::{self, CpuJob, SearchRoomMembersJob}; +use crate::sliding_sync::{submit_async_request, MatrixRequest}; -use makepad_widgets::*; +use makepad_widgets::{makepad_draw::text::selection::Cursor, *}; use matrix_sdk::ruma::{ - events::room::message::RoomMessageEventContent, - OwnedRoomId, + events::{room::message::RoomMessageEventContent, Mentions}, + OwnedRoomId, OwnedUserId, }; +use matrix_sdk::RoomMemberships; +use std::collections::{BTreeMap, BTreeSet}; +use unicode_segmentation::UnicodeSegmentation; +use crate::home::room_screen::RoomScreenProps; +use crate::shared::command_text_input::CommandTextInput; +use crate::LivePtr; + +// Channel types for member search communication +use std::sync::{mpsc::Receiver, Arc}; +use std::sync::atomic::{AtomicBool, Ordering}; + +/// Result type for member search channel communication +#[derive(Debug, Clone)] +pub struct SearchResult { + pub search_id: u64, + pub results: Vec, // indices in members vec + pub is_complete: bool, + pub search_text: Arc, +} + +/// State machine for mention search functionality +#[derive(Debug, Default)] +enum MentionSearchState { + /// Not in search mode + #[default] + Idle, + + /// Waiting for room members data to be loaded + WaitingForMembers { + trigger_position: usize, + pending_search_text: String, + }, + + /// Actively searching with background task + Searching { + trigger_position: usize, + search_text: String, + receiver: Receiver, + accumulated_results: Vec, + search_id: u64, + cancel_token: Arc, + }, + + /// Search was just cancelled (prevents immediate re-trigger) + JustCancelled, +} + +// Default is derived above; Idle is marked as the default variant + +// Constants for mention popup height calculations +const DESKTOP_ITEM_HEIGHT: f64 = 32.0; +const MOBILE_ITEM_HEIGHT: f64 = 64.0; +const MOBILE_USERNAME_SPACING: f64 = 0.5; + +// Constants for search behavior +const DESKTOP_MAX_VISIBLE_ITEMS: usize = 10; +const MOBILE_MAX_VISIBLE_ITEMS: usize = 5; +const SEARCH_BUFFER_MULTIPLIER: usize = 2; script_mod! { use mod.prelude.widgets.* use mod.widgets.* - mod.widgets.MentionableTextInput = #(MentionableTextInput::register_widget(vm)) { + let FOCUS_HOVER_COLOR = #C + let KEYBOARD_FOCUS_OR_COLOR_HOVER = #x1C274C + + // Template for user list items in the mention dropdown + mod.widgets.UserListItem = View { width: Fill, - height: Fit + height: Fit, + margin: Inset{left: 4 right: 4} + padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + show_bg: true + cursor: Hand + draw_bg: { + color: (COLOR_PRIMARY), + uniform border_radius: 4.0, + instance hover: 0.0, + instance selected: 0.0, + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size); + // Draw rounded rectangle with configurable radius + sdf.box(0., 0., self.rect_size.x, self.rect_size.y, self.border_radius); + + if self.selected > 0.0 { + sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) + } else if self.hover > 0.0 { + sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) + } else { + // Default state + sdf.fill(self.color) + } + return sdf.result + } + } + flow: Down + spacing: 2.0 - // Keep the same nested structure so that external DSL overrides - // (e.g., `persistent.center.text_input.empty_text`) still work. - persistent := RoundedView { + user_info = View { width: Fill, height: Fit, - flow: Down, - top := View { height: 0 } - center := RoundedView { - width: Fill, + flow: Right, + spacing: 8.0 + align: Align{y: 0.5} + + avatar = Avatar { + width: 24, + height: 24, + text_view = { text = { draw_text: { + text_style: { font_size: 12.0 } + }}} + } + + username = Label { height: Fit, - text_input := RobrixTextInput { + draw_text: { + color: #000, + text_style: {font_size: 14.0} + } + } + + filler = FillerX {} + } + + user_id = Label { + height: Fit, + draw_text: { + color: #666, + text_style: {font_size: 12.0} + } + } + } + + // Template for the @room mention list item + mod.widgets.RoomMentionListItem = View { + width: Fill, + height: Fit, + margin: Inset{left: 4 right: 4} + padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + show_bg: true + cursor: Hand + draw_bg: { + color: (COLOR_PRIMARY), + uniform border_radius: 4.0, + instance hover: 0.0, + instance selected: 0.0, + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size); + sdf.box(0., 0., self.rect_size.x, self.rect_size.y, self.border_radius); + + if self.selected > 0.0 { + sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) + } else if self.hover > 0.0 { + sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) + } else { + sdf.fill(self.color) + } + return sdf.result + } + } + flow: Down + spacing: 2.0 + align: Align{y: 0.5} + + user_info = View { + width: Fill, + height: Fit, + flow: Right, + spacing: 8.0 + align: Align{y: 0.5} + + room_avatar = Avatar { + width: 24, + height: 24, + text_view = { text = { draw_text: { + text_style: { font_size: 12.0 } + }}} + } + + room_mention = Label { + height: Fit, + draw_text: { + color: #000, + text_style: {font_size: 14.0} + } + text: "Notify the entire room" + } + + filler = FillerX {} + } + + room_user_id = Label { + height: Fit, + align: Align{y: 0.5}, + draw_text: { + color: #666, + text_style: {font_size: 12.0} + } + text: "@room" + } + } + + // Template for loading indicator when members are being fetched + mod.widgets.LoadingIndicator = View { + width: Fill, + height: 48, + margin: Inset{left: 4 right: 4} + padding: Inset{left: 8 right: 8 top: 8 bottom: 8} + flow: Right, + spacing: 8.0, + align: Align{x: 0.0 y: 0.5} + draw_bg: { + color: (COLOR_PRIMARY), + } + + loading_text = Label { + height: Fit, + draw_text: { + color: #666, + text_style: {font_size: 14.0} + } + text: "Loading members" + } + + loading_animation = BouncingDots { + width: 60, + height: 24, + draw_bg: { + color: (COLOR_ROBRIX_PURPLE), + dot_radius: 2.0, + } + } + } + + // Template for no matches indicator when no users match the search + mod.widgets.NoMatchesIndicator = View { + width: Fill, + height: 48, + margin: Inset{left: 4 right: 4} + padding: Inset{left: 8 right: 8 top: 8 bottom: 8} + flow: Right, + spacing: 8.0, + align: Align{x: 0.0 y: 0.5} + draw_bg: { + color: (COLOR_PRIMARY), + } + + no_matches_text = Label { + height: Fit, + draw_text: { + color: #666, + text_style: {font_size: 14.0} + } + text: "No matching users found" + } + } + + mod.widgets.MentionableTextInput = #(MentionableTextInput::register_widget(vm)) { + ..mod.widgets.CommandTextInput + width: Fill, + height: Fit + trigger: "@" + inline_search: true + + color_focus: (FOCUS_HOVER_COLOR), + color_hover: (FOCUS_HOVER_COLOR), + + popup +: { + spacing: 0.0 + padding: 0.0 + + draw_bg: { + color: (COLOR_SECONDARY), + } + header_view +: { + margin: Inset{left: 4 right: 4} + draw_bg: { + color: (COLOR_ROBRIX_PURPLE), + } + header_label +: { + draw_text: { + color: (COLOR_PRIMARY_DARKER), + } + text: "Users in this Room" + } + } + + list +: { + height: Fit + clip_y: true + spacing: 0.0 + padding: 0.0 + } + } + + persistent +: { + top +: { height: 0 } + bottom +: { height: 0 } + center +: { + text_input = RobrixTextInput { empty_text: "Start typing..." } } - bottom := View { height: 0 } } + + // Template for user list items in the mention popup + user_list_item: mod.widgets.UserListItem {} + room_mention_list_item: mod.widgets.RoomMentionListItem {} + loading_indicator: mod.widgets.LoadingIndicator {} + no_matches_indicator: mod.widgets.NoMatchesIndicator {} } } +// /// A special string used to denote the start of a mention within +// /// the actual text being edited. +// /// This is used to help easily locate and distinguish actual mentions +// /// from normal `@` characters. +// const MENTION_START_STRING: &str = "\u{8288}@\u{8288}"; + #[derive(Debug)] pub enum MentionableTextInputAction { /// Notifies the MentionableTextInput about updated power levels for the room. PowerLevelsUpdated { room_id: OwnedRoomId, can_notify_room: bool, - } + }, + /// Notifies the MentionableTextInput that room members have been loaded. + RoomMembersLoaded { + room_id: OwnedRoomId, + /// Whether member sync is still in progress + sync_in_progress: bool, + /// Whether we currently have cached members + has_members: bool, + }, } -/// Temporary mock widget that wraps a simple TextInput (RobrixTextInput) -/// while preserving the same external API as the real MentionableTextInput. +/// Widget that extends CommandTextInput with @mention capabilities #[derive(Script, ScriptHook, Widget)] pub struct MentionableTextInput { - #[source] source: ScriptObjectRef, - #[deref] view: View, - /// Whether the current user can notify everyone in the room (@room mention). - /// Stored but not used in this mock; kept for API compatibility. - #[rust] can_notify_room: bool, + /// Base command text input + #[deref] + cmd_text_input: CommandTextInput, + /// Template for user list items + #[live] + user_list_item: Option, + /// Template for the @room mention list item + #[live] + room_mention_list_item: Option, + /// Template for loading indicator + #[live] + loading_indicator: Option, + /// Template for no matches indicator + #[live] + no_matches_indicator: Option, + /// The set of users that were mentioned (at one point) in this text input. + /// Due to characters being deleted/removed, this list is a *superset* + /// of possible users who may have been mentioned. + /// All of these mentions may not exist in the final text input content; + /// this is just a list of users to search the final sent message for + /// when adding in new mentions. + #[rust] + possible_mentions: BTreeMap, + /// Indicates if the `@room` option was explicitly selected. + #[rust] + possible_room_mention: bool, + /// Whether the current user can notify everyone in the room (@room mention) + #[rust] + can_notify_room: bool, + /// Current state of the mention search functionality + #[rust] + search_state: MentionSearchState, + /// Last search text to avoid duplicate searches + #[rust] + last_search_text: Option, + /// Next identifier for submitted search jobs + #[rust] + next_search_id: u64, + /// Whether the background search task has pending results + #[rust] + search_results_pending: bool, + /// Active loading indicator widget while we wait for members/results + #[rust] + loading_indicator_ref: Option, + /// Cached text analysis to avoid repeated grapheme parsing + /// Format: (text, graphemes_as_strings, byte_positions) + #[rust] + cached_text_analysis: Option<(String, Vec, Vec)>, + /// Last known member count - used ONLY for change detection (not rendering) + /// Rendering always uses props as source of truth + #[rust] + last_member_count: usize, + /// Last known sync pending state - used ONLY for change detection (not rendering) + #[rust] + last_sync_pending: bool, + /// Whether a deferred popup cleanup is pending after focus loss + #[rust] + pending_popup_cleanup: bool, + /// Whether focus should be restored in the next draw_walk cycle + #[rust] + pending_draw_focus_restore: bool, } impl Widget for MentionableTextInput { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - self.view.handle_event(cx, event, scope); + // Handle ESC key early before passing to child widgets + if self.is_searching() { + if let Event::KeyUp(key_event) = event { + if key_event.key_code == KeyCode::Escape { + self.cancel_active_search(); + self.search_state = MentionSearchState::JustCancelled; + + // UI cleanup only - do NOT call close_mention_popup() as it resets + // state to Idle via reset_search_state(), losing the JustCancelled marker + let popup = self.cmd_text_input.view(cx, ids!(popup)); + popup.set_visible(cx, false); + self.cmd_text_input.clear_items(cx); + self.loading_indicator_ref = None; // Clear loading indicator + self.pending_popup_cleanup = false; // Prevent next frame from triggering cleanup + + self.redraw(cx); + return; // Don't process other events + } + } + } + + self.cmd_text_input.handle_event(cx, event, scope); + + // Best practice: Always check Scope first to get current context + // Scope represents the current widget context as passed down from parents + let (scope_room_id, scope_member_count) = { + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope for MentionableTextInput"); + let member_count = room_props + .room_members + .as_ref() + .map(|members| members.len()) + .unwrap_or(0); + (room_props.room_name_id.room_id().clone(), member_count) + }; + + // Check search channel on every frame if we're searching + if let MentionSearchState::Searching { .. } = &self.search_state { + if let Event::NextFrame(_) = event { + // Only continue requesting frames if we're still waiting for results + if self.check_search_channel(cx, scope) { + cx.new_next_frame(); + } + } + } + // Handle deferred cleanup after focus loss + if let Event::NextFrame(_) = event { + if self.pending_popup_cleanup { + let text_input_ref = self.cmd_text_input.text_input_ref(); + let text_input_area = text_input_ref.area(); + self.pending_popup_cleanup = false; + + // Only close if input still doesn't have focus and we're not actively searching + let has_focus = cx.has_key_focus(text_input_area); + + // If user refocused or is actively typing/searching, don't cleanup + if !has_focus && !self.is_searching() { + self.close_mention_popup(cx); + } + } + } - // Handle MentionableTextInputAction for API compatibility. if let Event::Actions(actions) = event { + let text_input_ref = self.cmd_text_input.text_input_ref(); + let text_input_uid = text_input_ref.widget_uid(); + let text_input_area = text_input_ref.area(); + let has_focus = cx.has_key_focus(text_input_area); + + // Handle item selection from mention popup + if let Some(selected) = self.cmd_text_input.item_selected(actions) { + self.on_user_selected(cx, scope, selected); + } + + // Handle build items request + if self.cmd_text_input.should_build_items(actions) { + if has_focus { + let search_text = self.cmd_text_input.search_text().to_lowercase(); + self.update_user_list(cx, &search_text, scope); + } else if self.cmd_text_input.view(cx, ids!(popup)).visible() { + self.close_mention_popup(cx); + } + } + + // Process all actions for action in actions { - if let Some(MentionableTextInputAction::PowerLevelsUpdated { - can_notify_room, .. - }) = action.downcast_ref() - { - self.can_notify_room = *can_notify_room; + // Handle TextInput changes + if let Some(widget_action) = action.as_widget_action() { + if widget_action.widget_uid == text_input_uid { + if let TextInputAction::Changed(text) = widget_action.cast() { + if has_focus { + self.handle_text_change(cx, scope, text.to_owned()); + } + continue; // Continue processing other actions + } + } + } + + // Handle MentionableTextInputAction actions + if let Some(action) = action.downcast_ref::() { + match action { + MentionableTextInputAction::PowerLevelsUpdated { + room_id, + can_notify_room, + } => { + if &scope_room_id != room_id { + continue; + } + log!("PowerLevelsUpdated received: room_id={}, can_notify_room={}, scope_room_id={}", + room_id, can_notify_room, scope_room_id); + + if self.can_notify_room != *can_notify_room { + log!("Updating can_notify_room from {} to {}", self.can_notify_room, can_notify_room); + self.can_notify_room = *can_notify_room; + if self.is_searching() && has_focus { + let search_text = + self.cmd_text_input.search_text().to_lowercase(); + self.update_user_list(cx, &search_text, scope); + } else { + self.cmd_text_input.redraw(cx); + } + } + } + MentionableTextInputAction::RoomMembersLoaded { + room_id, + sync_in_progress, + has_members, + } => { + if &scope_room_id != room_id { + continue; + } + + // CRITICAL: Use locally stored previous state for change detection + // (not from props, which is already the new state in the same frame) + let previous_member_count = self.last_member_count; + let was_sync_pending = self.last_sync_pending; + + // Current state: read fresh props to avoid stale snapshot from handle_event entry + let current_member_count = scope + .props + .get::() + .map(|p| p.room_members.as_ref().map(|m| m.len()).unwrap_or(0)) + .unwrap_or(scope_member_count); + let current_sync_pending = *sync_in_progress; + + // Detect actual changes + let member_count_changed = current_member_count != previous_member_count + && current_member_count > 0 + && previous_member_count > 0; + let sync_just_completed = !current_sync_pending && was_sync_pending; + + // Update local state for next comparison + self.last_member_count = current_member_count; + self.last_sync_pending = current_sync_pending; + + // Skip processing if search was cancelled by ESC + // This prevents async callbacks from reopening the popup + if matches!(self.search_state, MentionSearchState::JustCancelled) { + continue; + } + + if *has_members { + // CRITICAL FIX: Use saved state instead of reading from text input + // Reading from text input causes race condition (text may be empty when members arrive) + // Extract needed values first to avoid borrow checker issues + let action = match &self.search_state { + MentionSearchState::WaitingForMembers { + pending_search_text, + .. + } => Some((true, pending_search_text.clone())), + MentionSearchState::Searching { search_text, .. } => { + Some((false, search_text.clone())) + } + _ => None, + }; + + if let Some((is_waiting, search_text)) = action { + let member_set_updated = member_count_changed + && matches!(self.search_state, MentionSearchState::Searching { .. }); + + if is_waiting { + self.last_search_text = None; + self.update_user_list(cx, &search_text, scope); + } else { + // Already in Searching state + // Check if remote sync just completed or member set changed - need to re-search with full member list + if member_set_updated || sync_just_completed { + self.last_search_text = None; + self.update_user_list(cx, &search_text, scope); + } else { + self.update_ui_with_results(cx, scope, &search_text); + } + } + } else { + // Not in WaitingForMembers or Searching state + // Check if remote sync just completed - if so, refresh UI if there's an active mention trigger + if sync_just_completed { + let text = self.cmd_text_input.text_input_ref().text(); + let cursor_pos = self.cmd_text_input.text_input_ref() + .borrow() + .map_or(0, |p| p.cursor().index); + + if let Some(_trigger_pos) = self.find_mention_trigger_position(&text, cursor_pos) { + let search_text = self.cmd_text_input.search_text().to_lowercase(); + self.last_search_text = None; + self.update_user_list(cx, &search_text, scope); + } + } + } + } else if self.is_searching() { + // Still no members returned yet; keep showing loading indicator. + self.cmd_text_input.clear_items(cx); + self.show_loading_indicator(cx); + let popup = self.cmd_text_input.view(cx, ids!(popup)); + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } + } + } + } + } + + // Close popup and clean up search state if focus is lost while searching + // This prevents background search tasks from continuing when user is no longer interested + if !has_focus && self.is_searching() { + let popup = self.cmd_text_input.view(cx, ids!(popup)); + popup.set_visible(cx, false); + self.pending_popup_cleanup = true; + // Guarantee cleanup executes even if search completes and stops requesting frames + cx.new_next_frame(); + } + } + + // Check if we were waiting for members and they're now available + // When members arrive, always update regardless of focus state + // update_user_list will handle popup visibility based on current focus + if let MentionSearchState::WaitingForMembers { + trigger_position: _, + pending_search_text, + } = &self.search_state + { + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope"); + + if let Some(room_members) = &room_props.room_members { + if !room_members.is_empty() { + let search_text = pending_search_text.clone(); + self.update_user_list(cx, &search_text, scope); } } } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - self.view.draw_walk(cx, scope, walk) + let result = self.cmd_text_input.draw_walk(cx, scope, walk); + + // Restore focus after all child drawing is complete. + // This retries until focus is successfully restored, handling cases where + // finger_up events might steal focus after our initial restoration attempt. + if self.pending_draw_focus_restore { + let text_input_ref = self.cmd_text_input.text_input_ref(); + text_input_ref.set_key_focus(cx); + if let Some(mut ti) = text_input_ref.borrow_mut() { + ti.reset_blink_timer(cx); + } + // Check if we successfully got focus + let area = text_input_ref.area(); + if cx.has_key_focus(area) { + // Successfully restored focus, clear the flag + self.pending_draw_focus_restore = false; + } else { + // Focus restoration failed (likely due to finger_up event stealing focus) + // Keep the flag true and request another frame to retry + cx.new_next_frame(); + } + } + + result + } +} + +impl MentionableTextInput { + /// Check if currently in any form of search mode + fn is_searching(&self) -> bool { + matches!( + self.search_state, + MentionSearchState::WaitingForMembers { .. } | MentionSearchState::Searching { .. } + ) } - fn text(&self) -> String { - self.child_by_path(ids!(text_input)).as_text_input().text() + /// Generate the next unique identifier for a background search job. + fn allocate_search_id(&mut self) -> u64 { + if self.next_search_id == 0 { + self.next_search_id = 1; + } + let id = self.next_search_id; + self.next_search_id = self.next_search_id.wrapping_add(1); + if self.next_search_id == 0 { + self.next_search_id = 1; + } + id } - fn set_text(&mut self, cx: &mut Cx, text: &str) { - self.text_input(cx, ids!(persistent.center.text_input)).set_text(cx, text); - self.redraw(cx); + /// Get the current trigger position if in search mode + fn get_trigger_position(&self) -> Option { + match &self.search_state { + MentionSearchState::WaitingForMembers { + trigger_position, .. + } + | MentionSearchState::Searching { + trigger_position, .. + } => Some(*trigger_position), + _ => None, + } } - fn set_key_focus(&self, cx: &mut Cx) { - self.text_input(cx, ids!(persistent.center.text_input)).set_key_focus(cx); + /// Check if search was just cancelled + fn is_just_cancelled(&self) -> bool { + matches!(self.search_state, MentionSearchState::JustCancelled) } -} -impl MentionableTextInput { + /// Tries to add the `@room` mention item to the list of selectable popup mentions. + /// + /// Returns true if @room item was added to the list and will be displayed in the popup. + fn try_add_room_mention_item( + &mut self, + cx: &mut Cx, + search_text: &str, + room_props: &RoomScreenProps, + is_desktop: bool, + ) -> bool { + // Don't show @room option in direct messages + if false /* TODO: add is_direct_room to RoomScreenProps */ { + return false; + } + if !self.can_notify_room || !("@room".contains(search_text) || search_text.is_empty()) { + return false; + } + + let Some(ptr) = self.room_mention_list_item else { + return false; + }; + let mut room_mention_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); + let mut room_avatar_shown = false; + + let avatar_ref = room_mention_item.avatar(cx, ids!(user_info.room_avatar)); + + // Get room avatar fallback text from room name (with automatic ID fallback) + let room_label = room_props.room_name_id.to_string(); + let room_name_first_char = room_label + .graphemes(true) + .next() + .map(|s| s.to_uppercase()) + .filter(|s| s != "@" && s.chars().all(|c| c.is_alphabetic())) + .unwrap_or_default(); + + if let Some(avatar_url) = &room_props.room_avatar_url { + match get_or_fetch_avatar(cx, avatar_url) { + AvatarCacheEntry::Loaded(avatar_data) => { + // Display room avatar + let result = avatar_ref.show_image(cx, None, |cx, img| { + utils::load_png_or_jpg(&img, cx, &avatar_data) + }); + if result.is_ok() { + room_avatar_shown = true; + } + } + AvatarCacheEntry::Requested => { + avatar_ref.show_text( + cx, + Some(COLOR_UNKNOWN_ROOM_AVATAR), + None, + &room_name_first_char, + ); + room_avatar_shown = true; + } + AvatarCacheEntry::Failed => { + // Failed to load room avatar - will use fallback text + } + } + } + + // If unable to display room avatar, show first character of room name + if !room_avatar_shown { + avatar_ref.show_text( + cx, + Some(COLOR_UNKNOWN_ROOM_AVATAR), + None, + &room_name_first_char, + ); + } + + // Apply layout and height styling based on device type + let new_height = if is_desktop { + DESKTOP_ITEM_HEIGHT + } else { + MOBILE_ITEM_HEIGHT + }; + if is_desktop { + script_apply_eval!(cx, room_mention_item, { + height: #(new_height) + flow: Right + }); + } else { + script_apply_eval!(cx, room_mention_item, { + height: #(new_height) + flow: Down + }); + } + + self.cmd_text_input.add_item(cx, room_mention_item); + true + } + + /// Add user mention items to the list from search results + /// Returns the number of items added + fn add_user_mention_items_from_results( + &mut self, + cx: &mut Cx, + results: &[usize], + user_items_limit: usize, + is_desktop: bool, + room_props: &RoomScreenProps, + ) -> usize { + let mut items_added = 0; + + // Get the actual members vec from room_props + let Some(members) = &room_props.room_members else { + return 0; + }; + + for (index, &member_idx) in results.iter().take(user_items_limit).enumerate() { + // Get the actual member from the index + let Some(member) = members.get(member_idx) else { + continue; + }; + + // Get display name from member, with better fallback + // Trim whitespace and filter out empty/whitespace-only names + let display_name = member.display_name() + .map(|name| name.trim()) // Remove leading/trailing whitespace + .filter(|name| !name.is_empty()) // Filter out empty or whitespace-only names + .unwrap_or_else(|| member.user_id().localpart()) + .to_owned(); + + // Log warning for extreme cases where we still have no displayable text + #[cfg(debug_assertions)] + if display_name.is_empty() { + log!( + "Warning: Member {} has no displayable name (empty display_name and localpart)", + member.user_id() + ); + } + + let Some(user_list_item_ptr) = self.user_list_item else { + // user_list_item_ptr is None + continue; + }; + let mut item = crate::widget_ref_from_live_ptr(cx, Some(user_list_item_ptr)); + + item.label(cx, ids!(user_info.username)).set_text(cx, &display_name); + + // Use the full user ID string + let user_id_str = member.user_id().as_str(); + item.label(cx, ids!(user_id)).set_text(cx, user_id_str); + + if is_desktop { + script_apply_eval!(cx, item, { + flow: Right + height: #(DESKTOP_ITEM_HEIGHT) + align: Align{y: 0.5} + }); + item.view(cx, ids!(user_info.filler)).set_visible(cx, true); + } else { + script_apply_eval!(cx, item, { + flow: Down + height: #(MOBILE_ITEM_HEIGHT) + spacing: #(MOBILE_USERNAME_SPACING) + }); + item.view(cx, ids!(user_info.filler)).set_visible(cx, false); + } + + let avatar = item.avatar(cx, ids!(user_info.avatar)); + if let Some(mxc_uri) = member.avatar_url() { + match get_or_fetch_avatar(cx, &mxc_uri.to_owned()) { + AvatarCacheEntry::Loaded(avatar_data) => { + let _ = avatar.show_image(cx, None, |cx, img| { + utils::load_png_or_jpg(&img, cx, &avatar_data) + }); + } + AvatarCacheEntry::Requested | AvatarCacheEntry::Failed => { + avatar.show_text(cx, None, None, &display_name); + } + } + } else { + avatar.show_text(cx, None, None, &display_name); + } + + self.cmd_text_input.add_item(cx, item.clone()); + items_added += 1; + + // Set keyboard focus to the first item + if index == 0 { + // If @room exists, it's index 0, otherwise first user is index 0 + self.cmd_text_input.set_keyboard_focus_index(0); + } + } + + items_added + } + + /// Update popup visibility and layout based on current state + fn update_popup_visibility(&mut self, cx: &mut Cx, scope: &mut Scope, has_items: bool) { + let mut popup = self.cmd_text_input.view(cx, ids!(popup)); + + // Get current state from props + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope"); + let members_sync_pending = false; // TODO: add room_members_sync_pending to RoomScreenProps + let members_available = room_props + .room_members + .as_ref() + .is_some_and(|m| !m.is_empty()); + + match &self.search_state { + MentionSearchState::Idle | MentionSearchState::JustCancelled => { + // Not in search mode, hide popup + script_apply_eval!(cx, popup, { height: Fit }); + popup.set_visible(cx, false); + } + MentionSearchState::WaitingForMembers { .. } => { + // Waiting for room members to be loaded + self.show_loading_indicator(cx); + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } + MentionSearchState::Searching { + accumulated_results, + .. + } => { + if has_items { + // We have search results to display + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } else if accumulated_results.is_empty() { + if members_sync_pending || self.search_results_pending { + // Still fetching either member list or background search results. + self.show_loading_indicator(cx); + } else if members_available { + // Search completed with no results even though we have members. + self.show_no_matches_indicator(cx); + } else { + // No members available yet. + self.show_loading_indicator(cx); + } + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } else { + // Has accumulated results but no items (should not happen) + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } + } + } + } + + /// Handles item selection from mention popup (either user or @room) + fn on_user_selected(&mut self, cx: &mut Cx, _scope: &mut Scope, selected: WidgetRef) { + // Note: We receive scope as parameter but don't use it in this method + // This is good practice to maintain signature consistency with other methods + // and allow for future scope-based enhancements + + let text_input_ref = self.cmd_text_input.text_input_ref(); + let current_text = text_input_ref.text(); + let head = text_input_ref.borrow().map_or(0, |p| p.cursor().index); + + if let Some(start_idx) = self.get_trigger_position() { + let room_mention_label = selected.label(cx, ids!(user_info.room_mention)); + let room_mention_text = room_mention_label.text(); + let room_user_id_text = selected.label(cx, ids!(room_user_id)).text(); + + let is_room_mention = + { room_mention_text == "Notify the entire room" && room_user_id_text == "@room" }; + + let mention_to_insert = if is_room_mention { + // Always set to true, don't reset previously selected @room mentions + self.possible_room_mention = true; + "@room ".to_string() + } else { + // User selected a specific user + let username = selected.label(cx, ids!(user_info.username)).text(); + let user_id_str = selected.label(cx, ids!(user_id)).text(); + let Ok(user_id): Result = user_id_str.clone().try_into() else { + // Invalid user ID format - skip selection + return; + }; + self.possible_mentions + .insert(user_id.clone(), username.clone()); + + // Currently, we directly insert the markdown link for user mentions + // instead of the user's display name, because we don't yet have a way + // to track mentioned display names and replace them later. + format!("[{username}]({}) ", user_id.matrix_to_uri(),) + }; + + // Use utility function to safely replace text + let new_text = utils::safe_replace_by_byte_indices( + ¤t_text, + start_idx, + head, + &mention_to_insert, + ); + + self.cmd_text_input.set_text(cx, &new_text); + // Calculate new cursor position + let new_pos = start_idx + mention_to_insert.len(); + text_input_ref.set_cursor( + cx, + Cursor { + index: new_pos, + prefer_next_row: false, + }, + false, + ); + } + + self.cancel_active_search(); + self.search_state = MentionSearchState::JustCancelled; + // Clear cleanup flag to prevent next-frame cleanup from interfering with focus + self.pending_popup_cleanup = false; + self.close_mention_popup(cx); + // Schedule focus restoration for the draw cycle. + // This will retry until focus is successfully restored, handling cases where + // finger_up events might steal focus after our initial restoration attempt. + self.pending_draw_focus_restore = true; + } + + /// Core text change handler that manages mention context + fn handle_text_change(&mut self, cx: &mut Cx, scope: &mut Scope, text: String) { + // If search was just cancelled, clear the flag and don't re-trigger search + if self.is_just_cancelled() { + self.search_state = MentionSearchState::Idle; + return; + } + + // Check if text is empty or contains only whitespace + let trimmed_text = text.trim(); + if trimmed_text.is_empty() { + self.possible_mentions.clear(); + self.possible_room_mention = false; + if self.is_searching() { + self.close_mention_popup(cx); + } + return; + } + + let cursor_pos = self + .cmd_text_input + .text_input_ref() + .borrow() + .map_or(0, |p| p.cursor().index); + + // Check if we're currently searching and the @ symbol was deleted + if let Some(start_pos) = self.get_trigger_position() { + // Check if the @ symbol at the start position still exists + if start_pos >= text.len() + || text.get(start_pos..start_pos + 1).is_some_and(|c| c != "@") + { + // The @ symbol was deleted, stop searching + self.close_mention_popup(cx); + return; + } + } + + // Look for trigger position for @ menu + if let Some(trigger_pos) = self.find_mention_trigger_position(&text, cursor_pos) { + let search_text = + utils::safe_substring_by_byte_indices(&text, trigger_pos + 1, cursor_pos); + + // Check if this is a continuation of existing search or a new one + let is_new_search = self.get_trigger_position() != Some(trigger_pos); + + if is_new_search { + // This is a new @ mention, reset everything + self.last_search_text = None; + } else { + // User is editing existing mention, don't reset search state + // This allows smooth deletion/modification of search text + // But clear last_search_text if the new text is different to trigger search + if self.last_search_text.as_ref() != Some(&search_text) { + self.last_search_text = None; + } + } + + // Ensure header view is visible to prevent header disappearing during consecutive @mentions + let popup = self.cmd_text_input.view(cx, ids!(popup)); + let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); + header_view.set_visible(cx, true); + + // Transition to appropriate state and update user list + // update_user_list will handle state transition properly + self.update_user_list(cx, &search_text, scope); + + popup.set_visible(cx, true); + + // Immediately check for results instead of waiting for next frame + self.check_search_channel(cx, scope); + + // Redraw to ensure UI updates are visible + cx.redraw_all(); + } else if self.is_searching() { + self.close_mention_popup(cx); + } + } + + /// Check the search channel for new results + /// Returns true if we should continue checking for more results + fn check_search_channel(&mut self, cx: &mut Cx, scope: &mut Scope) -> bool { + // Only check if we're in Searching state + let mut is_complete = false; + let mut search_text: Option> = None; + let mut any_results = false; + let mut should_update_ui = false; + let mut new_results = Vec::new(); + + // Process all available results from the channel + if let MentionSearchState::Searching { + receiver, + accumulated_results, + search_id, + .. + } = &mut self.search_state + { + while let Ok(result) = receiver.try_recv() { + if result.search_id != *search_id { + continue; + } + + any_results = true; + search_text = Some(result.search_text.clone()); + is_complete = result.is_complete; + + // Collect results + if !result.results.is_empty() { + new_results.extend(result.results); + should_update_ui = true; + } + } + + if !new_results.is_empty() { + accumulated_results.extend(new_results); + } + } else { + return false; + } + + // Update UI immediately if we got new results + if should_update_ui { + if matches!( + &self.search_state, + MentionSearchState::Searching { accumulated_results, .. } + if !accumulated_results.is_empty() + ) { + // Results are already sorted in member_search.rs and indices are unique + let query = search_text + .as_ref() + .map(|s| s.as_str()) + .unwrap_or_default(); + self.update_ui_with_results(cx, scope, query); + } + } + + // Handle completion + if is_complete { + self.search_results_pending = false; + // Search is complete - get results for final UI update + if matches!( + &self.search_state, + MentionSearchState::Searching { accumulated_results, .. } + if accumulated_results.is_empty() + ) { + // No user results, but still update UI (may show @room) + let query = search_text + .as_ref() + .map(|s| s.as_str()) + .unwrap_or_default(); + self.update_ui_with_results(cx, scope, query); + } + + // Don't change state here - let update_ui_with_results handle it + } else if !any_results { + // No results received yet - check if channel is still open + let disconnected = + if let MentionSearchState::Searching { receiver, .. } = &self.search_state { + matches!( + receiver.try_recv(), + Err(std::sync::mpsc::TryRecvError::Disconnected) + ) + } else { + false + }; - /// Sets whether the current user can notify the entire room (@room mention). + if disconnected { + // Channel was closed - search completed or failed + self.search_results_pending = false; + self.handle_search_channel_closed(cx, scope); + // Stop checking - channel is closed, no more results will arrive + return false; + } + } + + // Return whether we should continue checking for results + !is_complete && matches!(self.search_state, MentionSearchState::Searching { .. }) + } + + /// Common UI update logic for both streaming and non-streaming results + fn update_ui_with_results(&mut self, cx: &mut Cx, scope: &mut Scope, search_text: &str) { + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope for MentionableTextInput"); + + // If we're in Searching state, we have local data - always show results + // Don't wait for remote sync to complete + // Remote sync will trigger update when it completes (if data changed) + self.cmd_text_input.clear_items(cx); + self.loading_indicator_ref = None; + + let is_desktop = cx.display_context.is_desktop(); + let max_visible_items: usize = if is_desktop { + DESKTOP_MAX_VISIBLE_ITEMS + } else { + MOBILE_MAX_VISIBLE_ITEMS + }; + let mut items_added = 0; + + // 4. Try to add @room mention item + let has_room_item = self.try_add_room_mention_item(cx, search_text, room_props, is_desktop); + if has_room_item { + items_added += 1; + } + + // Get accumulated results from current state + let results_to_display = if let MentionSearchState::Searching { + accumulated_results, + .. + } = &self.search_state + { + accumulated_results.clone() + } else { + Vec::new() + }; + + // Add user mention items using the results + if !results_to_display.is_empty() { + let user_items_limit = max_visible_items.saturating_sub(has_room_item as usize); + let user_items_added = self.add_user_mention_items_from_results( + cx, + &results_to_display, + user_items_limit, + is_desktop, + room_props, + ); + items_added += user_items_added; + } + + // If remote sync is still in progress, add loading indicator after results + // This gives visual feedback that more members may be loading + // IMPORTANT: Don't call show_loading_indicator here as it calls clear_items() + // which would remove the user list we just added + if false /* TODO: add room_members_sync_pending to RoomScreenProps */ { + // Add loading indicator widget without clearing existing items + if let Some(ptr) = self.loading_indicator { + let loading_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); + self.cmd_text_input.add_item(cx, loading_item.clone()); + self.loading_indicator_ref = Some(loading_item.clone()); + + // Start the loading animation + loading_item + .bouncing_dots(cx, ids!(loading_animation)) + .start_animation(cx); + cx.new_next_frame(); + + items_added += 1; + } + } + + // Update popup visibility based on whether we have items + self.update_popup_visibility(cx, scope, items_added > 0); + + // Force immediate redraw to ensure UI updates are visible + cx.redraw_all(); + } + + /// Updates the mention suggestion list based on search + fn update_user_list(&mut self, cx: &mut Cx, search_text: &str, scope: &mut Scope) { + // Get room_props to read real-time member state from props (single source of truth) + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope for MentionableTextInput"); + + // Get trigger position from current state (if in searching mode) + let trigger_pos = match &self.search_state { + MentionSearchState::WaitingForMembers { + trigger_position, .. + } + | MentionSearchState::Searching { + trigger_position, .. + } => *trigger_position, + _ => { + // Not in searching mode, need to determine trigger position + if let Some(pos) = self.find_mention_trigger_position( + &self.cmd_text_input.text_input_ref().text(), + self.cmd_text_input + .text_input_ref() + .borrow() + .map_or(0, |p| p.cursor().index), + ) { + pos + } else { + return; + } + } + }; + + // Skip if search text hasn't changed AND we're already in Searching state + // Don't skip if we're in WaitingForMembers - need to transition to Searching + if self.last_search_text.as_deref() == Some(search_text) { + if matches!(self.search_state, MentionSearchState::Searching { .. }) { + return; // Already searching with same text, skip + } + // In WaitingForMembers with same text -> need to start search now that members arrived + } + + self.last_search_text = Some(search_text.to_string()); + + let is_desktop = cx.display_context.is_desktop(); + let max_visible_items = if is_desktop { + DESKTOP_MAX_VISIBLE_ITEMS + } else { + MOBILE_MAX_VISIBLE_ITEMS + }; + + let cached_members = match &room_props.room_members { + Some(members) if !members.is_empty() => { + // Members available, continue to search + members.clone() + } + _ => { + let already_waiting = matches!( + self.search_state, + MentionSearchState::WaitingForMembers { .. } + ); + + self.cancel_active_search(); + + if !already_waiting { + submit_async_request(MatrixRequest::GetRoomMembers { + timeline_kind: crate::sliding_sync::TimelineKind::MainRoom { room_id: room_props.room_name_id.room_id().clone() }, + memberships: RoomMemberships::JOIN, + local_only: true, + }); + } + + self.search_state = MentionSearchState::WaitingForMembers { + trigger_position: trigger_pos, + pending_search_text: search_text.to_string(), + }; + + // Clear old items before showing loading indicator + self.cmd_text_input.clear_items(cx); + self.show_loading_indicator(cx); + // Request next frame to check when members are loaded + cx.new_next_frame(); + return; // Don't submit search request yet + } + }; + + // We have cached members, ensure popup is visible + let popup = self.cmd_text_input.view(cx, ids!(popup)); + let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); + header_view.set_visible(cx, true); + popup.set_visible(cx, true); + // Only restore focus if input currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + + // Create a new channel for this search + let (sender, receiver) = std::sync::mpsc::channel(); + + // Prepare background search job parameters + let search_text_clone = search_text.to_string(); + let max_results = max_visible_items * SEARCH_BUFFER_MULTIPLIER; + let search_id = self.allocate_search_id(); + + // Transition to Searching state with new receiver + self.cancel_active_search(); + let cancel_token = Arc::new(AtomicBool::new(false)); + self.search_state = MentionSearchState::Searching { + trigger_position: trigger_pos, + search_text: search_text.to_string(), + receiver, + accumulated_results: Vec::new(), + search_id, + cancel_token: cancel_token.clone(), + }; + self.search_results_pending = true; + + let precomputed_sort = None /* TODO: add room_members_sort to RoomScreenProps */; + let cancel_token_for_job = cancel_token.clone(); + cpu_worker::spawn_cpu_job(cx, CpuJob::SearchRoomMembers(SearchRoomMembersJob { + members: cached_members, + search_text: search_text_clone, + max_results, + sender, + search_id, + precomputed_sort, + cancel_token: Some(cancel_token_for_job), + })); + + // Request next frame to check the channel + cx.new_next_frame(); + + // Try to check immediately for faster response + self.check_search_channel(cx, scope); + } + + /// Detects valid mention trigger positions in text + fn find_mention_trigger_position(&mut self, text: &str, cursor_pos: usize) -> Option { + if cursor_pos == 0 { + return None; + } + + // Check cache and rebuild if text changed (performance optimization) + let (text_graphemes_owned, byte_positions) = if let Some((cached_text, cached_graphemes, cached_positions)) = &self.cached_text_analysis { + if cached_text == text { + // Cache hit - use cached data + (cached_graphemes.clone(), cached_positions.clone()) + } else { + // Cache miss - rebuild and update cache + let graphemes_owned: Vec = text.graphemes(true).map(|s| s.to_string()).collect(); + let positions = utils::build_grapheme_byte_positions(text); + self.cached_text_analysis = Some((text.to_string(), graphemes_owned.clone(), positions.clone())); + (graphemes_owned, positions) + } + } else { + // No cache - build and cache + let graphemes_owned: Vec = text.graphemes(true).map(|s| s.to_string()).collect(); + let positions = utils::build_grapheme_byte_positions(text); + self.cached_text_analysis = Some((text.to_string(), graphemes_owned.clone(), positions.clone())); + (graphemes_owned, positions) + }; + + // Convert owned strings to slices for processing + let text_graphemes: Vec<&str> = text_graphemes_owned.iter().map(|s| s.as_str()).collect(); + + // Use utility function to convert byte position to grapheme index + let cursor_grapheme_idx = utils::byte_index_to_grapheme_index(text, cursor_pos); + + // Simple logic: trigger when cursor is immediately after @ symbol + // Only trigger if @ is preceded by whitespace or beginning of text + if cursor_grapheme_idx > 0 && text_graphemes.get(cursor_grapheme_idx - 1) == Some(&"@") { + let is_preceded_by_whitespace_or_start = cursor_grapheme_idx == 1 + || (cursor_grapheme_idx > 1 + && text_graphemes + .get(cursor_grapheme_idx - 2) + .is_some_and(|g| g.trim().is_empty())); + if is_preceded_by_whitespace_or_start { + if let Some(&byte_pos) = byte_positions.get(cursor_grapheme_idx - 1) { + return Some(byte_pos); + } + } + } + + // Find the last @ symbol before the cursor for search continuation + // Only continue if we're already in search mode + if self.is_searching() { + let last_at_pos = text_graphemes.get(..cursor_grapheme_idx).and_then(|slice| { + slice + .iter() + .enumerate() + .filter(|(_, g)| **g == "@") + .map(|(i, _)| i) + .next_back() + }); + + if let Some(at_idx) = last_at_pos { + // Get the byte position of this @ symbol + let &at_byte_pos = byte_positions.get(at_idx)?; + + // Extract the text after the @ symbol up to the cursor position + let mention_text = text_graphemes + .get(at_idx + 1..cursor_grapheme_idx) + .unwrap_or(&[]); + + // Only trigger if this looks like an ongoing mention (contains only alphanumeric and basic chars) + if self.is_valid_mention_text(mention_text) { + return Some(at_byte_pos); + } + } + } + + None + } + + /// Simple validation for mention text + fn is_valid_mention_text(&self, graphemes: &[&str]) -> bool { + // Allow empty text (for @) + if graphemes.is_empty() { + return true; + } + + // Check if it contains newline characters + !graphemes.iter().any(|g| g.contains('\n')) + } + + /// Shows the loading indicator when waiting for initial members to be loaded + fn show_loading_indicator(&mut self, cx: &mut Cx) { + // Check if we already have a loading indicator displayed + // Avoid recreating it on every call, which would prevent animation from playing + if let Some(ref existing_indicator) = self.loading_indicator_ref { + // Already showing, just ensure animation is running + existing_indicator + .bouncing_dots(cx, ids!(loading_animation)) + .start_animation(cx); + cx.new_next_frame(); + return; + } + + // Clear old items before creating new loading indicator + self.cmd_text_input.clear_items(cx); + + // Create fresh loading indicator widget + let Some(ptr) = self.loading_indicator else { + return; + }; + let loading_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); + + // Start the loading animation + loading_item.bouncing_dots(cx, ids!(loading_animation)).start_animation(cx); + + // Now that the widget is in the UI tree, start the loading animation + loading_item + .bouncing_dots(cx, ids!(loading_animation)) + .start_animation(cx); + cx.new_next_frame(); + + // Setup popup dimensions for loading state + let popup = self.cmd_text_input.view(cx, ids!(popup)); + let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); + + // Ensure header is visible + header_view.set_visible(cx, true); + + // Don't manually set popup height for loading - let it auto-size based on content + // This avoids conflicts with list +: { height: Fill } + popup.set_visible(cx, true); + + // Maintain text input focus only if it currently has focus + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if self.is_searching() && cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } + + /// Shows the no matches indicator when no users match the search + fn show_no_matches_indicator(&mut self, cx: &mut Cx) { + // Clear any existing items + self.cmd_text_input.clear_items(cx); + + // Create no matches indicator widget + let Some(ptr) = self.no_matches_indicator else { + return; + }; + let no_matches_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); + + // Add the no matches indicator to the popup + self.cmd_text_input.add_item(cx, no_matches_item); + self.loading_indicator_ref = None; + + // Setup popup dimensions for no matches state + let mut popup = self.cmd_text_input.view(cx, ids!(popup)); + let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); + + // Ensure header is visible + header_view.set_visible(cx, true); + + // Let popup auto-size based on content + script_apply_eval!(cx, popup, { height: Fit }); + + // Maintain text input focus so user can continue typing, but only if currently focused + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if self.is_searching() && cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + } + + /// Check if mention search is currently active + pub fn is_mention_searching(&self) -> bool { + self.is_searching() + } + + /// Check if ESC was handled by mention popup + pub fn handled_escape(&self) -> bool { + self.is_just_cancelled() + } + + /// Handle search channel closed event + fn handle_search_channel_closed(&mut self, cx: &mut Cx, scope: &mut Scope) { + // Get accumulated results before changing state + let has_results = if let MentionSearchState::Searching { + accumulated_results, + .. + } = &self.search_state + { + !accumulated_results.is_empty() + } else { + false + }; + + // If no results were shown, show empty state + if !has_results { + self.update_ui_with_results(cx, scope, ""); + } + + // Keep searching state but mark search as complete + // The state will be reset when user types or closes popup + } + + fn cancel_active_search(&mut self) { + match &self.search_state { + MentionSearchState::Searching { cancel_token, .. } => { + cancel_token.store(true, Ordering::Relaxed); + } + MentionSearchState::WaitingForMembers { .. } => { + // WaitingForMembers has no cancel_token, but we need to mark as cancelled. + // The state will be set to JustCancelled by the caller, which prevents + // RoomMembersLoaded from reopening the popup. + } + _ => {} + } + self.search_results_pending = false; + } + + /// Reset all search-related state + fn reset_search_state(&mut self, cx: &Cx) { + self.cancel_active_search(); + + // Reset to idle state + self.search_state = MentionSearchState::Idle; + + // Reset last search text to allow new searches + self.last_search_text = None; + self.search_results_pending = false; + self.loading_indicator_ref = None; + + // Reset change detection state + self.last_member_count = 0; + self.last_sync_pending = false; + self.pending_popup_cleanup = false; + + // Clear list items + self.cmd_text_input.clear_items(cx); + } + + /// Cleanup helper for closing mention popup + fn close_mention_popup(&mut self, cx: &mut Cx) { + // Reset all search-related state + self.reset_search_state(cx); + + // Get popup and header view references + let mut popup = self.cmd_text_input.view(cx, ids!(popup)); + let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); + + // Force hide header view - necessary when handling deletion operations + // When backspace-deleting mentions, we want to completely hide the header + header_view.set_visible(cx, false); + + // Hide the entire popup + popup.set_visible(cx, false); + + // Reset popup height + script_apply_eval!(cx, popup, { height: Fit }); + + // Ensure header view is reset to visible next time it's triggered + // This will happen before update_user_list is called in handle_text_change + + // Note: Do NOT call request_text_input_focus() here. + // Focus restoration is handled solely via `pending_draw_focus_restore` in draw_walk + // to avoid race conditions between multiple focus mechanisms. + self.cmd_text_input.redraw(cx); + } + + /// Returns the current text content + pub fn text(&self) -> String { + self.cmd_text_input.text_input_ref().text() + } + + /// Sets the text content + pub fn set_text(&mut self, cx: &mut Cx, text: &str) { + self.cmd_text_input.text_input_ref().set_text(cx, text); + self.cmd_text_input.redraw(cx); + } + + /// Sets whether the current user can notify the entire room (@room mention) pub fn set_can_notify_room(&mut self, can_notify: bool) { self.can_notify_room = can_notify; } - /// Gets whether the current user can notify the entire room (@room mention). + /// Gets whether the current user can notify the entire room (@room mention) pub fn can_notify_room(&self) -> bool { self.can_notify_room } } impl MentionableTextInputRef { + pub fn text(&self) -> String { + self.borrow().map_or_else(String::new, |inner| inner.text()) + } + /// Returns a reference to the inner `TextInput` widget. pub fn text_input_ref(&self) -> TextInputRef { - self.child_by_path(ids!(persistent.center.text_input)).as_text_input() + self.borrow() + .map(|inner| inner.cmd_text_input.text_input_ref()) + .unwrap_or_default() + } + + /// Check if mention search is currently active + pub fn is_mention_searching(&self) -> bool { + self.borrow() + .is_some_and(|inner| inner.is_mention_searching()) } - /// Sets whether the current user can notify the entire room (@room mention). + /// Check if ESC was handled by mention popup + pub fn handled_escape(&self) -> bool { + self.borrow().is_some_and(|inner| inner.handled_escape()) + } + + pub fn set_text(&self, cx: &mut Cx, text: &str) { + if let Some(mut inner) = self.borrow_mut() { + inner.set_text(cx, text); + } + } + + /// Sets whether the current user can notify the entire room (@room mention) pub fn set_can_notify_room(&self, can_notify: bool) { if let Some(mut inner) = self.borrow_mut() { inner.set_can_notify_room(can_notify); } } - /// Gets whether the current user can notify the entire room (@room mention). + /// Gets whether the current user can notify the entire room (@room mention) pub fn can_notify_room(&self) -> bool { self.borrow().is_some_and(|inner| inner.can_notify_room()) } - /// Creates a message from the entered text. - /// - /// This mock version handles `/html` and `/plain` prefixes - /// but does not track or extract @mentions (since the mention popup is disabled). + /// Returns the mentions actually present in the given html message content. + fn get_real_mentions_in_html_text(&self, html: &str) -> Mentions { + let mut mentions = Mentions::new(); + + let Some(inner) = self.borrow() else { + return mentions; + }; + + let mut user_ids = BTreeSet::new(); + + for (user_id, username) in &inner.possible_mentions { + if html.contains(&format!( + "{}", + user_id.matrix_to_uri(), + username, + )) { + user_ids.insert(user_id.clone()); + } + } + + mentions.user_ids = user_ids; + // Check for @room mention in HTML content + mentions.room = inner.possible_room_mention && html.contains("@room"); + mentions + } + + /// Returns the mentions actually present in the given markdown message content. + fn get_real_mentions_in_markdown_text(&self, markdown: &str) -> Mentions { + let mut mentions = Mentions::new(); + + let Some(inner) = self.borrow() else { + return mentions; + }; + + let mut user_ids = BTreeSet::new(); + for (user_id, username) in &inner.possible_mentions { + // Check both username format and user_id format for flexibility + let username_pattern = format!("[{}]({})", username, user_id.matrix_to_uri()); + let userid_pattern = format!("[{}]({})", user_id, user_id.matrix_to_uri()); + + if markdown.contains(&username_pattern) || markdown.contains(&userid_pattern) { + user_ids.insert(user_id.clone()); + } + } + + mentions.user_ids = user_ids; + // Check for @room mention in markdown content + mentions.room = inner.possible_room_mention && markdown.contains("@room"); + mentions + } + + /// Processes entered text and creates a message with mentions based on detected message type. + /// This method handles /html, /plain prefixes and defaults to markdown. pub fn create_message_with_mentions(&self, entered_text: &str) -> RoomMessageEventContent { if let Some(html_text) = entered_text.strip_prefix("/html") { - RoomMessageEventContent::text_html(html_text, html_text) + let message = RoomMessageEventContent::text_html(html_text, html_text); + message.add_mentions(self.get_real_mentions_in_html_text(html_text)) } else if let Some(plain_text) = entered_text.strip_prefix("/plain") { + // Plain text messages don't support mentions RoomMessageEventContent::text_plain(plain_text) } else { - RoomMessageEventContent::text_markdown(entered_text) + let message = RoomMessageEventContent::text_markdown(entered_text); + message.add_mentions(self.get_real_mentions_in_markdown_text(entered_text)) } } } From 4bab148a8af9098c4d12965023a23c80454134b2 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 4 Apr 2026 06:13:55 +0800 Subject: [PATCH 073/283] Fix DSL syntax errors in mention templates and remove runtime DSL constants - Convert UserListItem/RoomMentionListItem templates to proper Makepad 2.0 syntax: draw_bg +: instead of draw_bg:, := for named children, remove commas, use REGULAR_TEXT for text_style - Remove script_apply_eval! calls that use DSL constants (Right, Down, Align, Fit) which are not available at runtime scope - Bake desktop layout into DSL templates (flow: Right, height: 32) - Remove MouseCursor.Hand from CommandTextInput (not found in scope) - Add debug logging for selectable_widgets tracking (temporary) Known issues: - Arrow key highlight not visually showing (keyboard navigation works internally but update_highlights not rendering) - Mouse click/hover selection not working Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/command_text_input.rs | 19 +- src/shared/mentionable_text_input.rs | 255 ++++++++++----------------- 2 files changed, 110 insertions(+), 164 deletions(-) diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index bf8a4d091..a75453e42 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -245,6 +245,8 @@ impl Widget for CommandTextInput { if cx.has_key_focus(self.key_controller_text_input_ref().area()) { if let Event::KeyDown(key_event) = event { let popup_visible = self.view(cx, ids!(popup)).visible(); + log!("DEBUG CommandTextInput::KeyDown: key={:?}, popup_visible={}, selectable_count={}, kb_focus={:?}", + key_event.key_code, popup_visible, self.selectable_widgets.len(), self.keyboard_focus_index); if popup_visible { let mut eat_the_event = true; @@ -308,14 +310,14 @@ impl Widget for CommandTextInput { let mut selected_by_click = None; let mut should_redraw = false; + log!("DEBUG CommandTextInput::Actions: self_ptr={:p}, selectable_count={}", self as *const _, self.selectable_widgets.len()); for (idx, item) in self.selectable_widgets.iter().enumerate() { let item = item.as_view(); - if item - .finger_down(actions) - .map(|fe| fe.tap_count == 1) - .unwrap_or(false) + let fd = item.finger_down(actions); + if fd.as_ref().map(|fe| fe.tap_count == 1).unwrap_or(false) { + log!("DEBUG CommandTextInput: finger_down on item {}", idx); selected_by_click = Some((*item).clone()); // Clear keyboard focus when mouse is clicked @@ -526,6 +528,7 @@ impl CommandTextInput { /// /// Normally called as response to `should_build_items`. pub fn clear_items(&mut self, cx: &Cx) { + log!("DEBUG CommandTextInput::clear_items: self_ptr={:p}, was_count={}", self as *const _, self.selectable_widgets.len()); self.list(cx, ids!(list)).clear(); self.selectable_widgets.clear(); self.keyboard_focus_index = None; @@ -538,6 +541,7 @@ impl CommandTextInput { pub fn add_item(&mut self, cx: &Cx, widget: WidgetRef) { self.list(cx, ids!(list)).add(widget.clone()); self.selectable_widgets.push(widget); + log!("DEBUG CommandTextInput::add_item: self_ptr={:p}, new_count={}", self as *const _, self.selectable_widgets.len()); self.keyboard_focus_index = self.keyboard_focus_index.or(Some(0)); } @@ -747,12 +751,15 @@ impl CommandTextInput { fn update_highlights(&mut self, cx: &mut Cx) { // Check if currently there is a keyboard-focused item let has_keyboard_focus = self.keyboard_focus_index.is_some(); + if !self.selectable_widgets.is_empty() { + log!("DEBUG update_highlights: self_ptr={:p}, count={}, kb_focus={:?}, hover={:?}", + self as *const _, self.selectable_widgets.len(), self.keyboard_focus_index, self.pointer_hover_index); + } for (idx, item) in self.selectable_widgets.iter().enumerate() { let mut item = item.clone(); script_apply_eval!(cx, item, { - show_bg: true, - cursor: MouseCursor.Hand + show_bg: true }); // If there is a keyboard focus, prioritize it over mouse hover diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 02f9fd9b9..ef7bf4c77 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -175,137 +175,96 @@ script_mod! { // Template for user list items in the mention dropdown mod.widgets.UserListItem = View { - width: Fill, - height: Fit, + width: Fill + height: 32 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 4 bottom: 4} show_bg: true - cursor: Hand - draw_bg: { - color: (COLOR_PRIMARY), - uniform border_radius: 4.0, - instance hover: 0.0, - instance selected: 0.0, - - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size); - // Draw rounded rectangle with configurable radius - sdf.box(0., 0., self.rect_size.x, self.rect_size.y, self.border_radius); - - if self.selected > 0.0 { - sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) - } else if self.hover > 0.0 { - sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) - } else { - // Default state - sdf.fill(self.color) - } - return sdf.result - } + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 4.0 } - flow: Down + flow: Right spacing: 2.0 + align: Align{y: 0.5} - user_info = View { - width: Fill, - height: Fit, - flow: Right, + user_info := View { + width: Fill + height: Fit + flow: Right spacing: 8.0 align: Align{y: 0.5} - avatar = Avatar { - width: 24, - height: 24, - text_view = { text = { draw_text: { - text_style: { font_size: 12.0 } - }}} + avatar := Avatar { + width: 24 + height: 24 } - username = Label { - height: Fit, - draw_text: { - color: #000, - text_style: {font_size: 14.0} + username := Label { + height: Fit + draw_text +: { + color: #000 + text_style: REGULAR_TEXT {font_size: 14.0} } } - filler = FillerX {} + filler := FillerX {} } - user_id = Label { - height: Fit, - draw_text: { - color: #666, - text_style: {font_size: 12.0} + user_id := Label { + height: Fit + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 12.0} } } } // Template for the @room mention list item mod.widgets.RoomMentionListItem = View { - width: Fill, - height: Fit, + width: Fill + height: 32 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 4 bottom: 4} show_bg: true - cursor: Hand - draw_bg: { - color: (COLOR_PRIMARY), - uniform border_radius: 4.0, - instance hover: 0.0, - instance selected: 0.0, - - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size); - sdf.box(0., 0., self.rect_size.x, self.rect_size.y, self.border_radius); - - if self.selected > 0.0 { - sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) - } else if self.hover > 0.0 { - sdf.fill(KEYBOARD_FOCUS_OR_COLOR_HOVER) - } else { - sdf.fill(self.color) - } - return sdf.result - } + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 4.0 } - flow: Down + flow: Right spacing: 2.0 align: Align{y: 0.5} - user_info = View { - width: Fill, - height: Fit, - flow: Right, + user_info := View { + width: Fill + height: Fit + flow: Right spacing: 8.0 align: Align{y: 0.5} - room_avatar = Avatar { - width: 24, - height: 24, - text_view = { text = { draw_text: { - text_style: { font_size: 12.0 } - }}} + room_avatar := Avatar { + width: 24 + height: 24 } - room_mention = Label { - height: Fit, - draw_text: { - color: #000, - text_style: {font_size: 14.0} + room_mention := Label { + height: Fit + draw_text +: { + color: #000 + text_style: REGULAR_TEXT {font_size: 14.0} } text: "Notify the entire room" } - filler = FillerX {} + filler := FillerX {} } - room_user_id = Label { - height: Fit, - align: Align{y: 0.5}, - draw_text: { - color: #666, - text_style: {font_size: 12.0} + room_user_id := Label { + height: Fit + align: Align{y: 0.5} + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 12.0} } text: "@room" } @@ -313,54 +272,56 @@ script_mod! { // Template for loading indicator when members are being fetched mod.widgets.LoadingIndicator = View { - width: Fill, - height: 48, + width: Fill + height: 48 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 8 bottom: 8} - flow: Right, - spacing: 8.0, + flow: Right + spacing: 8.0 align: Align{x: 0.0 y: 0.5} - draw_bg: { - color: (COLOR_PRIMARY), + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) } - loading_text = Label { - height: Fit, - draw_text: { - color: #666, - text_style: {font_size: 14.0} + loading_text := Label { + height: Fit + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 14.0} } text: "Loading members" } - loading_animation = BouncingDots { - width: 60, - height: 24, - draw_bg: { - color: (COLOR_ROBRIX_PURPLE), - dot_radius: 2.0, + loading_animation := BouncingDots { + width: 60 + height: 24 + draw_bg +: { + color: (COLOR_ROBRIX_PURPLE) + dot_radius: 2.0 } } } // Template for no matches indicator when no users match the search mod.widgets.NoMatchesIndicator = View { - width: Fill, - height: 48, + width: Fill + height: 48 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 8 bottom: 8} - flow: Right, - spacing: 8.0, + flow: Right + spacing: 8.0 align: Align{x: 0.0 y: 0.5} - draw_bg: { - color: (COLOR_PRIMARY), + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) } - no_matches_text = Label { - height: Fit, - draw_text: { - color: #666, - text_style: {font_size: 14.0} + no_matches_text := Label { + height: Fit + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 14.0} } text: "No matching users found" } @@ -368,29 +329,29 @@ script_mod! { mod.widgets.MentionableTextInput = #(MentionableTextInput::register_widget(vm)) { ..mod.widgets.CommandTextInput - width: Fill, + width: Fill height: Fit trigger: "@" inline_search: true - color_focus: (FOCUS_HOVER_COLOR), - color_hover: (FOCUS_HOVER_COLOR), + color_focus: (FOCUS_HOVER_COLOR) + color_hover: (FOCUS_HOVER_COLOR) popup +: { spacing: 0.0 padding: 0.0 - draw_bg: { - color: (COLOR_SECONDARY), + draw_bg +: { + color: (COLOR_SECONDARY) } header_view +: { margin: Inset{left: 4 right: 4} - draw_bg: { - color: (COLOR_ROBRIX_PURPLE), + draw_bg +: { + color: (COLOR_ROBRIX_PURPLE) } header_label +: { - draw_text: { - color: (COLOR_PRIMARY_DARKER), + draw_text +: { + color: (COLOR_PRIMARY_DARKER) } text: "Users in this Room" } @@ -408,7 +369,7 @@ script_mod! { top +: { height: 0 } bottom +: { height: 0 } center +: { - text_input = RobrixTextInput { + text_input := RobrixTextInput { empty_text: "Start typing..." } } @@ -918,17 +879,8 @@ impl MentionableTextInput { } else { MOBILE_ITEM_HEIGHT }; - if is_desktop { - script_apply_eval!(cx, room_mention_item, { - height: #(new_height) - flow: Right - }); - } else { - script_apply_eval!(cx, room_mention_item, { - height: #(new_height) - flow: Down - }); - } + // Layout is set in the DSL template (defaults to desktop layout). + // TODO: add mobile-specific layout when adaptive layout is implemented. self.cmd_text_input.add_item(cx, room_mention_item); true @@ -986,21 +938,8 @@ impl MentionableTextInput { let user_id_str = member.user_id().as_str(); item.label(cx, ids!(user_id)).set_text(cx, user_id_str); - if is_desktop { - script_apply_eval!(cx, item, { - flow: Right - height: #(DESKTOP_ITEM_HEIGHT) - align: Align{y: 0.5} - }); - item.view(cx, ids!(user_info.filler)).set_visible(cx, true); - } else { - script_apply_eval!(cx, item, { - flow: Down - height: #(MOBILE_ITEM_HEIGHT) - spacing: #(MOBILE_USERNAME_SPACING) - }); - item.view(cx, ids!(user_info.filler)).set_visible(cx, false); - } + // Layout is set in the DSL template (defaults to desktop layout). + // TODO: add mobile-specific layout when adaptive layout is implemented. let avatar = item.avatar(cx, ids!(user_info.avatar)); if let Some(mxc_uri) = member.avatar_url() { @@ -1049,7 +988,7 @@ impl MentionableTextInput { match &self.search_state { MentionSearchState::Idle | MentionSearchState::JustCancelled => { // Not in search mode, hide popup - script_apply_eval!(cx, popup, { height: Fit }); + popup.set_visible(cx, false); } MentionSearchState::WaitingForMembers { .. } => { @@ -1729,7 +1668,7 @@ impl MentionableTextInput { header_view.set_visible(cx, true); // Let popup auto-size based on content - script_apply_eval!(cx, popup, { height: Fit }); + // Maintain text input focus so user can continue typing, but only if currently focused let text_input_area = self.cmd_text_input.text_input_ref().area(); @@ -1823,7 +1762,7 @@ impl MentionableTextInput { popup.set_visible(cx, false); // Reset popup height - script_apply_eval!(cx, popup, { height: Fit }); + // Ensure header view is reset to visible next time it's triggered // This will happen before update_user_list is called in handle_text_change From 810fd6bb841373dd03e6d662df0d34cffb0c3bbf Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 4 Apr 2026 23:11:16 +0800 Subject: [PATCH 074/283] Fix popup item selection: add cursor, refactor highlight logic Co-authored with Codex: - Add cursor: MouseCursor.Hand to UserListItem/RoomMentionListItem (fixes finger_down events not being dispatched) - Extract popup_item_highlight_color() helper with unit tests - Change highlight syntax from draw_bg.color: to draw_bg: { color: } - Fix color_focus fallback to visible dark blue (#1C274C) Arrow key and mouse selection now work internally. Highlight rendering still not visible (needs further investigation). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/command_text_input.rs | 101 +++++++++++++++++++-------- src/shared/mentionable_text_input.rs | 9 +-- 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index a75453e42..6614fa737 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -749,39 +749,21 @@ impl CommandTextInput { } fn update_highlights(&mut self, cx: &mut Cx) { - // Check if currently there is a keyboard-focused item - let has_keyboard_focus = self.keyboard_focus_index.is_some(); - if !self.selectable_widgets.is_empty() { - log!("DEBUG update_highlights: self_ptr={:p}, count={}, kb_focus={:?}, hover={:?}", - self as *const _, self.selectable_widgets.len(), self.keyboard_focus_index, self.pointer_hover_index); - } - for (idx, item) in self.selectable_widgets.iter().enumerate() { + let color = popup_item_highlight_color( + idx, + self.keyboard_focus_index, + self.pointer_hover_index, + self.color_focus, + self.color_hover, + ); + let mut item = item.clone(); script_apply_eval!(cx, item, { - show_bg: true + draw_bg: { + color: #(color) + } }); - - // If there is a keyboard focus, prioritize it over mouse hover - // If there is no keyboard focus, show mouse hover - if Some(idx) == self.keyboard_focus_index { - // Keyboard-selected item is highlighted in blue - let color = self.color_focus; - script_apply_eval!(cx, item, { - draw_bg.color: #(color) - }); - } else if Some(idx) == self.pointer_hover_index && !has_keyboard_focus { - // Mouse-hovered item is highlighted in gray, but only when there is no keyboard focus - let color = self.color_hover; - script_apply_eval!(cx, item, { - draw_bg.color: #(color) - }); - } else { - // Default state - script_apply_eval!(cx, item, { - draw_bg.color: #00000000 - }); - } } } @@ -873,6 +855,67 @@ fn is_whitespace(grapheme: &str) -> bool { grapheme.chars().all(char::is_whitespace) } +fn popup_item_highlight_color( + idx: usize, + keyboard_focus_index: Option, + pointer_hover_index: Option, + color_focus: Vec4f, + color_hover: Vec4f, +) -> Vec4f { + if Some(idx) == keyboard_focus_index { + if color_focus == Vec4f::default() { + vec4(0.11, 0.15, 0.30, 1.0) + } else { + color_focus + } + } else if Some(idx) == pointer_hover_index && keyboard_focus_index.is_none() { + if color_hover == Vec4f::default() { + vec4(0.11, 0.15, 0.30, 0.7) + } else { + color_hover + } + } else { + vec4(0.0, 0.0, 0.0, 0.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn highlight_prefers_keyboard_focus_over_hover() { + let focus = vec4(0.11, 0.15, 0.30, 1.0); + let hover = vec4(0.80, 0.80, 0.80, 1.0); + + assert_eq!( + popup_item_highlight_color(1, Some(1), Some(1), focus, hover), + focus, + ); + } + + #[test] + fn highlight_uses_hover_only_without_keyboard_focus() { + let focus = vec4(0.11, 0.15, 0.30, 1.0); + let hover = vec4(0.80, 0.80, 0.80, 1.0); + + assert_eq!( + popup_item_highlight_color(1, None, Some(1), focus, hover), + hover, + ); + } + + #[test] + fn highlight_falls_back_to_dark_blue_when_focus_color_missing() { + let hover = vec4(0.80, 0.80, 0.80, 1.0); + + assert_eq!( + popup_item_highlight_color(0, Some(0), None, Vec4f::default(), hover), + vec4(0.11, 0.15, 0.30, 1.0), + ); + } +} + /// Reduced and adapted copy of the `List` widget from Moly. #[derive(Script, ScriptHook, Widget)] struct List { diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index ef7bf4c77..cb63dcef0 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -179,6 +179,7 @@ script_mod! { height: 32 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: (COLOR_PRIMARY) @@ -226,6 +227,7 @@ script_mod! { height: 32 margin: Inset{left: 4 right: 4} padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: (COLOR_PRIMARY) @@ -334,7 +336,7 @@ script_mod! { trigger: "@" inline_search: true - color_focus: (FOCUS_HOVER_COLOR) + color_focus: (KEYBOARD_FOCUS_OR_COLOR_HOVER) color_hover: (FOCUS_HOVER_COLOR) popup +: { @@ -823,7 +825,7 @@ impl MentionableTextInput { let Some(ptr) = self.room_mention_list_item else { return false; }; - let mut room_mention_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); + let room_mention_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); let mut room_avatar_shown = false; let avatar_ref = room_mention_item.avatar(cx, ids!(user_info.room_avatar)); @@ -930,7 +932,7 @@ impl MentionableTextInput { // user_list_item_ptr is None continue; }; - let mut item = crate::widget_ref_from_live_ptr(cx, Some(user_list_item_ptr)); + let item = crate::widget_ref_from_live_ptr(cx, Some(user_list_item_ptr)); item.label(cx, ids!(user_info.username)).set_text(cx, &display_name); @@ -962,7 +964,6 @@ impl MentionableTextInput { // Set keyboard focus to the first item if index == 0 { - // If @room exists, it's index 0, otherwise first user is index 0 self.cmd_text_input.set_keyboard_focus_index(0); } } From 4d77a58f1a5b9090aa512e5c9cd6e8f956babf32 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sun, 5 Apr 2026 01:30:32 +0800 Subject: [PATCH 075/283] Fix highlight rendering and polish mention popup UI Root cause of highlight not working: script_apply_eval! cannot modify draw_bg on widgets created via widget_ref_from_live_ptr() because script_source() returns ScriptObject::ZERO (empty eval scope). Fix: Use Animator states with shader instance variables instead: - Add selected: instance(0.0) to UserListItem/RoomMentionListItem draw_bg - Add pixel shader that mixes base color with highlight via selected var - Add Animator with highlight.on/off states - update_highlights() now uses view.animator_cut() instead of script_apply_eval! UI polish: - Popup: rounded corners, border, shadow - Header: white text, better padding - Highlight: soft blue (#1E90FF at 20% opacity) with animation - Username: bold, dark color - User ID: right-aligned via FillerX, smaller and lighter - Compact items: height 36px, minimal spacing Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/command_text_input.rs | 25 ++--- src/shared/mentionable_text_input.rs | 160 +++++++++++++++++---------- 2 files changed, 110 insertions(+), 75 deletions(-) diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index 6614fa737..2ab7a02e9 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -749,21 +749,18 @@ impl CommandTextInput { } fn update_highlights(&mut self, cx: &mut Cx) { + let has_keyboard_focus = self.keyboard_focus_index.is_some(); + for (idx, item) in self.selectable_widgets.iter().enumerate() { - let color = popup_item_highlight_color( - idx, - self.keyboard_focus_index, - self.pointer_hover_index, - self.color_focus, - self.color_hover, - ); - - let mut item = item.clone(); - script_apply_eval!(cx, item, { - draw_bg: { - color: #(color) - } - }); + let is_highlighted = Some(idx) == self.keyboard_focus_index + || (Some(idx) == self.pointer_hover_index && !has_keyboard_focus); + + let view = item.as_view(); + if is_highlighted { + view.animator_cut(cx, ids!(highlight.on)); + } else { + view.animator_cut(cx, ids!(highlight.off)); + } } } diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index cb63dcef0..7ca74670c 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -176,47 +176,63 @@ script_mod! { // Template for user list items in the mention dropdown mod.widgets.UserListItem = View { width: Fill - height: 32 - margin: Inset{left: 4 right: 4} - padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + height: 36 + margin: Inset{left: 3 right: 3} + padding: Inset{left: 10 right: 10 top: 4 bottom: 4} cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: (COLOR_PRIMARY) border_radius: 4.0 + selected: instance(0.0) + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + sdf.box(0. 0. self.rect_size.x self.rect_size.y self.border_radius) + let highlight = #x1E90FF30 + sdf.fill(Pal.premul(self.color.mix(highlight self.selected))) + return sdf.result + } + } + + animator: Animator { + highlight: { + default: @off + off: AnimatorState { + from: { all: Forward { duration: 0.12 } } + apply: { draw_bg: { selected: 0.0 } } + } + on: AnimatorState { + from: { all: Forward { duration: 0.08 } } + apply: { draw_bg: { selected: 1.0 } } + } + } } + flow: Right - spacing: 2.0 + spacing: 8.0 align: Align{y: 0.5} - user_info := View { - width: Fill - height: Fit - flow: Right - spacing: 8.0 - align: Align{y: 0.5} - - avatar := Avatar { - width: 24 - height: 24 - } + avatar := Avatar { + width: 26 + height: 26 + } - username := Label { - height: Fit - draw_text +: { - color: #000 - text_style: REGULAR_TEXT {font_size: 14.0} - } + username := Label { + height: Fit + draw_text +: { + color: #222 + text_style: BOLD_TEXT {font_size: 13.0} } - - filler := FillerX {} } + filler := FillerX {} + user_id := Label { height: Fit draw_text +: { - color: #666 - text_style: REGULAR_TEXT {font_size: 12.0} + color: #aaa + text_style: REGULAR_TEXT {font_size: 10.0} } } } @@ -224,49 +240,62 @@ script_mod! { // Template for the @room mention list item mod.widgets.RoomMentionListItem = View { width: Fill - height: 32 + height: 40 margin: Inset{left: 4 right: 4} - padding: Inset{left: 8 right: 8 top: 4 bottom: 4} + padding: Inset{left: 10 right: 10 top: 6 bottom: 6} cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: (COLOR_PRIMARY) border_radius: 4.0 + selected: instance(0.0) + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + sdf.box(0. 0. self.rect_size.x self.rect_size.y self.border_radius) + let highlight = #x1E90FF30 + sdf.fill(Pal.premul(self.color.mix(highlight self.selected))) + return sdf.result + } } + + animator: Animator { + highlight: { + default: @off + off: AnimatorState { + from: { all: Forward { duration: 0.12 } } + apply: { draw_bg: { selected: 0.0 } } + } + on: AnimatorState { + from: { all: Forward { duration: 0.08 } } + apply: { draw_bg: { selected: 1.0 } } + } + } + } + flow: Right - spacing: 2.0 + spacing: 10.0 align: Align{y: 0.5} - user_info := View { - width: Fill - height: Fit - flow: Right - spacing: 8.0 - align: Align{y: 0.5} - - room_avatar := Avatar { - width: 24 - height: 24 - } + room_avatar := Avatar { + width: 28 + height: 28 + } - room_mention := Label { - height: Fit - draw_text +: { - color: #000 - text_style: REGULAR_TEXT {font_size: 14.0} - } - text: "Notify the entire room" + room_mention := Label { + height: Fit + draw_text +: { + color: #222 + text_style: BOLD_TEXT {font_size: 13.0} } - - filler := FillerX {} + text: "Notify the entire room" } room_user_id := Label { height: Fit - align: Align{y: 0.5} draw_text +: { - color: #666 - text_style: REGULAR_TEXT {font_size: 12.0} + color: #aaa + text_style: REGULAR_TEXT {font_size: 10.0} } text: "@room" } @@ -341,19 +370,28 @@ script_mod! { popup +: { spacing: 0.0 - padding: 0.0 + padding: Inset{top: 0 bottom: 6 left: 0 right: 0} draw_bg +: { - color: (COLOR_SECONDARY) + color: (COLOR_PRIMARY) + border_radius: 6.0 + border_size: 1.0 + border_color: #ddd + shadow_color: #0003 + shadow_radius: 12.0 + shadow_offset: vec2(0.0 2.0) } header_view +: { - margin: Inset{left: 4 right: 4} + margin: Inset{left: 0 right: 0 top: 0 bottom: 2} + padding: Inset{left: 12 right: 12 top: 8 bottom: 8} draw_bg +: { color: (COLOR_ROBRIX_PURPLE) + border_radius: 6.0 } header_label +: { draw_text +: { - color: (COLOR_PRIMARY_DARKER) + color: #fff + text_style: REGULAR_TEXT {font_size: 11.0} } text: "Users in this Room" } @@ -363,7 +401,7 @@ script_mod! { height: Fit clip_y: true spacing: 0.0 - padding: 0.0 + padding: Inset{top: 2 bottom: 2 left: 0 right: 0} } } @@ -828,7 +866,7 @@ impl MentionableTextInput { let room_mention_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); let mut room_avatar_shown = false; - let avatar_ref = room_mention_item.avatar(cx, ids!(user_info.room_avatar)); + let avatar_ref = room_mention_item.avatar(cx, ids!(room_avatar)); // Get room avatar fallback text from room name (with automatic ID fallback) let room_label = room_props.room_name_id.to_string(); @@ -934,7 +972,7 @@ impl MentionableTextInput { }; let item = crate::widget_ref_from_live_ptr(cx, Some(user_list_item_ptr)); - item.label(cx, ids!(user_info.username)).set_text(cx, &display_name); + item.label(cx, ids!(username)).set_text(cx, &display_name); // Use the full user ID string let user_id_str = member.user_id().as_str(); @@ -943,7 +981,7 @@ impl MentionableTextInput { // Layout is set in the DSL template (defaults to desktop layout). // TODO: add mobile-specific layout when adaptive layout is implemented. - let avatar = item.avatar(cx, ids!(user_info.avatar)); + let avatar = item.avatar(cx, ids!(avatar)); if let Some(mxc_uri) = member.avatar_url() { match get_or_fetch_avatar(cx, &mxc_uri.to_owned()) { AvatarCacheEntry::Loaded(avatar_data) => { @@ -1055,7 +1093,7 @@ impl MentionableTextInput { let head = text_input_ref.borrow().map_or(0, |p| p.cursor().index); if let Some(start_idx) = self.get_trigger_position() { - let room_mention_label = selected.label(cx, ids!(user_info.room_mention)); + let room_mention_label = selected.label(cx, ids!(room_mention)); let room_mention_text = room_mention_label.text(); let room_user_id_text = selected.label(cx, ids!(room_user_id)).text(); @@ -1068,7 +1106,7 @@ impl MentionableTextInput { "@room ".to_string() } else { // User selected a specific user - let username = selected.label(cx, ids!(user_info.username)).text(); + let username = selected.label(cx, ids!(username)).text(); let user_id_str = selected.label(cx, ids!(user_id)).text(); let Ok(user_id): Result = user_id_str.clone().try_into() else { // Invalid user ID format - skip selection From 582905cb1dfb0a345a27f757eaef67b84f282e71 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sun, 5 Apr 2026 21:46:27 +0800 Subject: [PATCH 076/283] Fix mention popup quality issues and sync gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored with Codex: - Fix No-matches/Loading items being selectable (use unselectable path) - Fix loading indicator not showing (rebuild on each frame instead of caching, prevents same-frame clear_items from removing it) - Fix popup not refreshing on member count changes or sync completion - Fix member search Top-K tie-breaker: use explicit SearchCandidate comparator for stable ordering at same priority - Fix logout→login first-room @mention: show loading until sync completes, then refresh with full member list - Add room_members_sync_pending lifecycle to RoomScreen - Post-sync local member refresh for accurate popup rendering Co-Authored-By: Claude Opus 4.6 (1M context) --- src/home/room_screen.rs | 27 ++- src/room/member_search.rs | 325 ++++++++++++++------------- src/shared/command_text_input.rs | 1 + src/shared/mentionable_text_input.rs | 286 +++++++++++++++++------ 4 files changed, 411 insertions(+), 228 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 81e62106e..66059b365 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2015,6 +2015,7 @@ impl Widget for RoomScreen { room_name_id: self.room_name_id.clone().unwrap_or_else(|| RoomNameId::empty(room_id)), timeline_kind: tl.kind.clone(), room_members, + room_members_sync_pending: tl.room_members_sync_pending, room_avatar_url: self.room_avatar_url.clone(), app_service_enabled, app_service_room_bound, @@ -2028,6 +2029,7 @@ impl Widget for RoomScreen { timeline_kind: self.timeline_kind.clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, + room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, @@ -2046,6 +2048,7 @@ impl Widget for RoomScreen { room_name_id: RoomNameId::empty(room_id.clone()), timeline_kind: TimelineKind::MainRoom { room_id }, room_members: None, + room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, @@ -3184,9 +3187,12 @@ impl RoomScreen { } } TimelineUpdate::RoomMembersSynced => { - // log!("process_timeline_updates(): room members fetched for room {}", tl.kind.room_id()); - // Here, to be most efficient, we could redraw only the user avatars and names in the timeline, - // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. + tl.awaiting_post_sync_member_refresh = true; + submit_async_request(MatrixRequest::GetRoomMembers { + timeline_kind: tl.kind.clone(), + memberships: matrix_sdk::RoomMemberships::JOIN, + local_only: true, + }); } TimelineUpdate::RoomMembersListFetched { members } => { let members = Arc::new(members); @@ -3203,6 +3209,10 @@ impl RoomScreen { }); } } + if tl.awaiting_post_sync_member_refresh { + tl.room_members_sync_pending = false; + tl.awaiting_post_sync_member_refresh = false; + } tl.room_members = Some(members); }, TimelineUpdate::MediaFetched(request) => { @@ -4065,6 +4075,8 @@ impl RoomScreen { user_power: UserPowerLevels::all(), // Room members start as None and get populated when fetched from the server room_members: None, + room_members_sync_pending: false, + awaiting_post_sync_member_refresh: false, // We assume timelines being viewed for the first time haven't been fully paginated. fully_paginated: false, backwards_pagination_in_flight: false, @@ -4128,6 +4140,8 @@ impl RoomScreen { // Even though we specify that room member profiles should be lazy-loaded, // the matrix server still doesn't consistently send them to our client properly. // So we kick off a request to fetch the room members here upon first viewing the room. + tl_state.room_members_sync_pending = true; + tl_state.awaiting_post_sync_member_refresh = false; submit_async_request(MatrixRequest::SyncRoomMemberList { timeline_kind: tl_state.kind.clone(), }); @@ -4464,6 +4478,7 @@ pub struct RoomScreenProps { pub room_name_id: RoomNameId, pub timeline_kind: TimelineKind, pub room_members: Option>>, + pub room_members_sync_pending: bool, pub room_avatar_url: Option, pub app_service_enabled: bool, pub app_service_room_bound: bool, @@ -4623,6 +4638,12 @@ struct TimelineUiState { /// The list of room members for this room. room_members: Option>>, + /// Whether the initial room-member sync is still in progress for this room. + room_members_sync_pending: bool, + + /// Whether we're waiting for a refreshed local member snapshot after sync completion. + awaiting_post_sync_member_refresh: bool, + /// Whether this room's timeline has been fully paginated, which means /// that the oldest (first) event in the timeline is locally synced and available. /// When `true`, further backwards pagination requests will not be sent. diff --git a/src/room/member_search.rs b/src/room/member_search.rs index 99db6911f..dacb68c73 100644 --- a/src/room/member_search.rs +++ b/src/room/member_search.rs @@ -18,7 +18,7 @@ use makepad_widgets::log; const BATCH_SIZE: usize = 10; // Number of results per streamed batch /// Pre-computed member sort key for fast empty search -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct MemberSortKey { /// Power level rank: 0=Admin, 1=Moderator, 2=User pub power_rank: u8, @@ -37,6 +37,126 @@ pub struct PrecomputedMemberSort { pub member_keys: Vec, } +#[derive(Debug, Clone, Eq, PartialEq)] +struct SearchCandidate { + priority: u8, + sort_key: MemberSortKey, + index: usize, +} + +impl SearchCandidate { + fn from_member( + members: &[RoomMember], + index: usize, + priority: u8, + precomputed_sort: Option<&PrecomputedMemberSort>, + ) -> Self { + let sort_key = precomputed_sort + .map(|sort_data| sort_data.member_keys[index].clone()) + .unwrap_or_else(|| member_sort_key_for_member(&members[index])); + + Self { + priority, + sort_key, + index, + } + } + + #[cfg(test)] + fn new_for_test( + priority: u8, + power_rank: u8, + name_category: u8, + sort_key: &str, + index: usize, + ) -> Self { + Self { + priority, + sort_key: MemberSortKey { + power_rank, + name_category, + sort_key: sort_key.to_owned(), + }, + index, + } + } +} + +impl Ord for SearchCandidate { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.priority + .cmp(&other.priority) + .then_with(|| self.sort_key.cmp(&other.sort_key)) + .then_with(|| self.index.cmp(&other.index)) + } +} + +impl PartialOrd for SearchCandidate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn push_top_candidate( + heap: &mut BinaryHeap, + max_results: usize, + candidate: SearchCandidate, +) { + if max_results == 0 { + return; + } + + if heap.len() < max_results { + heap.push(candidate); + } else if heap.peek().is_some_and(|worst| candidate < *worst) { + let _ = heap.pop(); + heap.push(candidate); + } +} + +fn member_sort_key_for_member(member: &RoomMember) -> MemberSortKey { + let power_rank = role_to_rank(member.suggested_role_for_power_level()); + + let raw_name = member + .display_name() + .map(|n| n.trim()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| member.user_id().localpart()); + + let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); + let sort_key = if stripped.is_empty() { + if raw_name.is_ascii() { + raw_name.to_ascii_lowercase() + } else { + raw_name.to_lowercase() + } + } else if stripped.is_ascii() { + stripped.to_ascii_lowercase() + } else { + stripped.to_lowercase() + }; + + let name_category = if !stripped.is_empty() { + match stripped.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + } + } else { + match raw_name.chars().next() { + Some(c) if c.is_alphabetic() => 0, + Some(c) if c.is_numeric() => 1, + _ => 2, + } + }; + + MemberSortKey { + power_rank, + name_category, + sort_key, + } +} + /// Pre-compute sort keys and indices for room members /// This is called once when members are fetched, avoiding repeated computation pub fn precompute_member_sort(members: &[RoomMember]) -> PrecomputedMemberSort { @@ -58,60 +178,10 @@ pub fn precompute_member_sort(members: &[RoomMember]) -> PrecomputedMemberSort { } } - // Get power level rank - let power_rank = role_to_rank(member.suggested_role_for_power_level()); - - // Get normalized display name - let raw_name = member - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| member.user_id().localpart()); - - // Generate sort key by stripping leading non-alphanumeric - let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); - let sort_key = if stripped.is_empty() { - // Name is all symbols, use original - if raw_name.is_ascii() { - raw_name.to_ascii_lowercase() - } else { - raw_name.to_lowercase() - } - } else { - // Use stripped version for sorting - if stripped.is_ascii() { - stripped.to_ascii_lowercase() - } else { - stripped.to_lowercase() - } - }; - - // Determine name category based on stripped name for consistency - // This makes "!!!alice" categorized as alphabetic, not symbols - let name_category = if !stripped.is_empty() { - // Use first char of stripped name - match stripped.chars().next() { - Some(c) if c.is_alphabetic() => 0, - Some(c) if c.is_numeric() => 1, - _ => 2, - } - } else { - // Name is all symbols, use original first char - match raw_name.chars().next() { - Some(c) if c.is_alphabetic() => 0, // Shouldn't happen if stripped is empty - Some(c) if c.is_numeric() => 1, // Shouldn't happen if stripped is empty - _ => 2, // Symbols - } - }; - - let key = MemberSortKey { - power_rank, - name_category, - sort_key: sort_key.clone(), - }; + let key = member_sort_key_for_member(member); member_keys.push(key.clone()); - sortable_members.push((power_rank, name_category, sort_key, index)); + sortable_members.push((key.power_rank, key.name_category, key.sort_key.clone(), index)); } // Sort all valid members @@ -230,7 +300,7 @@ fn compute_empty_search_indices( return Some(indices); } - let mut valid_members: Vec<(u8, u8, usize)> = Vec::with_capacity(members.len()); + let mut valid_members: Vec<(MemberSortKey, usize)> = Vec::with_capacity(members.len()); for (index, member) in members.iter().enumerate() { if is_cancelled(cancel_token) { @@ -241,55 +311,14 @@ fn compute_empty_search_indices( continue; } - let power_rank = role_to_rank(member.suggested_role_for_power_level()); - - let raw_name = member - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| member.user_id().localpart()); - - let stripped = raw_name.trim_start_matches(|c: char| !c.is_alphanumeric()); - let name_category = if !stripped.is_empty() { - match stripped.chars().next() { - Some(c) if c.is_alphabetic() => 0, - Some(c) if c.is_numeric() => 1, - _ => 2, - } - } else { - 2 - }; - - valid_members.push((power_rank, name_category, index)); + valid_members.push((member_sort_key_for_member(member), index)); } if is_cancelled(cancel_token) { return None; } - valid_members.sort_by(|a, b| match a.0.cmp(&b.0) { - std::cmp::Ordering::Equal => match a.1.cmp(&b.1) { - std::cmp::Ordering::Equal => { - let name_a = members[a.2] - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| members[a.2].user_id().localpart()); - let name_b = members[b.2] - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| members[b.2].user_id().localpart()); - - name_a - .chars() - .map(|c| c.to_ascii_lowercase()) - .cmp(name_b.chars().map(|c| c.to_ascii_lowercase())) - } - other => other, - }, - other => other, - }); + valid_members.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); if is_cancelled(cancel_token) { return None; @@ -297,7 +326,7 @@ fn compute_empty_search_indices( valid_members.truncate(max_results); - Some(valid_members.into_iter().map(|(_, _, idx)| idx).collect()) + Some(valid_members.into_iter().map(|(_, idx)| idx).collect()) } fn compute_non_empty_search_indices( @@ -312,7 +341,7 @@ fn compute_non_empty_search_indices( return None; } - let mut top_matches: BinaryHeap<(u8, usize)> = BinaryHeap::with_capacity(max_results); + let mut top_matches: BinaryHeap = BinaryHeap::with_capacity(max_results); let mut high_priority_count = 0; let mut best_priority_seen = u8::MAX; @@ -331,14 +360,11 @@ fn compute_non_empty_search_indices( } best_priority_seen = best_priority_seen.min(priority); - if top_matches.len() < max_results { - top_matches.push((priority, index)); - } else if let Some(&(worst_priority, _)) = top_matches.peek() { - if priority < worst_priority { - top_matches.pop(); - top_matches.push((priority, index)); - } - } + push_top_candidate( + &mut top_matches, + max_results, + SearchCandidate::from_member(members, index, priority, precomputed_sort), + ); if max_results > 0 && high_priority_count >= max_results * 2 @@ -354,64 +380,13 @@ fn compute_non_empty_search_indices( return None; } - let mut all_matches: Vec<(u8, usize)> = top_matches.into_iter().collect(); - - all_matches.sort_by(|(priority_a, idx_a), (priority_b, idx_b)| { - match priority_a.cmp(priority_b) { - std::cmp::Ordering::Equal => { - if let Some(sort_data) = precomputed_sort { - let key_a = &sort_data.member_keys[*idx_a]; - let key_b = &sort_data.member_keys[*idx_b]; - - match key_a.power_rank.cmp(&key_b.power_rank) { - std::cmp::Ordering::Equal => match key_a.name_category.cmp(&key_b.name_category) { - std::cmp::Ordering::Equal => key_a.sort_key.cmp(&key_b.sort_key), - other => other, - }, - other => other, - } - } else { - let member_a = &members[*idx_a]; - let member_b = &members[*idx_b]; - - let power_a = role_to_rank(member_a.suggested_role_for_power_level()); - let power_b = role_to_rank(member_b.suggested_role_for_power_level()); - - match power_a.cmp(&power_b) { - std::cmp::Ordering::Equal => { - let name_a = member_a - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| member_a.user_id().localpart()); - let name_b = member_b - .display_name() - .map(|n| n.trim()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| member_b.user_id().localpart()); - - if name_a.is_ascii() && name_b.is_ascii() { - name_a - .chars() - .map(|c| c.to_ascii_lowercase()) - .cmp(name_b.chars().map(|c| c.to_ascii_lowercase())) - } else { - name_a.to_lowercase().cmp(&name_b.to_lowercase()) - } - } - other => other, - } - } - } - other => other, - } - }); + let all_matches = top_matches.into_sorted_vec(); if is_cancelled(cancel_token) { return None; } - Some(all_matches.into_iter().map(|(_, idx)| idx).collect()) + Some(all_matches.into_iter().map(|candidate| candidate.index).collect()) } /// Search room members with optional pre-computed sort data @@ -1184,6 +1159,36 @@ mod tests { assert_eq!(priorities, vec![0, 1, 2, 3, 4]); } + #[test] + fn test_top_k_prefers_better_tie_breaker_when_priorities_match() { + let max_results = 2; + let mut heap = BinaryHeap::with_capacity(max_results); + + push_top_candidate( + &mut heap, + max_results, + SearchCandidate::new_for_test(5, 2, 0, "zoe", 10), + ); + push_top_candidate( + &mut heap, + max_results, + SearchCandidate::new_for_test(5, 2, 0, "mike", 11), + ); + push_top_candidate( + &mut heap, + max_results, + SearchCandidate::new_for_test(5, 1, 0, "alice", 12), + ); + + let kept_names: Vec<_> = heap + .into_sorted_vec() + .into_iter() + .map(|candidate| candidate.sort_key.sort_key) + .collect(); + + assert_eq!(kept_names, vec!["alice".to_owned(), "mike".to_owned()]); + } + #[test] fn test_when_grapheme_search_is_used() { // This test demonstrates when grapheme_starts_with is actually called diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index 2ab7a02e9..3e4eb442f 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -852,6 +852,7 @@ fn is_whitespace(grapheme: &str) -> bool { grapheme.chars().all(char::is_whitespace) } +#[cfg(test)] fn popup_item_highlight_color( idx: usize, keyboard_focus_index: Option, diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 7ca74670c..95476819c 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -157,15 +157,43 @@ enum MentionSearchState { // Default is derived above; Idle is marked as the default variant // Constants for mention popup height calculations -const DESKTOP_ITEM_HEIGHT: f64 = 32.0; -const MOBILE_ITEM_HEIGHT: f64 = 64.0; -const MOBILE_USERNAME_SPACING: f64 = 0.5; - // Constants for search behavior const DESKTOP_MAX_VISIBLE_ITEMS: usize = 10; const MOBILE_MAX_VISIBLE_ITEMS: usize = 5; const SEARCH_BUFFER_MULTIPLIER: usize = 2; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PopupStatusItemKind { + Loading, + NoMatches, +} + +fn popup_status_item_is_selectable(_kind: PopupStatusItemKind) -> bool { + false +} + +fn member_list_ready_for_mentions(member_count: usize, sync_pending: bool) -> bool { + member_count > 0 && !sync_pending +} + +fn member_data_change_requires_popup_refresh( + previous_member_count: usize, + current_member_count: usize, + previous_sync_pending: bool, + current_sync_pending: bool, + search_state: &MentionSearchState, +) -> bool { + let member_count_changed = + current_member_count > 0 && current_member_count != previous_member_count; + let sync_just_completed = previous_sync_pending && !current_sync_pending; + + (member_count_changed || sync_just_completed) + && matches!( + search_state, + MentionSearchState::WaitingForMembers { .. } | MentionSearchState::Searching { .. } + ) +} + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -539,7 +567,7 @@ impl Widget for MentionableTextInput { // Best practice: Always check Scope first to get current context // Scope represents the current widget context as passed down from parents - let (scope_room_id, scope_member_count) = { + let (scope_room_id, scope_member_count, scope_sync_pending) = { let room_props = scope .props .get::() @@ -549,9 +577,20 @@ impl Widget for MentionableTextInput { .as_ref() .map(|members| members.len()) .unwrap_or(0); - (room_props.room_name_id.room_id().clone(), member_count) + ( + room_props.room_name_id.room_id().clone(), + member_count, + room_props.room_members_sync_pending, + ) }; + self.refresh_popup_for_member_change( + cx, + scope, + scope_member_count, + scope_sync_pending, + ); + // Check search channel on every frame if we're searching if let MentionSearchState::Searching { .. } = &self.search_state { if let Event::NextFrame(_) = event { @@ -752,26 +791,6 @@ impl Widget for MentionableTextInput { } } - // Check if we were waiting for members and they're now available - // When members arrive, always update regardless of focus state - // update_user_list will handle popup visibility based on current focus - if let MentionSearchState::WaitingForMembers { - trigger_position: _, - pending_search_text, - } = &self.search_state - { - let room_props = scope - .props - .get::() - .expect("RoomScreenProps should be available in scope"); - - if let Some(room_members) = &room_props.room_members { - if !room_members.is_empty() { - let search_text = pending_search_text.clone(); - self.update_user_list(cx, &search_text, scope); - } - } - } } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { @@ -803,6 +822,69 @@ impl Widget for MentionableTextInput { } impl MentionableTextInput { + fn active_search_text(&self) -> Option { + match &self.search_state { + MentionSearchState::WaitingForMembers { + pending_search_text, + .. + } => Some(pending_search_text.clone()), + MentionSearchState::Searching { search_text, .. } => Some(search_text.clone()), + _ => None, + } + } + + fn add_popup_status_item( + &mut self, + cx: &Cx, + widget: WidgetRef, + item_kind: PopupStatusItemKind, + ) { + if popup_status_item_is_selectable(item_kind) { + self.cmd_text_input.add_item(cx, widget); + } else { + self.cmd_text_input.add_unselectable_item(cx, widget); + } + } + + fn refresh_popup_for_member_change( + &mut self, + cx: &mut Cx, + scope: &mut Scope, + current_member_count: usize, + current_sync_pending: bool, + ) { + let previous_member_count = self.last_member_count; + let previous_sync_pending = self.last_sync_pending; + self.last_member_count = current_member_count; + self.last_sync_pending = current_sync_pending; + + if !member_data_change_requires_popup_refresh( + previous_member_count, + current_member_count, + previous_sync_pending, + current_sync_pending, + &self.search_state, + ) { + return; + } + + if self.pending_popup_cleanup { + return; + } + + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if !cx.has_key_focus(text_input_area) { + return; + } + + let Some(search_text) = self.active_search_text() else { + return; + }; + + self.last_search_text = None; + self.update_user_list(cx, &search_text, scope); + } + /// Check if currently in any form of search mode fn is_searching(&self) -> bool { matches!( @@ -850,7 +932,7 @@ impl MentionableTextInput { cx: &mut Cx, search_text: &str, room_props: &RoomScreenProps, - is_desktop: bool, + _is_desktop: bool, ) -> bool { // Don't show @room option in direct messages if false /* TODO: add is_direct_room to RoomScreenProps */ { @@ -913,12 +995,6 @@ impl MentionableTextInput { ); } - // Apply layout and height styling based on device type - let new_height = if is_desktop { - DESKTOP_ITEM_HEIGHT - } else { - MOBILE_ITEM_HEIGHT - }; // Layout is set in the DSL template (defaults to desktop layout). // TODO: add mobile-specific layout when adaptive layout is implemented. @@ -933,7 +1009,7 @@ impl MentionableTextInput { cx: &mut Cx, results: &[usize], user_items_limit: usize, - is_desktop: bool, + _is_desktop: bool, room_props: &RoomScreenProps, ) -> usize { let mut items_added = 0; @@ -1011,14 +1087,14 @@ impl MentionableTextInput { /// Update popup visibility and layout based on current state fn update_popup_visibility(&mut self, cx: &mut Cx, scope: &mut Scope, has_items: bool) { - let mut popup = self.cmd_text_input.view(cx, ids!(popup)); + let popup = self.cmd_text_input.view(cx, ids!(popup)); // Get current state from props let room_props = scope .props .get::() .expect("RoomScreenProps should be available in scope"); - let members_sync_pending = false; // TODO: add room_members_sync_pending to RoomScreenProps + let members_sync_pending = room_props.room_members_sync_pending; let members_available = room_props .room_members .as_ref() @@ -1386,11 +1462,11 @@ impl MentionableTextInput { // This gives visual feedback that more members may be loading // IMPORTANT: Don't call show_loading_indicator here as it calls clear_items() // which would remove the user list we just added - if false /* TODO: add room_members_sync_pending to RoomScreenProps */ { + if room_props.room_members_sync_pending { // Add loading indicator widget without clearing existing items if let Some(ptr) = self.loading_indicator { let loading_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); - self.cmd_text_input.add_item(cx, loading_item.clone()); + self.add_popup_status_item(cx, loading_item.clone(), PopupStatusItemKind::Loading); self.loading_indicator_ref = Some(loading_item.clone()); // Start the loading animation @@ -1459,9 +1535,17 @@ impl MentionableTextInput { } else { MOBILE_MAX_VISIBLE_ITEMS }; + let members_sync_pending = room_props.room_members_sync_pending; + let cached_member_count = room_props + .room_members + .as_ref() + .map(|members| members.len()) + .unwrap_or(0); let cached_members = match &room_props.room_members { - Some(members) if !members.is_empty() => { + Some(members) + if member_list_ready_for_mentions(cached_member_count, members_sync_pending) => + { // Members available, continue to search members.clone() } @@ -1470,10 +1554,11 @@ impl MentionableTextInput { self.search_state, MentionSearchState::WaitingForMembers { .. } ); + let needs_local_member_fetch = cached_member_count == 0 && !members_sync_pending; self.cancel_active_search(); - if !already_waiting { + if !already_waiting && needs_local_member_fetch { submit_async_request(MatrixRequest::GetRoomMembers { timeline_kind: crate::sliding_sync::TimelineKind::MainRoom { room_id: room_props.room_name_id.room_id().clone() }, memberships: RoomMemberships::JOIN, @@ -1637,33 +1722,23 @@ impl MentionableTextInput { /// Shows the loading indicator when waiting for initial members to be loaded fn show_loading_indicator(&mut self, cx: &mut Cx) { - // Check if we already have a loading indicator displayed - // Avoid recreating it on every call, which would prevent animation from playing - if let Some(ref existing_indicator) = self.loading_indicator_ref { - // Already showing, just ensure animation is running - existing_indicator - .bouncing_dots(cx, ids!(loading_animation)) - .start_animation(cx); - cx.new_next_frame(); - return; - } - - // Clear old items before creating new loading indicator + // Rebuild the popup body every time. Mention search can request loading twice + // within the same actions batch (`should_build_items` and `Changed`), and the + // second pass may have already cleared the list before we get here again. self.cmd_text_input.clear_items(cx); - // Create fresh loading indicator widget - let Some(ptr) = self.loading_indicator else { - return; + let loading_item = if let Some(existing_indicator) = self.loading_indicator_ref.clone() { + existing_indicator + } else { + let Some(ptr) = self.loading_indicator else { + return; + }; + crate::widget_ref_from_live_ptr(cx, Some(ptr)) }; - let loading_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); - // Start the loading animation + self.add_popup_status_item(cx, loading_item.clone(), PopupStatusItemKind::Loading); + self.loading_indicator_ref = Some(loading_item.clone()); loading_item.bouncing_dots(cx, ids!(loading_animation)).start_animation(cx); - - // Now that the widget is in the UI tree, start the loading animation - loading_item - .bouncing_dots(cx, ids!(loading_animation)) - .start_animation(cx); cx.new_next_frame(); // Setup popup dimensions for loading state @@ -1696,11 +1771,10 @@ impl MentionableTextInput { let no_matches_item = crate::widget_ref_from_live_ptr(cx, Some(ptr)); // Add the no matches indicator to the popup - self.cmd_text_input.add_item(cx, no_matches_item); + self.add_popup_status_item(cx, no_matches_item, PopupStatusItemKind::NoMatches); self.loading_indicator_ref = None; // Setup popup dimensions for no matches state - let mut popup = self.cmd_text_input.view(cx, ids!(popup)); let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); // Ensure header is visible @@ -1790,7 +1864,7 @@ impl MentionableTextInput { self.reset_search_state(cx); // Get popup and header view references - let mut popup = self.cmd_text_input.view(cx, ids!(popup)); + let popup = self.cmd_text_input.view(cx, ids!(popup)); let header_view = self.cmd_text_input.view(cx, ids!(popup.header_view)); // Force hide header view - necessary when handling deletion operations @@ -1941,3 +2015,85 @@ impl MentionableTextInputRef { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn popup_status_items_are_never_selectable() { + assert!(!popup_status_item_is_selectable(PopupStatusItemKind::Loading)); + assert!(!popup_status_item_is_selectable(PopupStatusItemKind::NoMatches)); + } + + #[test] + fn member_list_waits_while_sync_is_pending() { + assert!(!member_list_ready_for_mentions(1, true)); + assert!(!member_list_ready_for_mentions(0, false)); + assert!(member_list_ready_for_mentions(3, false)); + } + + #[test] + fn member_count_change_refreshes_active_search() { + assert!(member_data_change_requires_popup_refresh( + 2, + 5, + false, + false, + &MentionSearchState::Searching { + trigger_position: 0, + search_text: "al".to_owned(), + receiver: std::sync::mpsc::channel().1, + accumulated_results: Vec::new(), + search_id: 1, + cancel_token: Arc::new(AtomicBool::new(false)), + }, + )); + } + + #[test] + fn member_count_change_refreshes_waiting_search_when_members_arrive() { + assert!(member_data_change_requires_popup_refresh( + 0, + 3, + false, + true, + &MentionSearchState::WaitingForMembers { + trigger_position: 0, + pending_search_text: "al".to_owned(), + }, + )); + } + + #[test] + fn unchanged_member_count_does_not_refresh_popup() { + assert!(!member_data_change_requires_popup_refresh( + 3, + 3, + false, + false, + &MentionSearchState::Searching { + trigger_position: 0, + search_text: "al".to_owned(), + receiver: std::sync::mpsc::channel().1, + accumulated_results: Vec::new(), + search_id: 1, + cancel_token: Arc::new(AtomicBool::new(false)), + }, + )); + } + + #[test] + fn sync_completion_refreshes_popup_even_without_member_count_change() { + assert!(member_data_change_requires_popup_refresh( + 1, + 1, + true, + false, + &MentionSearchState::WaitingForMembers { + trigger_position: 0, + pending_search_text: "al".to_owned(), + }, + )); + } +} From 191f90096cf5850f3485ca12079a3ec5a738de9c Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 01:17:42 +0800 Subject: [PATCH 077/283] Add CLAUDE.md, DESIGN.md, and project-level spec - CLAUDE.md: Project instructions for Claude Code sessions, references DESIGN.md and specs as required reading, critical rules for Makepad 2.0 - DESIGN.md: Architecture overview, three-layer design, module organization, technology stack, key patterns - specs/project.spec.md: Project-level constraints (no cargo fmt, Makepad 2.0 syntax, dynamic widget rules), decisions, forbidden actions - specs/task-mention-user.spec.md: Updated to reflect actual implementation (not migration), 15 BDD scenarios, lint score 100% Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 72 +++++++++++++++++++ DESIGN.md | 115 +++++++++++++++++++++++++++++ specs/project.spec.md | 46 ++++++++++++ specs/task-mention-user.spec.md | 124 ++++++++++++++++---------------- 4 files changed, 294 insertions(+), 63 deletions(-) create mode 100644 CLAUDE.md create mode 100644 DESIGN.md create mode 100644 specs/project.spec.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..9c1a14e37 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# Robrix2 — Claude Code Instructions + +## Required Reading + +Before starting any task, read these documents: + +1. **[DESIGN.md](DESIGN.md)** — Architecture overview, module organization, technology stack +2. **[specs/project.spec.md](specs/project.spec.md)** — Project-level constraints, decisions, and forbidden actions +3. **[AGENTS.md](AGENTS.md)** — Makepad 2.0 patterns and DSL syntax reference + +## Critical Rules + +### Do NOT run `cargo fmt` +This project does not use rustfmt. Formatting changes create noisy diffs and break existing code style. + +### Do NOT commit or create PRs without user testing +Always let the user test changes before committing. Present what's ready for testing, wait for confirmation. + +### Makepad 2.0 Syntax (NOT 1.x) +- Use `script_mod!` (NOT `live_design!`) +- Use `#[derive(Script, ScriptHook, Widget)]` (NOT `Live, LiveHook`) +- Use `:=` for named children (NOT `=`) +- Use `+:` to merge properties (NOT `:` which replaces) +- Use `script_apply_eval!` for runtime updates (NOT `apply_over` + `live!`) + +### Dynamic Widget State Changes +`script_apply_eval!` does NOT work on widgets created via `widget_ref_from_live_ptr()` (ScriptObject is ZERO). Use Animator + shader instance variables instead: + +```rust +// In DSL template: +draw_bg +: { selected: instance(0.0) } +animator: Animator { highlight: { ... apply: { draw_bg: { selected: 1.0 } } } } + +// In Rust: +view.animator_cut(cx, ids!(highlight.on)); +``` + +### Async Matrix Operations +Always use `submit_async_request(MatrixRequest::*)`. Do NOT spawn raw tokio tasks for Matrix API calls. + +## Build & Test + +```bash +# Build +cargo build + +# Run +cargo run + +# Run with hot reload +cargo run -- --hot + +# Tests (limited — mostly manual testing) +cargo test +``` + +## Project Structure + +See [DESIGN.md](DESIGN.md) for full module organization. + +Key entry points: +- `src/app.rs` — Root app, global state +- `src/sliding_sync.rs` — Matrix client, sync +- `src/home/room_screen.rs` — Timeline rendering +- `src/shared/mentionable_text_input.rs` — @mention system + +## Specs + +Task specs live in `specs/` and inherit from `specs/project.spec.md`: +- `specs/task-mention-user.spec.md` — @mention autocomplete feature + +Use `agent-spec parse` and `agent-spec lint --min-score 0.7` to validate specs. diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..cc5628b9a --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,115 @@ +# Robrix Design Document + +## Overview + +Robrix is a multi-platform Matrix chat client written in pure Rust using the Makepad UI framework and Project Robius application development framework. It targets macOS, Windows, Linux, Android, iOS, and OpenHarmony. + +## Architecture + +### Three-Layer Architecture + +``` ++─────────────────────────────────────────────────────+ +│ UI Layer │ +│ Makepad script_mod! DSL, Widgets, MatchEvent │ +│ (app.rs, home/, shared/, room/, login/, settings/) │ ++─────────────────────────────────────────────────────+ + │ + Actions / Scope + │ ++─────────────────────────────────────────────────────+ +│ Matrix Protocol Layer │ +│ sliding_sync.rs — async client, auth, timelines │ +│ space_service_sync.rs — space hierarchy │ +│ submit_async_request() → tokio background tasks │ ++─────────────────────────────────────────────────────+ + │ + Cx::post_action / MPSC + │ ++─────────────────────────────────────────────────────+ +│ Persistence & Cache Layer │ +│ persistence/ — session, app state, window geometry │ +│ avatar_cache, media_cache, user_profile_cache │ +│ account_manager — multi-account switching │ ++─────────────────────────────────────────────────────+ +``` + +### Key Components + +| Component | File(s) | Responsibility | +|-----------|---------|----------------| +| App | `app.rs` | Root state, event dispatch, modal management | +| Sliding Sync | `sliding_sync.rs` | Matrix client lifecycle, room sync, timeline subscriptions | +| Room Screen | `home/room_screen.rs` | Timeline rendering, message display, pagination | +| Rooms List | `home/rooms_list.rs` | Room list with categories (invited, direct, regular) | +| Room Input Bar | `room/room_input_bar.rs` | Message composition, replies, mentions | +| Mentionable Text Input | `shared/mentionable_text_input.rs` | @mention autocomplete with background search | +| Command Text Input | `shared/command_text_input.rs` | Generic popup/autocomplete infrastructure | +| HTML/Plaintext | `shared/html_or_plaintext.rs` | Message rendering with Matrix HTML support | + +### Technology Stack + +- **UI Framework**: Makepad 2.0 (`script_mod!` DSL, `Script`/`ScriptHook` derives) +- **Matrix SDK**: `matrix-sdk` with sliding sync, E2E encryption, SQLite storage +- **Async Runtime**: Tokio +- **Serialization**: Serde (JSON for persistence, RON for legacy) + +### UI Patterns (Makepad 2.0) + +- **Widget DSL**: `script_mod!` blocks define widget trees with Splash syntax +- **Named children**: Use `:=` operator (NOT `=`) for addressable widgets +- **Property merge**: Use `+:` to extend inherited properties, `:` to replace +- **Event flow**: `handle_event` → `MatchEvent::handle_actions` → widget action queries +- **State changes on dynamic widgets**: Use Animator + shader instance variables (NOT `script_apply_eval!` which fails on `widget_ref_from_live_ptr()` widgets due to `ScriptObject::ZERO`) +- **Runtime property limits**: `script_apply_eval!` cannot use DSL constants (`Right`, `Fit`, `Align`) — bake into templates or use `#(rust_expr)` interpolation + +### Async Communication Pattern + +``` +UI Thread Background Thread + │ │ + ├── submit_async_request() ──────────►│ MatrixRequest::* + │ │ + │◄── Cx::post_action() ──────────────┤ Result action + │ │ + ├── handle_actions() processes result │ +``` + +### Persistence + +- Session data: `~/.local/share/org.robius.robrix//persistent_state/` +- App state: `latest_app_state.json` (dock layout, open rooms, selected room) +- Window geometry: `window_geom_state.json` + +## Module Organization + +``` +src/ +├── app.rs # Root app, modals, global state +├── sliding_sync.rs # Matrix client, sync, requests +├── space_service_sync.rs # Space hierarchy +├── cpu_worker.rs # Background CPU tasks +├── home/ # Main UI screens +│ ├── room_screen.rs # Timeline + message display +│ ├── rooms_list.rs # Room list sidebar +│ ├── main_desktop_ui.rs # Desktop dock layout +│ ├── home_screen.rs # Adaptive desktop/mobile +│ └── ... +├── shared/ # Reusable widgets +│ ├── mentionable_text_input.rs # @mention system +│ ├── command_text_input.rs # Popup autocomplete +│ ├── html_or_plaintext.rs # Message rendering +│ ├── avatar.rs # Avatar display +│ └── ... +├── room/ # Room-specific logic +│ ├── room_input_bar.rs # Message input +│ ├── member_search.rs # Member search algorithm +│ └── ... +├── login/ # Authentication +├── logout/ # Session cleanup +├── settings/ # User preferences +├── persistence/ # State storage +├── profile/ # User profiles +├── i18n.rs # Internationalization +└── utils.rs # Shared utilities +``` diff --git a/specs/project.spec.md b/specs/project.spec.md new file mode 100644 index 000000000..41ab5c4d5 --- /dev/null +++ b/specs/project.spec.md @@ -0,0 +1,46 @@ +spec: project +name: "Robrix2 — Matrix Chat Client on Makepad 2.0" +tags: [makepad, matrix, rust, gui] +--- + +## Intent + +Robrix is a multi-platform Matrix chat client built with Makepad 2.0 and matrix-sdk. This project spec defines the shared constraints, coding standards, and technical decisions that all task specs inherit. + +## Constraints + +- All code must compile with `cargo build` on the `feature/mention-user-migration` branch (or `main` after merge) +- All UI widgets must use Makepad 2.0 `script_mod!` DSL syntax — do NOT use Makepad 1.x `live_design!` syntax +- Named widget children must use `:=` operator, NOT `=` +- Property overrides on inherited widgets must use `+:` merge operator to preserve parent properties +- Do NOT use `cargo fmt` — the project does not enforce rustfmt and formatting changes create noisy diffs +- Do NOT add new cargo dependencies without explicit approval in the task spec +- Do NOT use `.unwrap()` on user-facing code paths — use proper error handling with `anyhow` or pattern matching +- Async Matrix operations must go through `submit_async_request(MatrixRequest::*)` — do NOT spawn raw tokio tasks for Matrix API calls +- Widget state changes on dynamically-created widgets (via `widget_ref_from_live_ptr()`) must use Animator + shader instance variables, NOT `script_apply_eval!` (which silently fails due to `ScriptObject::ZERO`) +- `script_apply_eval!` must NOT use DSL constants (`Right`, `Down`, `Fit`, `Fill`, `Align`, `Inset`, `MouseCursor`) — these are not available at runtime scope +- All `draw_bg` property modifications must use `+:` merge syntax, NOT `:` replace syntax, to avoid losing shader/border/animation properties + +## Decisions + +- UI Framework: Makepad 2.0 with `script_mod!` DSL (fork: `kevinaboos/makepad`, branch: `stack_nav_improvements`) +- Matrix SDK: `matrix-sdk` with sliding sync, E2E encryption, SQLite storage +- Async runtime: Tokio +- State persistence: JSON serialization via serde to `~/.local/share/org.robius.robrix/` +- Widget template instantiation: `crate::widget_ref_from_live_ptr(cx, Some(ptr))` for creating widgets from `#[live] Option` fields +- Derive macros: `#[derive(Script, ScriptHook, Widget)]` for widget structs (NOT `Live`/`LiveHook`) +- DSL property syntax: whitespace-separated (no commas), `Inset{...}` for margins/padding, `Align{...}` for alignment +- Hex colors with letter 'e': use `#x` prefix (e.g., `#x1E90FF`) +- Background CPU work: `cpu_worker::spawn_cpu_job(cx, CpuJob::*)` via `cx.spawn_thread()` +- Dock state restoration: programmatic tab recreation via `close_all_tabs()` + `focus_or_create_tab()`, NOT `Dock.load_state()` (which corrupts DrawList references) + +## Boundaries + +### Forbidden +- Do NOT run `cargo fmt` on any files +- Do NOT modify `Cargo.toml` dependencies without task-level approval +- Do NOT use `live_design!` macro (Makepad 1.x syntax) +- Do NOT use `apply_over(cx, live!{...})` — use `script_apply_eval!(cx, widget, {...})` for runtime updates +- Do NOT call `Dock.load_state()` during event handling (causes DrawList corruption) +- Do NOT commit code that doesn't pass `cargo build` +- Do NOT create PRs without user testing and approval first diff --git a/specs/task-mention-user.spec.md b/specs/task-mention-user.spec.md index db120b8c6..8ea08f280 100644 --- a/specs/task-mention-user.spec.md +++ b/specs/task-mention-user.spec.md @@ -1,54 +1,52 @@ spec: task -name: "Migrate @mention User Feature from Makepad 1.0 to 2.0" -tags: [feature, migration, mention, makepad-2.0] +name: "@mention User Autocomplete" +inherits: project +tags: [feature, mention, ui, matrix] estimate: 3d --- ## Intent -Migrate the complete @mention user feature from the robrix (Makepad 1.0) project to robrix2 (Makepad 2.0). The 1.0 project at `/Users/zhangalex/Work/Projects/FW/robius/robrix` has a fully working mention system (~4600 lines) covering autocomplete popup, background member search, mention insertion as markdown links, mention metadata in sent messages, and pill-style rendering in the timeline. The robrix2 project currently has only a placeholder `MentionableTextInput` with no actual mention functionality. The existing `CommandTextInput` popup infrastructure in robrix2 provides the autocomplete foundation to build upon. +Provide @mention autocomplete functionality in the message input bar. When a user types `@` followed by text, a popup appears showing matching room members with avatars and display names. Selecting a member inserts a markdown mention link that includes Matrix mention metadata when the message is sent. The system supports background search for responsiveness in large rooms (1000+ members) and renders received mentions as colored pills in the timeline. ## Decisions -- Reuse `CommandTextInput` as the popup base widget for mention autocomplete — do not recreate popup from scratch -- Port `member_search.rs` algorithm as-is (pure Rust, no UI framework changes needed) -- Port `cpu_worker.rs` for background thread search execution via `cx.spawn_thread()` -- Mention insertion format: `[{username}](matrixUri)` markdown link with trailing space -- Track mentions via `possible_mentions: BTreeMap` and `possible_room_mention: bool` -- Extract real mentions from message text using `matrix_sdk::ruma::events::Mentions` before sending -- Enable existing `MatrixLinkPill` rendering in `html_or_plaintext.rs` by uncommenting Matrix URI parsing -- `@room` mention availability controlled by user's room power levels via `MentionableTextInputAction::PowerLevelsUpdated` +- Popup infrastructure: reuse `CommandTextInput` as the base widget via `#[deref]` composition - Trigger character: `@` preceded by whitespace or start of text - State machine: `Idle` → `WaitingForMembers` → `Searching` → `Idle` (also `JustCancelled` on ESC) -- Search results streamed in batches of 10 via MPSC channel for progressive UI updates -- Cancellation via `Arc` token for graceful background search abort -- Makepad 2.0 syntax: `script_mod!` DSL, `#[derive(Script, ScriptHook, Widget)]`, `script_apply_eval!` for runtime property updates +- Search execution: background thread via `cpu_worker::spawn_cpu_job()` with MPSC channel streaming results in batches of 10 +- Search cancellation: `Arc` token for graceful abort +- Mention insertion format: `[{username}](matrixUri)` markdown link with trailing space +- Mention tracking: `possible_mentions: BTreeMap` for users, `possible_room_mention: bool` for @room +- Mention extraction: scan final message text for tracked mention patterns before sending via `Mentions` struct +- Highlight rendering: Animator states with `selected: instance(0.0)` shader variable + `animator_cut(cx, ids!(highlight.on/off))` — NOT `script_apply_eval!` (fails on dynamic widgets) +- Timeline pill rendering: `MatrixLinkPill` widget with avatar + display name, current-user mentions in red `#d91b38` +- @room availability: controlled by user's room power levels via `MentionableTextInputAction::PowerLevelsUpdated` +- Popup styling: rounded corners (6px), border (#ddd), shadow, compact items (36px height) +- Username display: bold text, user_id right-aligned in lighter gray ## Boundaries ### Allowed Changes - src/shared/mentionable_text_input.rs +- src/shared/command_text_input.rs - src/shared/html_or_plaintext.rs -- src/room/member_search.rs (new) -- src/cpu_worker.rs (new) +- src/room/member_search.rs +- src/cpu_worker.rs - src/room/mod.rs - src/lib.rs - src/home/room_screen.rs -- src/home/editing_pane.rs ### Forbidden -- Do not modify `CommandTextInput` internals — extend via composition only -- Do not add new cargo dependencies — use existing `matrix_sdk`, `unicode_segmentation`, `ruma` crates -- Do not change the message sending pipeline in `sliding_sync.rs` -- Do not modify the `RoomInputBar` DSL layout — the `mentionable_text_input` widget slot already exists +- Do not modify the message sending pipeline in `sliding_sync.rs` +- Do not modify `RoomInputBar` DSL layout — the `mentionable_text_input` widget slot already exists ## Out of Scope - Vertical alignment of inline MatrixLinkPill with surrounding text (known Makepad limitation) -- Mention extraction during message editing (editing_pane.rs has a TODO for this) +- Mention extraction during message editing (editing_pane.rs has a TODO) - Custom pill colors per user -- Mention notification sound/vibration -- Desktop vs mobile adaptive layout for popup items (use single layout initially) +- Desktop vs mobile adaptive popup layout (single desktop layout used) ## Completion Criteria @@ -66,10 +64,17 @@ Scenario: Search filters members by typed text Then the popup shows "Alice" and "Alex" And the popup does not show "Bob" +Scenario: Keyboard arrow keys highlight items + Test: manual_test_arrow_key_highlight + Given the mention popup is showing results + When the user presses ArrowDown + Then the next item is visually highlighted with a blue background + And the previous item returns to default background + Scenario: Selecting a mention inserts markdown link Test: manual_test_mention_insert_markdown Given the mention popup is showing results - When the user selects "Alice" from the popup + When the user selects "Alice" via Enter or mouse click Then the input text contains "[Alice](matrix:u/alice:example.com) " And the popup closes And the cursor is positioned after the trailing space @@ -82,71 +87,64 @@ Scenario: ESC dismisses mention popup And typing another "@" immediately does not re-open the popup (JustCancelled state) Scenario: Sent message includes Mentions metadata - Test: manual_test_mentions_metadata_in_sent_message - Given the user inserted a mention for "@alice:example.com" in the input + Test: manual_test_mentions_metadata + Given the user inserted a mention for "@alice:example.com" When the user sends the message - Then the `RoomMessageEventContent` includes `Mentions` with `user_ids` containing "alice:example.com" + Then the RoomMessageEventContent includes Mentions with user_ids containing "alice:example.com" Scenario: @room mention requires power level Test: manual_test_at_room_power_level Given the user has room notification power level When the user types "@room" and selects the @room item - Then the message includes `Mentions` with `room: true` + Then the message includes Mentions with room: true -Scenario: @room hidden when user lacks power level - Test: manual_test_at_room_hidden_without_power +Scenario: @room hidden without power level + Test: manual_test_at_room_hidden Given the user does not have room notification power level When the user types "@" Then the popup does not show the @room option -Scenario: Member search runs in background without UI freeze - Test: manual_test_background_search_responsive +Scenario: Background search keeps UI responsive + Test: manual_test_background_search Given a room with 1000+ members When the user types "@a" Then the UI remains responsive during search - And results appear progressively as they are found + And results appear progressively Scenario: MatrixLinkPill renders for received mentions - Test: manual_test_pill_renders_in_timeline - Given a message containing an HTML mention link `Alice` + Test: manual_test_pill_renders + Given a message containing an HTML mention link When the message is displayed in the timeline - Then a pill-style widget renders with avatar and display name "Alice" + Then a pill-style widget renders with avatar and display name Scenario: Current user mention renders with red background - Test: manual_test_current_user_pill_red + Test: manual_test_current_user_pill Given a message mentions the current logged-in user When the message is displayed in the timeline - Then the mention pill renders with red background color `#d91b38` + Then the mention pill renders with red background color -Scenario: Empty search shows no-matches indicator - Test: manual_test_no_matches_indicator +Scenario: No matches shows indicator + Test: manual_test_no_matches Given a room with members "Alice", "Bob" When the user types "@zzzzzzz" Then the popup shows "No matching users found" indicator -Scenario: Member search handles Unicode names - Test: manual_test_unicode_member_search - Given a room with a member named "张三" (Zhang San) - When the user types "@张" - Then the popup shows "张三" in the results - -Scenario: Mention popup handles room with no loaded members gracefully - Test: manual_test_no_members_loaded_error - Given a room where members have not been fetched yet +Scenario: Popup shows loading while members sync + Test: manual_test_loading_during_sync + Given a room where members have not been synced yet When the user types "@" Then the popup shows a loading indicator - And the popup does not crash or show empty results prematurely + And after sync completes the member list appears -Scenario: Stale search results are discarded after cancellation +Scenario: Stale search results are discarded Test: manual_test_stale_search_discarded - Given a background member search is in progress for "@al" - When the user presses ESC to cancel and then types "@bo" - Then results from the first search "@al" are discarded - And only results matching "@bo" are displayed - -Scenario: Member search compiles and runs correctly in Makepad 2.0 - Test: manual_test_member_search_ported - Given the `member_search.rs` module is ported from robrix 1.0 - When `cargo build` is executed - Then the project compiles without errors - And `search_room_members_streaming_with_sort()` produces correct results + Given a background search is in progress for "@al" + When the user presses ESC and then types "@bo" + Then results from "@al" search are discarded + And only "@bo" results are displayed + +Scenario: Unicode member names are searchable + Test: manual_test_unicode_search + Given a room with a member named "Zhang San" + When the user types the first character of their name + Then the popup shows the member in results From 97faa8ca39c6e25ec3398f2a4957bc828af39803 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 01:53:37 +0800 Subject: [PATCH 078/283] Add Makepad 2.0 skills reference table to CLAUDE.md Maps common situations to the correct skill to invoke, plus highlights project-specific pitfalls #40-#44. Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 9c1a14e37..ab6ccc264 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,30 @@ view.animator_cut(cx, ids!(highlight.on)); ### Async Matrix Operations Always use `submit_async_request(MatrixRequest::*)`. Do NOT spawn raw tokio tasks for Matrix API calls. +## Makepad 2.0 Skills + +When working on Makepad UI code, **always invoke the relevant Makepad 2.0 skill** before writing or debugging: + +| Situation | Skill to Use | +|-----------|-------------| +| UI not rendering, widget invisible, click not working | `makepad-2.0-troubleshooting` (Pitfalls #1-#44) | +| DSL syntax questions, `script_mod!`, property system | `makepad-2.0-dsl` | +| Layout issues (width/height/flow/align) | `makepad-2.0-layout` | +| Hover effects, state transitions, animation | `makepad-2.0-animation` | +| Shader code, `draw_bg`, `Sdf2d`, pixel functions | `makepad-2.0-shaders` | +| Event handling, `handle_event`, actions, `script_apply_eval!` | `makepad-2.0-events` | +| Widget catalog (View, Button, Label, etc.) | `makepad-2.0-widgets` | +| Migrating from Makepad 1.x to 2.0 | `makepad-2.0-migration` | +| App structure, `app_main!`, `MatchEvent` | `makepad-2.0-app-structure` | +| Theme system, colors, fonts | `makepad-2.0-theme` | + +**Key pitfalls from this project** (in `makepad-2.0-troubleshooting`): +- **#40**: `script_apply_eval!` fails on dynamic widgets — use Animator instead +- **#41**: DSL constants (`Right`, `Fit`, `Align`) unavailable at runtime in `script_apply_eval!` +- **#42**: `Dock.load_state()` corrupts DrawList references +- **#43**: Named children: `=` vs `:=` in `script_mod!` +- **#44**: `draw_bg:` replaces vs `draw_bg +:` merges + ## Build & Test ```bash From 76f7bc22e4aa6010e337fb9e896ff50a00dffd14 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 02:45:14 +0800 Subject: [PATCH 079/283] Update SPLASH.md with runtime property update limitations Add new section documenting: - script_apply_eval! usage and #(expr) interpolation - DSL constants not available at runtime (Right, Fit, Align, etc.) - Dynamic widgets (widget_ref_from_live_ptr) ignore script_apply_eval - Animator + instance variable pattern as the correct alternative - draw_bg: vs draw_bg +: merge semantics warning - Update comma preference to newline preference Co-Authored-By: Claude Opus 4.6 (1M context) --- SPLASH.md | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/SPLASH.md b/SPLASH.md index 88af44bba..30f507e3a 100644 --- a/SPLASH.md +++ b/SPLASH.md @@ -1,7 +1,7 @@ # Splash Script Manual (Terse AI Reference) -Splash is Makepad's UI scripting language. It is whitespace-delimited, but Robrix prefers either newlines or commas to separate properties, for readability's sake. -**Please always use newlines or commas to separate properties, not just whitespace.** +Splash is Makepad's UI scripting language. It is whitespace-delimited. Robrix uses newlines to separate properties for readability. +**Please use newlines to separate properties. Commas are tolerated but newlines are preferred.** **Do NOT use `Root{}` or `Window{}`** — those are host-level wrappers handled externally. Your output is the content inside a body/splash widget. @@ -1552,6 +1552,102 @@ Pow {begin: 0.0, end: 1.0} Bezier {cp0: 0.0, cp1: 0.0, cp2: 1.0, cp3: 1.0} ``` +## Runtime Property Updates (`script_apply_eval!`) + +From Rust code, use `script_apply_eval!` to patch widget properties at runtime: + +```rust +let color = vec4(1.0, 0.0, 0.0, 1.0); +let height = 36.0_f64; +script_apply_eval!(cx, my_widget, { + draw_bg +: { color: #(color) } + height: #(height) +}); +``` + +Use `#(rust_expr)` to interpolate Rust values into the script. + +### What works in `script_apply_eval!` +- Numeric values: `height: #(h)`, `spacing: 10` +- Hex colors: `draw_bg +: { color: #ff0000 }` +- Rust expression interpolation: `#(my_vec4_variable)` +- Property paths: `draw_bg +: { ... }`, `draw_text +: { ... }` + +### What does NOT work in `script_apply_eval!` + +**DSL constants are NOT available at runtime:** +```rust +// WRONG — these all fail with "variable not found in scope" +script_apply_eval!(cx, item, { + flow: Right // ❌ Right not found + height: Fit // ❌ Fit not found + align: Align{y: 0.5} // ❌ Align not found + cursor: MouseCursor.Hand // ❌ MouseCursor not found +}); + +// CORRECT — bake layout into DSL template, or use numeric values +script_apply_eval!(cx, item, { + height: #(36.0_f64) // ✅ numeric value via interpolation + draw_bg +: { color: #(c) } // ✅ color via interpolation +}); +``` + +**Dynamic widgets ignore `script_apply_eval!` entirely:** + +Widgets created via `widget_ref_from_live_ptr()` (or `cx.with_vm(|vm| WidgetRef::script_from_value(...))`) have `ScriptObject::ZERO` as their script source. All `script_apply_eval!` calls on them silently do nothing. + +To change visual state on dynamic widgets, use **Animator + shader instance variables**: + +``` +// In DSL template definition: +mod.widgets.MyItem = View { + show_bg: true + draw_bg +: { + color: #fff + selected: instance(0.0) + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + sdf.box(0. 0. self.rect_size.x self.rect_size.y 4.0) + let highlight = #x1E90FF30 + sdf.fill(Pal.premul(self.color.mix(highlight self.selected))) + return sdf.result + } + } + animator: Animator { + highlight: { + default: @off + off: AnimatorState { + from: { all: Forward { duration: 0.12 } } + apply: { draw_bg: { selected: 0.0 } } + } + on: AnimatorState { + from: { all: Forward { duration: 0.08 } } + apply: { draw_bg: { selected: 1.0 } } + } + } + } +} +``` + +```rust +// In Rust — toggle state via Animator (works on ALL widgets including dynamic ones): +let view = item.as_view(); +view.animator_cut(cx, ids!(highlight.on)); // highlight on +view.animator_cut(cx, ids!(highlight.off)); // highlight off +``` + +### `draw_bg:` vs `draw_bg +:` + +**Always use `+:` when modifying sub-properties:** +``` +draw_bg +: { color: #f00 } // ✅ merges — keeps border_radius, shader, etc. +draw_bg: { color: #f00 } // ❌ replaces — loses ALL other draw_bg properties +``` + +This applies in both DSL definitions and `script_apply_eval!`. + +--- + ## Theme Variables (prefix: `theme.`) ### Spacing From 2f354d96635c05a5e416a62e29a32273a46b6f4d Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 03:01:48 +0800 Subject: [PATCH 080/283] Fix SPLASH.md comma guidance to match actual code style Robrix codebase uses commas extensively in script_mod! blocks (inherited from Makepad 1.x migration). Commas are treated as whitespace by the Splash tokenizer. Guidance now says to match surrounding code style rather than preferring one over the other. Co-Authored-By: Claude Opus 4.6 (1M context) --- SPLASH.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPLASH.md b/SPLASH.md index 30f507e3a..3c67bb28c 100644 --- a/SPLASH.md +++ b/SPLASH.md @@ -1,7 +1,7 @@ # Splash Script Manual (Terse AI Reference) -Splash is Makepad's UI scripting language. It is whitespace-delimited. Robrix uses newlines to separate properties for readability. -**Please use newlines to separate properties. Commas are tolerated but newlines are preferred.** +Splash is Makepad's UI scripting language. It is whitespace-delimited, but Robrix uses commas or newlines to separate properties for readability (commas are treated as whitespace by the tokenizer). +**Please use commas or newlines to separate properties, matching the surrounding code style.** **Do NOT use `Root{}` or `Window{}`** — those are host-level wrappers handled externally. Your output is the content inside a body/splash widget. From 80f5b5fe9cb13ae6e2fd0813d0c1fddab5aa001b Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 03:32:41 +0800 Subject: [PATCH 081/283] Performance: fix P1/P2/P3 from code review P1: Replace cx.redraw_all() with self.redraw(cx) in mention popup Avoids triggering global redraw for a local popup UI change. P2: Wire precomputed member sort into RoomScreenProps - Add room_members_sort field to RoomScreenProps and TimelineUiState - Compute PrecomputedMemberSort when room members are fetched - Pass through to mention search, eliminating repeated O(n log n) sorting on every @ empty search in large rooms P3: Fix grapheme cache cloning on every trigger detection - Restructure find_mention_trigger_position to update cache first, then borrow from it without cloning - Eliminates O(n) Vec + Vec allocation per keystroke Co-Authored-By: Claude Opus 4.6 (1M context) --- src/home/room_screen.rs | 14 +++++++++++- src/shared/mentionable_text_input.rs | 32 ++++++++++------------------ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 66059b365..64b3b7f44 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2016,6 +2016,7 @@ impl Widget for RoomScreen { timeline_kind: tl.kind.clone(), room_members, room_members_sync_pending: tl.room_members_sync_pending, + room_members_sort: tl.room_members_sort.clone(), room_avatar_url: self.room_avatar_url.clone(), app_service_enabled, app_service_room_bound, @@ -2029,6 +2030,7 @@ impl Widget for RoomScreen { timeline_kind: self.timeline_kind.clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, + room_members_sort: None, room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, @@ -2048,6 +2050,7 @@ impl Widget for RoomScreen { room_name_id: RoomNameId::empty(room_id.clone()), timeline_kind: TimelineKind::MainRoom { room_id }, room_members: None, + room_members_sort: None, room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, @@ -3213,6 +3216,9 @@ impl RoomScreen { tl.room_members_sync_pending = false; tl.awaiting_post_sync_member_refresh = false; } + tl.room_members_sort = Some(Arc::new( + crate::room::member_search::precompute_member_sort(&members) + )); tl.room_members = Some(members); }, TimelineUpdate::MediaFetched(request) => { @@ -4075,7 +4081,8 @@ impl RoomScreen { user_power: UserPowerLevels::all(), // Room members start as None and get populated when fetched from the server room_members: None, - room_members_sync_pending: false, + room_members_sort: None, + room_members_sync_pending: false, awaiting_post_sync_member_refresh: false, // We assume timelines being viewed for the first time haven't been fully paginated. fully_paginated: false, @@ -4479,6 +4486,8 @@ pub struct RoomScreenProps { pub timeline_kind: TimelineKind, pub room_members: Option>>, pub room_members_sync_pending: bool, + /// Pre-computed sort order for room members (for mention search optimization). + pub room_members_sort: Option>, pub room_avatar_url: Option, pub app_service_enabled: bool, pub app_service_room_bound: bool, @@ -4638,6 +4647,9 @@ struct TimelineUiState { /// The list of room members for this room. room_members: Option>>, + /// Pre-computed sort order for room members (for efficient mention search). + room_members_sort: Option>, + /// Whether the initial room-member sync is still in progress for this room. room_members_sync_pending: bool, diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 95476819c..c4db28df6 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -1301,7 +1301,7 @@ impl MentionableTextInput { self.check_search_channel(cx, scope); // Redraw to ensure UI updates are visible - cx.redraw_all(); + self.redraw(cx); } else if self.is_searching() { self.close_mention_popup(cx); } @@ -1483,7 +1483,7 @@ impl MentionableTextInput { self.update_popup_visibility(cx, scope, items_added > 0); // Force immediate redraw to ensure UI updates are visible - cx.redraw_all(); + self.redraw(cx); } /// Updates the mention suggestion list based on search @@ -1612,7 +1612,7 @@ impl MentionableTextInput { }; self.search_results_pending = true; - let precomputed_sort = None /* TODO: add room_members_sort to RoomScreenProps */; + let precomputed_sort = room_props.room_members_sort.clone(); let cancel_token_for_job = cancel_token.clone(); cpu_worker::spawn_cpu_job(cx, CpuJob::SearchRoomMembers(SearchRoomMembersJob { members: cached_members, @@ -1637,27 +1637,17 @@ impl MentionableTextInput { return None; } - // Check cache and rebuild if text changed (performance optimization) - let (text_graphemes_owned, byte_positions) = if let Some((cached_text, cached_graphemes, cached_positions)) = &self.cached_text_analysis { - if cached_text == text { - // Cache hit - use cached data - (cached_graphemes.clone(), cached_positions.clone()) - } else { - // Cache miss - rebuild and update cache - let graphemes_owned: Vec = text.graphemes(true).map(|s| s.to_string()).collect(); - let positions = utils::build_grapheme_byte_positions(text); - self.cached_text_analysis = Some((text.to_string(), graphemes_owned.clone(), positions.clone())); - (graphemes_owned, positions) - } - } else { - // No cache - build and cache + // Ensure cache is up-to-date (rebuild only if text changed) + let needs_rebuild = self.cached_text_analysis.as_ref() + .map_or(true, |(cached_text, _, _)| cached_text != text); + if needs_rebuild { let graphemes_owned: Vec = text.graphemes(true).map(|s| s.to_string()).collect(); let positions = utils::build_grapheme_byte_positions(text); - self.cached_text_analysis = Some((text.to_string(), graphemes_owned.clone(), positions.clone())); - (graphemes_owned, positions) - }; + self.cached_text_analysis = Some((text.to_string(), graphemes_owned, positions)); + } - // Convert owned strings to slices for processing + // Borrow directly from cache — no clone needed + let (_, text_graphemes_owned, byte_positions) = self.cached_text_analysis.as_ref().unwrap(); let text_graphemes: Vec<&str> = text_graphemes_owned.iter().map(|s| s.as_str()).collect(); // Use utility function to convert byte position to grapheme index From 86c1c174ca966d8b31eb518af492c72241dc8f33 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 03:47:59 +0800 Subject: [PATCH 082/283] Fix: clear room_members_sort when closing room to free memory The precomputed sort data (PrecomputedMemberSort) was not cleared alongside room_members when a room is closed, defeating the memory optimization for large rooms. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/home/room_screen.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 64b3b7f44..3bc0c1b87 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -4254,8 +4254,10 @@ impl RoomScreen { room_input_bar_state: room_input_bar.save_state(), }; tl.saved_state = state; - // Clear room_members to avoid wasting memory (in case this room is never re-opened). + // Clear room_members and precomputed sort to avoid wasting memory + // (in case this room is never re-opened). tl.room_members = None; + tl.room_members_sort = None; // Store this Timeline's `TimelineUiState` in the global map of states. TIMELINE_STATES.with_borrow_mut(|ts| ts.insert(tl.kind.clone(), tl)); } From 1d589ba6a4bf1d6b6d12a75be088db8d47691d70 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 03:54:09 +0800 Subject: [PATCH 083/283] Move precompute_member_sort to background thread Avoids UI stall on large rooms when members are first fetched. The sort computation is dispatched as a CpuJob::PrecomputeMemberSort, and the result is posted back via Cx::post_action to update tl.room_members_sort on the UI thread. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cpu_worker.rs | 25 ++++++++++++++++++++++++- src/home/room_screen.rs | 17 +++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/cpu_worker.rs b/src/cpu_worker.rs index 95da8a361..430f5834c 100644 --- a/src/cpu_worker.rs +++ b/src/cpu_worker.rs @@ -7,13 +7,27 @@ use makepad_widgets::{Cx, CxOsApi}; use std::sync::{atomic::AtomicBool, mpsc::Sender, Arc}; use crate::{ - room::member_search::{search_room_members_streaming_with_sort, PrecomputedMemberSort}, + room::member_search::{self, search_room_members_streaming_with_sort, PrecomputedMemberSort}, shared::mentionable_text_input::SearchResult, + sliding_sync::TimelineKind, }; use matrix_sdk::room::RoomMember; pub enum CpuJob { SearchRoomMembers(SearchRoomMembersJob), + PrecomputeMemberSort(PrecomputeMemberSortJob), +} + +/// Action posted back to UI thread when precomputed sort is ready. +#[derive(Debug)] +pub struct PrecomputedMemberSortReady { + pub timeline_kind: TimelineKind, + pub sort: Arc, +} + +pub struct PrecomputeMemberSortJob { + pub timeline_kind: TimelineKind, + pub members: Arc>, } pub struct SearchRoomMembersJob { @@ -48,9 +62,18 @@ fn run_member_search(params: SearchRoomMembersJob) { ); } +fn run_precompute_sort(params: PrecomputeMemberSortJob) { + let sort = member_search::precompute_member_sort(¶ms.members); + Cx::post_action(PrecomputedMemberSortReady { + timeline_kind: params.timeline_kind, + sort: Arc::new(sort), + }); +} + /// Spawns a CPU-bound job on a detached native thread. pub fn spawn_cpu_job(cx: &mut Cx, job: CpuJob) { cx.spawn_thread(move || match job { CpuJob::SearchRoomMembers(params) => run_member_search(params), + CpuJob::PrecomputeMemberSort(params) => run_precompute_sort(params), }); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 3bc0c1b87..fcfd2a60f 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2257,6 +2257,15 @@ impl Widget for RoomScreen { _ => {} } + // Handle precomputed member sort ready (from background thread) + if let Some(sort_ready) = action.downcast_ref::() { + if let Some(tl) = self.tl_state.as_mut() { + if tl.kind == sort_ready.timeline_kind { + tl.room_members_sort = Some(sort_ready.sort.clone()); + } + } + } + match action.downcast_ref::() { Some(CreateBotModalAction::Close) => { self.close_create_bot_modal(cx); @@ -3216,8 +3225,12 @@ impl RoomScreen { tl.room_members_sync_pending = false; tl.awaiting_post_sync_member_refresh = false; } - tl.room_members_sort = Some(Arc::new( - crate::room::member_search::precompute_member_sort(&members) + // Compute sort in background thread to avoid UI stall on large rooms + crate::cpu_worker::spawn_cpu_job(cx, crate::cpu_worker::CpuJob::PrecomputeMemberSort( + crate::cpu_worker::PrecomputeMemberSortJob { + timeline_kind: tl.kind.clone(), + members: Arc::clone(&members), + } )); tl.room_members = Some(members); }, From 5ae2ef8c51aef276021cf6f79d966ea044f30ec4 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 03:58:34 +0800 Subject: [PATCH 084/283] Fix race condition: validate member count before accepting sort result When the same room receives two consecutive RoomMembersListFetched updates, the background sort from the first (smaller) member list could arrive after the second and overwrite the sort data. Subsequent mention searches would then index into member_keys with out-of-bounds indices from the newer, larger member list. Fix: include member_count in PrecomputedMemberSortReady and only accept the result if it matches the current room_members length. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cpu_worker.rs | 4 ++++ src/home/room_screen.rs | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cpu_worker.rs b/src/cpu_worker.rs index 430f5834c..7329c0f19 100644 --- a/src/cpu_worker.rs +++ b/src/cpu_worker.rs @@ -23,6 +23,8 @@ pub enum CpuJob { pub struct PrecomputedMemberSortReady { pub timeline_kind: TimelineKind, pub sort: Arc, + /// The member count this sort was computed for, used to reject stale results. + pub member_count: usize, } pub struct PrecomputeMemberSortJob { @@ -63,10 +65,12 @@ fn run_member_search(params: SearchRoomMembersJob) { } fn run_precompute_sort(params: PrecomputeMemberSortJob) { + let member_count = params.members.len(); let sort = member_search::precompute_member_sort(¶ms.members); Cx::post_action(PrecomputedMemberSortReady { timeline_kind: params.timeline_kind, sort: Arc::new(sort), + member_count, }); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index fcfd2a60f..f66eea9bd 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2257,11 +2257,16 @@ impl Widget for RoomScreen { _ => {} } - // Handle precomputed member sort ready (from background thread) + // Handle precomputed member sort ready (from background thread). + // Validate member count to reject stale results from an older member list. if let Some(sort_ready) = action.downcast_ref::() { if let Some(tl) = self.tl_state.as_mut() { if tl.kind == sort_ready.timeline_kind { - tl.room_members_sort = Some(sort_ready.sort.clone()); + let current_count = tl.room_members.as_ref().map_or(0, |m| m.len()); + if current_count == sort_ready.member_count { + tl.room_members_sort = Some(sort_ready.sort.clone()); + } + // else: stale sort from an older member snapshot, discard } } } From 32dffa522df5e9c355c13575f41ef874949b2069 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 04:05:00 +0800 Subject: [PATCH 085/283] Strengthen sort staleness check: use Arc pointer identity Replace member_count comparison with Arc::as_ptr() identity check. This rejects stale sort results even when two different member snapshots happen to have the same length, fully closing the race condition window. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cpu_worker.rs | 9 +++++---- src/home/room_screen.rs | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cpu_worker.rs b/src/cpu_worker.rs index 7329c0f19..687524e53 100644 --- a/src/cpu_worker.rs +++ b/src/cpu_worker.rs @@ -23,8 +23,9 @@ pub enum CpuJob { pub struct PrecomputedMemberSortReady { pub timeline_kind: TimelineKind, pub sort: Arc, - /// The member count this sort was computed for, used to reject stale results. - pub member_count: usize, + /// Pointer identity of the Arc> this sort was computed for. + /// Used to reject stale results if room_members was replaced. + pub members_identity: usize, } pub struct PrecomputeMemberSortJob { @@ -65,12 +66,12 @@ fn run_member_search(params: SearchRoomMembersJob) { } fn run_precompute_sort(params: PrecomputeMemberSortJob) { - let member_count = params.members.len(); + let members_identity = Arc::as_ptr(¶ms.members) as usize; let sort = member_search::precompute_member_sort(¶ms.members); Cx::post_action(PrecomputedMemberSortReady { timeline_kind: params.timeline_kind, sort: Arc::new(sort), - member_count, + members_identity, }); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index f66eea9bd..85b7a0a2e 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2258,15 +2258,17 @@ impl Widget for RoomScreen { } // Handle precomputed member sort ready (from background thread). - // Validate member count to reject stale results from an older member list. + // Validate by Arc pointer identity to reject stale results from a + // different member snapshot (even if same length). if let Some(sort_ready) = action.downcast_ref::() { if let Some(tl) = self.tl_state.as_mut() { if tl.kind == sort_ready.timeline_kind { - let current_count = tl.room_members.as_ref().map_or(0, |m| m.len()); - if current_count == sort_ready.member_count { + let current_identity = tl.room_members.as_ref() + .map_or(0, |m| Arc::as_ptr(m) as usize); + if current_identity == sort_ready.members_identity { tl.room_members_sort = Some(sort_ready.sort.clone()); } - // else: stale sort from an older member snapshot, discard + // else: stale sort from a different member snapshot, discard } } } From 3d142a95b38df9bd5e248df51fb26255f4d2cc6b Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 04:14:21 +0800 Subject: [PATCH 086/283] Fix ABA in sort staleness check: hold Arc alive, use ptr_eq The previous usize-based pointer comparison had an ABA problem: after the background thread posted the action, the old Arc could be freed and its address reused by a new member snapshot, causing a false match. Fix: PrecomputedMemberSortReady now holds the Arc> itself (keeping it alive), and the handler uses Arc::ptr_eq() for comparison. This makes address reuse impossible while the action is in flight. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cpu_worker.rs | 9 ++++----- src/home/room_screen.rs | 11 +++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/cpu_worker.rs b/src/cpu_worker.rs index 687524e53..27fe3bbd7 100644 --- a/src/cpu_worker.rs +++ b/src/cpu_worker.rs @@ -23,9 +23,9 @@ pub enum CpuJob { pub struct PrecomputedMemberSortReady { pub timeline_kind: TimelineKind, pub sort: Arc, - /// Pointer identity of the Arc> this sort was computed for. - /// Used to reject stale results if room_members was replaced. - pub members_identity: usize, + /// The Arc> this sort was computed for. + /// Held alive to prevent ABA via address reuse; compared by Arc::ptr_eq. + pub members_arc: Arc>, } pub struct PrecomputeMemberSortJob { @@ -66,12 +66,11 @@ fn run_member_search(params: SearchRoomMembersJob) { } fn run_precompute_sort(params: PrecomputeMemberSortJob) { - let members_identity = Arc::as_ptr(¶ms.members) as usize; let sort = member_search::precompute_member_sort(¶ms.members); Cx::post_action(PrecomputedMemberSortReady { timeline_kind: params.timeline_kind, sort: Arc::new(sort), - members_identity, + members_arc: params.members, // keep alive to prevent ABA }); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 85b7a0a2e..5d01937cf 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2258,17 +2258,16 @@ impl Widget for RoomScreen { } // Handle precomputed member sort ready (from background thread). - // Validate by Arc pointer identity to reject stale results from a - // different member snapshot (even if same length). + // Validate by Arc::ptr_eq to reject stale results from a different + // member snapshot. The Arc is kept alive in the action to prevent ABA. if let Some(sort_ready) = action.downcast_ref::() { if let Some(tl) = self.tl_state.as_mut() { if tl.kind == sort_ready.timeline_kind { - let current_identity = tl.room_members.as_ref() - .map_or(0, |m| Arc::as_ptr(m) as usize); - if current_identity == sort_ready.members_identity { + let is_same = tl.room_members.as_ref() + .is_some_and(|m| Arc::ptr_eq(m, &sort_ready.members_arc)); + if is_same { tl.room_members_sort = Some(sort_ready.sort.clone()); } - // else: stale sort from a different member snapshot, discard } } } From 0d1bd94c6041cebbe70fb450c4f9b2daab2439fc Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 04:20:41 +0800 Subject: [PATCH 087/283] Fix: clear stale sort before replacing members to prevent OOB When RoomMembersListFetched arrives, the old room_members_sort was not cleared before replacing room_members. In the window between member replacement and background sort completion, mention search would use new members with old sort keys, risking index out of bounds if the new list is longer. Fix: set room_members_sort = None before replacing room_members. Search falls back to the unsorted path until the new sort arrives. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/home/room_screen.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 5d01937cf..306a4d463 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -3231,14 +3231,17 @@ impl RoomScreen { tl.room_members_sync_pending = false; tl.awaiting_post_sync_member_refresh = false; } - // Compute sort in background thread to avoid UI stall on large rooms + // Invalidate old sort before replacing members to prevent + // stale sort + new members mismatch (index out of bounds). + tl.room_members_sort = None; + tl.room_members = Some(Arc::clone(&members)); + // Compute new sort in background thread crate::cpu_worker::spawn_cpu_job(cx, crate::cpu_worker::CpuJob::PrecomputeMemberSort( crate::cpu_worker::PrecomputeMemberSortJob { timeline_kind: tl.kind.clone(), - members: Arc::clone(&members), + members, } )); - tl.room_members = Some(members); }, TimelineUpdate::MediaFetched(request) => { log!("process_timeline_updates(): media fetched for room {}", tl.kind.room_id()); From 376ca6ff0c79d4872ae3631805745c12a5215ff0 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 04:44:07 +0800 Subject: [PATCH 088/283] Streamline AGENTS.md and allow CLAUDE.md in repo - Rewrite AGENTS.md from verbose 731-line Makepad guide to concise 104-line project rules document. Detailed reference is now in CLAUDE.md, DESIGN.md, SPLASH.md, and Makepad 2.0 skills. - Remove CLAUDE.md from .gitignore so it can be tracked in git Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 - AGENTS.md | 758 +++++------------------------------------------------ 2 files changed, 65 insertions(+), 694 deletions(-) diff --git a/.gitignore b/.gitignore index 1f891a019..9d61dcd77 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ .vscode .DS_Store -CLAUDE.md proxychains.conf diff --git a/AGENTS.md b/AGENTS.md index 9a393de6f..b8cd5decd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,732 +1,104 @@ +# Robrix2 — Agent Instructions -# Makepad Project Guide +Keep this file short. Use it for project rules and working guidance. Use the codebase, `CLAUDE.md`, and Makepad 2.0 skills as the detailed reference. -## Important: When Converting Syntax +## Required Reading -**Always search for existing usage patterns in the NEW crates (widgets, code_editor, studio) before making syntax changes.** The old `widgets` and `live_design!` syntax is deprecated. When unsure about the correct syntax for something, grep for similar usage in `widgets/src/` to find the correct pattern. +Before starting work, read these documents: -```bash -# Example: find how texture declarations work in new system -grep -r "texture_2d" widgets/src/ -``` - -**Critical: Always use `Name: value` syntax, never `Name = value`.** The old `Key = Value` syntax no longer works. For named widget instances, use `name := Type{...}` syntax. - -## Running UI Programs - -```bash -RUST_BACKTRACE=1 cargo run -p makepad-example-splash --release & PID=$!; sleep 15; kill $PID 2>/dev/null; echo "Process $PID killed" -``` - -## Cargo.toml Setup - -```toml -[package] -name = "makepad-example-myapp" -version = "0.1.0" -edition = "2021" - -[dependencies] -makepad-widgets = { path = "../../widgets" } -``` - - -## Widgets DSL (script_mod!) - -The new DSL uses `script_mod!` macro with runtime script evaluation instead of the old `live_design!` compile-time macros. - -### Imports and App Setup - -```rust -use makepad_widgets::*; - -app_main!(App); - -script_mod!{ - use mod.prelude.widgets.* - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ - main_window := Window{ - window.inner_size: vec2(800, 600) - body +: { - // UI content here - } - } - } - } -} - -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); // Register all widgets - // Platform-specific initialization goes here (e.g., vm.cx().start_stdin_service() for macos) - App::from_script_mod(vm, self::script_mod) - } -} - -#[derive(Script, ScriptHook)] -pub struct App { - #[live] ui: WidgetRef, -} - -impl MatchEvent for App { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - // Handle widget actions - } -} - -impl AppMain for App { - fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - self.match_event(cx, event); - self.ui.handle_event(cx, event, &mut Scope::empty()); - } -} -``` - -### Available Widgets (widgets/src/lib.rs) - -Core: `View`, `SolidView`, `RoundedView`, `ScrollXView`, `ScrollYView`, `ScrollXYView` -Text: `Label`, `H1`, `H2`, `H3`, `LinkLabel`, `TextInput` -Buttons: `Button`, `ButtonFlat`, `ButtonFlatter` -Toggles: `CheckBox`, `Toggle`, `RadioButton` -Input: `Slider`, `DropDown` -Layout: `Splitter`, `FoldButton`, `FoldHeader`, `Hr` -Lists: `PortalList` -Navigation: `StackNavigation`, `ExpandablePanel` -Overlays: `Modal`, `Tooltip`, `PopupNotification` -Dock: `Dock`, `DockSplitter`, `DockTabs`, `DockTab` -Media: `Image`, `Icon`, `LoadingSpinner` -Special: `FileTree`, `PageFlip`, `CachedWidget` -Window: `Window`, `Root` -Markup: `Html`, `Markdown` (feature-gated) - -### Widget Definition Pattern - -```rust -// Rust struct -#[derive(Script, ScriptHook, Widget)] -pub struct MyWidget { - #[source] source: ScriptObjectRef, // Required for script integration - #[walk] walk: Walk, - #[layout] layout: Layout, - #[redraw] #[live] draw_bg: DrawQuad, - #[live] draw_text: DrawText, - #[rust] my_state: i32, // Runtime-only field -} - -// For widgets with animations, add Animator derive: -#[derive(Script, ScriptHook, Widget, Animator)] -pub struct AnimatedWidget { - #[source] source: ScriptObjectRef, - #[apply_default] animator: Animator, - // ... -} -``` - -### Script Module Structure - -```rust -script_mod!{ - use mod.prelude.widgets_internal.* // For internal widget definitions - use mod.widgets.* // Access other widgets - - // Register base widget (connects Rust struct to script) - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) - - // Create styled variant with defaults - mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{ - width: Fill - height: Fit - padding: theme.space_2 - - draw_bg +: { - color: theme.color_bg_app - } - } -} -``` - -### Key Syntax Differences (Old vs New) - -| Old (live_design!) | New (script_mod!) | -|-------------------|-------------------| -| `` | `mod.widgets.BaseWidget{ }` | -| `{{StructName}}` | `#(Struct::register_widget(vm))` | -| `(THEME_COLOR_X)` | `theme.color_x` | -| `` | `theme.font_regular` | -| `instance hover: 0.0` | `hover: instance(0.0)` | -| `uniform color: #fff` | `color: uniform(#fff)` | -| `draw_bg: { }` (replace) | `draw_bg +: { }` (merge) | -| `default: off` | `default: @off` | -| `fn pixel(self)` | `pixel: fn()` | -| `item.apply_over(cx, live!{...})` | `script_apply_eval!(cx, item, {...})` | - -### Runtime Property Updates with script_apply_eval! - -Use `script_apply_eval!` macro to dynamically update widget properties at runtime: -```rust -// Old system (live! macro with apply_over) -item.apply_over(cx, live!{ - height: (height) - draw_bg: {is_even: (if is_even {1.0} else {0.0})} -}); - -// New system (script_apply_eval! macro) -script_apply_eval!(cx, item, { - height: #(height) - draw_bg +: {is_even: #(if is_even {1.0} else {0.0})} -}); - -// For colors, use #(color) syntax -let color = self.color_focus; -script_apply_eval!(cx, item, { - draw_bg +: { - color: #(color) - } -}); -``` - -Note: In `script_apply_eval!`, use `#(expr)` for Rust expression interpolation instead of `(expr)`. - -### Theme Access - -Always use `theme.` prefix: -```rust -color: theme.color_bg_app -padding: theme.space_2 -font_size: theme.font_size_p -text_style: theme.font_regular -``` - -### Property Merging with `+:` - -The `+:` operator merges with parent instead of replacing: -```rust -mod.widgets.MyButton = mod.widgets.Button{ - draw_bg +: { - color: #f00 // Only overrides color, keeps other draw_bg properties - } -} -``` - -### Shader Instance vs Uniform - -- `instance(value)` - Per-draw-call value (can vary per widget instance) -- `uniform(value)` - Shared across all instances using same shader - -```rust -draw_bg +: { - hover: instance(0.0) // Each button has its own hover state - color: uniform(theme.color_x) // Shared base color - color_hover: instance(theme.color_y) // Per-instance if color varies -} -``` - -### Animator Definition - -```rust -animator: Animator{ - hover: { - default: @off - off: AnimatorState{ - from: {all: Forward {duration: 0.1}} - apply: { - draw_bg: {hover: 0.0} - draw_text: {hover: 0.0} - } - } - on: AnimatorState{ - from: {all: Snap} // Instant transition - apply: { - draw_bg: {hover: 1.0} - draw_text: {hover: 1.0} - } - } - } -} -``` - -### Shader Functions - -```rust -draw_bg +: { - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size) - sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 4.0) - sdf.fill(self.color.mix(self.color_hover, self.hover)) - return sdf.result - } -} -``` - -Note: Use `.method()` not `::method()` in shaders. - -### Color Mixing (Method Chaining) - -```rust -// Old nested style (avoid) -mix(mix(mix(color1, color2, hover), color3, down), color4, focus) - -// New chained style (preferred) -color1.mix(color2, hover).mix(color3, down).mix(color4, focus) -``` +1. [DESIGN.md](DESIGN.md) — architecture overview, module organization, technology stack +2. [specs/project.spec.md](specs/project.spec.md) — project constraints, decisions, forbidden actions +3. [CLAUDE.md](CLAUDE.md) — project workflow rules and Makepad 2.0 guidance -### App Structure Pattern - -```rust -script_mod!{ - use mod.prelude.widgets.* - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ - main_window := Window{ - window.inner_size: vec2(1000, 700) - body +: { - // Your UI here - MyWidget{} - } - } - } - } -} - -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); - // Platform-specific initialization (e.g., vm.cx().start_stdin_service() for macos) - App::from_script_mod(vm, self::script_mod) - } -} - -#[derive(Script, ScriptHook)] -pub struct App { - #[live] ui: WidgetRef, -} - -impl MatchEvent for App { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - if self.ui.button(ids!(my_button)).clicked(actions) { - log!("Button clicked!"); - } - } -} - -impl AppMain for App { - fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - self.match_event(cx, event); - self.ui.handle_event(cx, event, &mut Scope::empty()); - } -} -``` - -### Widget ID References - -Use `:=` for named widget instances: -```rust -// In DSL -my_button := Button{text: "Click"} - -// In Rust code -self.ui.button(ids!(my_button)).clicked(actions) -``` - -### Template Definitions in Dock - -Templates inside Dock are local; use `let` bindings at script level for reusable components: -```rust -script_mod!{ - // Reusable at script level - let MyPanel = SolidView{ - width: Fill - height: Fill - // ... - } - - // Use directly - body +: { - MyPanel{} // Works because it's a let binding - } -} -``` - -### Custom Draw Widget Example - -```rust -#[derive(Script, ScriptHook, Widget)] -pub struct CustomDraw { - #[walk] walk: Walk, - #[layout] layout: Layout, - #[redraw] #[live] draw_quad: DrawQuad, - #[rust] area: Area, -} - -impl Widget for CustomDraw { - fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { - cx.begin_turtle(walk, self.layout); - let rect = cx.turtle().rect(); - self.draw_quad.draw_abs(cx, rect); - cx.end_turtle_with_area(&mut self.area); - DrawStep::done() - } - - fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) {} -} -``` - -### Script Object Storage: map vs vec - -In script objects, properties are stored in two different places: -- **`map`**: Contains `key: value` pairs (regular properties) -- **`vec`**: Contains named template items (via `:=` syntax) - -This distinction is important when working with `on_after_apply` or inspecting script objects directly. - -### Templates in List Widgets (PortalList, FlatList) - -In list widgets, named IDs (using `:=`) define **templates** that are stored in the widget's `templates` HashMap. These are NOT regular properties - they go into the script object's vec and are collected via `on_after_apply`. - -```rust -// In script_mod! - defining templates for a list -my_list := PortalList { - // Regular properties (go into struct fields) - width: Fill - height: Fill - scroll_bar: mod.widgets.ScrollBar {} - - // Templates (named with :=) - stored in templates HashMap, NOT struct fields - Item := View { - height: 40 - title := Label { text: "Default" } - } - Header := View { - draw_bg: { color: #333 } - } -} -``` - -The templates are collected in `on_after_apply`: -```rust -impl ScriptHook for PortalList { - fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) { - if let Some(obj) = value.as_object() { - vm.vec_with(obj, |_vm, vec| { - for kv in vec { - if let Some(id) = kv.key.as_id() { - self.templates.insert(id, kv.value); - } - } - }); - } - } -} -``` - -Then used during drawing: -```rust -while let Some(item_id) = list.next_visible_item(cx) { - let item = list.item(cx, item_id, id!(Item)); - item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id)); - item.draw_all(cx, &mut Scope::empty()); -} -``` - -**Key distinction**: Regular properties like `scroll_bar: mod.widgets.ScrollBar {}` are applied directly to struct fields. Template definitions like `Item := View {...}` are stored separately for dynamic instantiation. - -### PortalList Usage - -```rust -#[derive(Script, ScriptHook, Widget)] -pub struct MyList { - #[deref] view: View, -} - -impl Widget for MyList { - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - while let Some(item) = self.view.draw_walk(cx, scope, walk).step() { - if let Some(mut list) = item.borrow_mut::() { - list.set_item_range(cx, 0, 100); // 100 items - - while let Some(item_id) = list.next_visible_item(cx) { - let item = list.item(cx, item_id, id!(Item)); - item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id)); - item.draw_all(cx, &mut Scope::empty()); - } - } - } - DrawStep::done() - } -} -``` - -### FileTree Usage - -```rust -impl Widget for FileTreeDemo { - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - while self.file_tree.draw_walk(cx, scope, walk).is_step() { - self.file_tree.set_folder_is_open(cx, live_id!(root), true, Animate::No); - // Draw nodes recursively - self.draw_node(cx, live_id!(root)); - } - DrawStep::done() - } -} -``` - -### Registering Custom Draw Shaders - -For custom draw types with shader fields, use `script_shader`: - -```rust -script_mod!{ - use mod.prelude.widgets_internal.* - - // Register custom draw shader - set_type_default() do #(DrawMyShader::script_shader(vm)){ - ..mod.draw.DrawQuad // Inherit from DrawQuad - } - - // Register widget that uses it - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) -} - -#[derive(Script, ScriptHook)] -#[repr(C)] -struct DrawMyShader { - #[deref] draw_super: DrawQuad, - #[live] my_param: f32, -} -``` - -### Registering Components (non-Widget) - -For structs that aren't full widgets but need script registration: - -```rust -script_mod!{ - // For components (not widgets) - mod.widgets.MyComponentBase = #(MyComponent::script_component(vm)) - - // For widgets (implements Widget trait) - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) -} -``` +## Critical Rules -### Script Prelude Modules - -Two prelude modules available: -- `mod.prelude.widgets_internal.*` - For internal widget library development -- `mod.prelude.widgets.*` - For app development (includes all widgets) - -```rust -script_mod!{ - // App development - use widgets prelude - use mod.prelude.widgets.* - - // Or for widget library internals - use mod.prelude.widgets_internal.* - use mod.widgets.* -} -``` +### Do NOT run `cargo fmt` or `rustfmt` -### Default Enum Values - -For enums with a `None` variant that need `Default`, use standard Rust `#[default]` attribute instead of `DefaultNone` derive: - -```rust -// Correct - use #[default] attribute on the None variant -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum MyAction { - SomeAction, - AnotherAction, - #[default] - None, -} - -// Wrong - don't use DefaultNone derive -#[derive(Clone, Copy, Debug, PartialEq, DefaultNone)] // Don't do this -pub enum MyAction { - SomeAction, - None, -} -``` +This project does not use automatic Rust formatting. Do not run `cargo fmt`, `rustfmt`, or formatter wrappers. Formatting churn creates noisy diffs and breaks the repo's hand-maintained style. -### Multi-Module Script Registration Pattern - -When refactoring a multi-file project (like studio) from `live_design!` to `script_mod!`: - -1. **Each widget module** defines its own `script_mod!` that registers to `mod.widgets.*`: -```rust -// In studio_editor.rs -script_mod! { - use mod.prelude.widgets_internal.* - use mod.widgets.* - - mod.widgets.StudioCodeEditorBase = #(StudioCodeEditor::register_widget(vm)) - mod.widgets.StudioCodeEditor = set_type_default() do mod.widgets.StudioCodeEditorBase { - editor := CodeEditor {} - } -} -``` +### Do NOT commit or create PRs without user testing -2. **The lib.rs** aggregates all widget script_mods: -```rust -pub fn script_mod(vm: &mut ScriptVm) { - crate::module1::script_mod(vm); - crate::module2::script_mod(vm); - // ... all widget modules -} -``` +Present changes for testing first. Wait for user confirmation before committing or opening a PR. -3. **The app.rs** calls them in correct order: -```rust -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); // Base widgets first - crate::script_mod(vm); // Your widget modules - crate::app_ui::script_mod(vm); // UI that uses the widgets - App::from_script_mod(vm, self::script_mod) - } -} -``` +### Makepad 2.0 only -4. **The app_ui.rs** can then use registered widgets: -```rust -script_mod! { - use mod.prelude.widgets.* - // Now StudioCodeEditor is available from mod.widgets - - let EditorContent = View { - editor := StudioCodeEditor {} - } -} -``` +- Use `script_mod!`, not `live_design!` +- Use `#[derive(Script, ScriptHook, Widget)]`, not `Live` / `LiveHook` +- Use `:=` for named children, not `=` +- Use `+:` to merge properties; bare `:` replaces +- Use `script_apply_eval!` for runtime updates, not `apply_over` + `live!` -### Cross-Module Sharing via `mod` Object - -**IMPORTANT**: `use crate.module.*` does NOT work in script_mod. The `crate.` prefix is not available. - -To share definitions between script_mod blocks in different files, store them in the `mod` object: - -```rust -// In app_ui.rs - export to mod.widgets namespace -script_mod! { - use mod.prelude.widgets.* - - // This makes AppUI available as mod.widgets.AppUI - mod.widgets.AppUI = Window{ - // ... - } -} - -// In app.rs - import via mod.widgets -script_mod! { - use mod.prelude.widgets.* - use mod.widgets.* // Now AppUI is in scope - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ AppUI{} } - } -} -``` +### Converting syntax -The `mod` object is the only way to share data between script_mod blocks. +- Search the new crates first: `widgets`, `code_editor`, `studio` +- Prefer copying an existing Makepad 2.0 pattern over guessing syntax +- Always use `Name: value`, never `Name = value` +- Named widget instances use `name := Type{...}` -### Prelude Alias Syntax +### Dynamic widget state changes -When defining a prelude, use `name:mod.path` to create an alias: -```rust -mod.prelude.widgets = { - ..mod.std, // Spread all of mod.std into scope - theme:mod.theme, // Create 'theme' as alias for mod.theme - draw:mod.draw, // Create 'draw' as alias for mod.draw -} -``` +`script_apply_eval!` does not work on widgets created via `widget_ref_from_live_ptr()` because the backing `ScriptObject` is `ZERO`. For dynamic popup and list items, use Animator state plus shader instance variables instead. -Without the alias (just `mod.theme,`), the module is included but has no name - you can't access it! +### Async Matrix operations -### Let Bindings are Local +Always use `submit_async_request(MatrixRequest::*)`. Do not spawn raw tokio tasks for Matrix API calls from UI code. -`let` bindings in script_mod are LOCAL to that script_mod block. They cannot be: -- Accessed from other script_mod blocks -- Used as property values directly (e.g., `content +: MyLetBinding` won't work) +## Quick Makepad Notes -To use a `let` binding, instantiate it: `MyLetBinding{}` or store it in `mod.*` for cross-module access. +- `draw_bg +:` merges with the parent shader config; `draw_bg:` replaces it +- In `script_apply_eval!`, Rust expressions use `#(expr)` interpolation +- Runtime `script_apply_eval!` cannot rely on DSL constants like `Right`, `Fit`, or `Align` +- `Dock.load_state()` can corrupt DrawList references in this project -### Debug Logging with `~` +## Build & Test -Use `~expression` to log the value of an expression during script evaluation: -```rust -script_mod! { - ~mod.theme // Logs the theme object - ~mod.prelude.widgets // Logs what's in the prelude - ~some_variable // Logs a variable's value (or "not found" error) -} +```bash +cargo build +cargo run +cargo test ``` -### Common Pitfalls - -**Widget ID references**: Named widget instances use `:=` in the DSL and plain names in Rust id macros: -- DSL defines `code_block := View { ... }` → Rust uses `id!(code_block)` -- DSL defines `my_button := Button { ... }` → Rust uses `ids!(my_button)` - -1. **Missing `#[source]`**: All Script-derived structs need `#[source] source: ScriptObjectRef` - -2. **Template scope**: Templates defined inside Dock aren't available outside; use `let` at script level - -3. **Uniform vs Instance**: Use `instance()` for per-widget varying colors (like hover states on backgrounds) - -4. **Forgot `+:`**: Without `+:`, you replace the entire property instead of merging - -5. **Theme access**: Always `theme.color_x`, never `THEME_COLOR_X` or `(theme.color_x)` +## Key Entry Points -6. **Missing widget registration**: Call `crate::makepad_widgets::script_mod(vm)` in `App::run()` before your own `script_mod`. Note: the old `live_design!` system and its crates are archived under `old/` +- `src/app.rs` — root app and global state +- `src/sliding_sync.rs` — Matrix sync pipeline +- `src/home/room_screen.rs` — room timeline and input integration +- `src/shared/mentionable_text_input.rs` — `@mention` system -7. **Draw shader repr**: Custom draw shaders need `#[repr(C)]` for correct memory layout +## Specs -8. **DefaultNone derive**: Don't use `DefaultNone` derive - use standard `#[derive(Default)]` with `#[default]` attribute on the `None` variant +Task specs live in `specs/` and inherit from [specs/project.spec.md](specs/project.spec.md). -9. **Script_mod call order**: Widget modules must be registered BEFORE UI modules that use them. Always call `lib.rs::script_mod` before `app_ui::script_mod` +- `specs/task-mention-user.spec.md` — `@mention` autocomplete feature -10. **`pub` keyword invalid in script_mod**: Don't use `pub mod.widgets.X = ...`, just use `mod.widgets.X = ...`. Visibility is controlled by the Rust module system, not script_mod. +Use `agent-spec parse` and `agent-spec lint --min-score 0.7` when working on specs. -11. **Syntax for Inset/Align/Walk**: Use constructor syntax - `margin: Inset{left: 10}` not `margin: {left: 10}`, `align: Align{x: 0.5 y: 0.5}` not `align: {x: 0.5, y: 0.5}` +## Working Philosophy -12. **Cursor values**: Use `cursor: MouseCursor.Hand` not `cursor: Hand` or `cursor: @Hand` +You are an engineering collaborator on this project, not a standby assistant. Work in a direct, execution-first style: -13. **Resource paths**: Use `crate_resource("self://path")` not `dep("crate://self/path")` +- Finish concrete work before reporting back +- Report what you changed, why you changed it, and what tradeoffs you made +- Prefer complete, reviewable units over tentative partial steps +- Keep mid-work chatter low; use delivery reports for important context -14. **Texture declarations in shaders**: Use `tex: texture_2d(float)` not `tex: texture2d` +## What You Submit To -15. **Enums not exposed to script**: Some Rust enums like `PopupMenuPosition::BelowInput` may not be exposed to script. If you get "not found" errors on enum variants, just remove the property and use the default +In priority order: -17. **Shader `mod` vs `modf`**: The Makepad shader language uses `modf(a, b)` for float modulo, NOT `mod(a, b)`. Similarly, use `atan2(y, x)` not `atan(y, x)` for two-argument arctangent. `atan(x)` (single arg) is also available. `fract(x)` works as expected. +1. The task's completion criteria +2. The project's existing style and patterns +3. The user's explicit, unambiguous instructions -16. **Draw shader struct field ordering**: In `#[repr(C)]` draw shader structs that extend another draw shader via `#[deref]`, NEVER place `#[rust]` or other non-instance data AFTER `DrawVars` and the instance fields. The system uses an unsafe pointer trick in `DrawVars::as_slice()` that reads contiguously past the end of `dyn_instances` into the subsequent `#[live]` fields. Any non-instance data between `DrawVars` and the instance fields will corrupt the GPU instance buffer. Put all extra data (like `#[rust]`, `#[live]` non-instance fields such as resource handles, booleans, etc.) BEFORE the `#[deref]` field, and only `#[live]` instance fields (the ones that map to shader inputs) AFTER. - ```rust - // CORRECT - non-instance data before deref, instance fields after - #[derive(Script, ScriptHook)] - #[repr(C)] - pub struct MyDrawShader { - #[live] pub svg: Option, // non-instance, BEFORE deref - #[rust] my_state: bool, // non-instance, BEFORE deref - #[deref] pub draw_super: DrawVector, // contains DrawVars + base instance fields - #[live] pub tint: Vec4f, // instance field, AFTER deref - OK - } +Correctness outranks performative deference. Do the engineering work instead of offloading routine implementation choices back to the user. - // WRONG - rust data after instance fields breaks the memory layout - #[derive(Script, ScriptHook)] - #[repr(C)] - pub struct MyDrawShader { - #[deref] pub draw_super: DrawVector, - #[live] pub tint: Vec4f, // instance field - #[rust] my_state: bool, // BAD: sits between tint and the next shader's fields - } - ``` +## On Stopping to Ask -18. **Don't put comments or blank lines before the first real code in `script!`/`script_mod!`**: Rust's proc macro token stream strips comments entirely — they produce no tokens. This shifts error column/line info because the span tracking starts from the first actual token. Always start with real code (e.g., `use mod.std.assert`) immediately after the opening brace. +Stop and ask only when genuine ambiguity would likely produce output contrary to the user's intent. -19. **WARNING: Hex colors containing the letter `e` in `script_mod!`**: The Rust tokenizer interprets `e` or `E` in hex color literals as a scientific notation exponent, causing parse errors like `expected at least one digit in exponent`. For example, `#2ecc71` fails because `2e` looks like the start of `2e`. **Use the `#x` prefix** to escape this: write `#x2ecc71` instead of `#x2ecc71`. This applies to any hex color where a digit is immediately followed by `e`/`E` (e.g., `#1e1e2e`, `#4466ee`, `#7799ee`, `#bb99ee`). Colors without `e` (like `#ff4444`, `#44cc44`) work fine with plain `#`. +Do not stop just to ask about: -20. **Shader enums**: Prefer `match` on enum values with `_ =>` as the catch-all arm, not `if/else` chains over integer-like values. If enum `match` fails in shader compilation, treat it as a compiler bug: add or extend a `platform/script/test` case and fix the shader compiler path instead of rewriting shader logic to `if/else`. \ No newline at end of file +- Reversible implementation details +- Obvious next steps that are already part of the task +- Style choices you can resolve by reading the codebase +- Post-hoc "should I also do X" follow-ups when X is already implied by the task From 1db933fc48763ddc7016aa3d448e8e3c717f11b8 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 05:38:45 +0800 Subject: [PATCH 089/283] Add scrollable @mention user list with keyboard auto-scroll Wrap the mention popup's List widget in a ScrollYView to enable vertical scrolling when results exceed the visible area. Previously, the list was hardcoded to show at most 10 items (desktop) / 5 (mobile) with no way to browse further matches. Key changes: - Wrap List in ScrollYView with fixed viewport height (360px desktop, 216px mobile), dynamically shrunk for fewer items - Increase display limits to 50/25 and backend search limits to match - Add keyboard auto-scroll (ArrowUp/Down keeps focused item visible) - Reset scroll position on new search and popup close - Handle loading/no-matches states with appropriate viewport sizing Co-Authored-By: Claude Opus 4.6 (1M context) --- specs/task-mention-list-scrolling.spec.md | 82 ++++++++++++++++++++ src/shared/command_text_input.rs | 70 ++++++++++++++++- src/shared/mentionable_text_input.rs | 93 ++++++++++++++++++----- 3 files changed, 223 insertions(+), 22 deletions(-) create mode 100644 specs/task-mention-list-scrolling.spec.md diff --git a/specs/task-mention-list-scrolling.spec.md b/specs/task-mention-list-scrolling.spec.md new file mode 100644 index 000000000..641bde5a4 --- /dev/null +++ b/specs/task-mention-list-scrolling.spec.md @@ -0,0 +1,82 @@ +spec: task +name: "Scrollable @mention User List" +inherits: project +tags: [feature, mention, ui, scrolling] +estimate: 1d +--- + +## Intent + +Make the @mention autocomplete popup's user list scrollable so that users can browse all matching members instead of being limited to a hardcoded maximum (10 on desktop, 5 on mobile). In rooms with many members, the current truncation silently hides relevant results. A scrollable list with a fixed maximum height improves discoverability while keeping the popup compact. + +## Context + +The current `List` widget (in `command_text_input.rs`) is a plain `View` with `flow: Down` — it has no scroll capability. The popup uses `height: Fit` which means it grows unbounded. To prevent this, `DESKTOP_MAX_VISIBLE_ITEMS = 10` and `MOBILE_MAX_VISIBLE_ITEMS = 5` artificially cap the displayed items. The search buffer fetches `max_visible_items * 2` results but only the first half are shown. + +Keyboard navigation (ArrowUp/Down) is managed via `keyboard_focus_index` in `CommandTextInput`. The highlight uses Animator states (`highlight.on/off`) on dynamically-created widgets. + +## Acceptance Criteria + +### Scenario: Popup shows scrollable list when results exceed visible area +- **Given** a room with 50+ members +- **When** the user types `@` (no filter text) +- **Then** the popup shows up to ~10 visible items with a scrollbar +- **And** the user can scroll down to see more results + +### Scenario: Keyboard navigation scrolls the list +- **Given** the mention popup is open with 20+ results +- **When** the user presses ArrowDown past the visible area +- **Then** the list scrolls to keep the focused item visible +- **When** the user presses ArrowUp past the top of the visible area +- **Then** the list scrolls back up to show the focused item + +### Scenario: Mouse wheel scrolling works +- **Given** the mention popup is open with results exceeding the visible area +- **When** the user scrolls with the mouse wheel/trackpad over the list +- **Then** the list scrolls smoothly + +### Scenario: Search results are no longer artificially truncated +- **Given** a room where 30 members match the search query +- **When** the popup displays results +- **Then** all 30 results are accessible via scrolling (not capped at 10) + +### Scenario: Popup height is bounded +- **Given** a room with 500+ members +- **When** the popup shows unfiltered results +- **Then** the popup height does not exceed a reasonable maximum (~400px desktop, ~250px mobile) +- **And** the popup does not overflow the screen + +## Decisions + +- Wrap the `List` widget in a `ScrollYView` named `list_scroll` in the CommandTextInput DSL — do NOT rewrite List's draw logic +- Use `height: Fit{max: Abs(360)}` on `list_scroll` in MentionableTextInput DSL for auto-sizing with a cap (native Makepad bounded Fit) +- Add separate `DESKTOP_MAX_DISPLAY_ITEMS = 50` / `MOBILE_MAX_DISPLAY_ITEMS = 25` constants for the scrollable list item limit +- Also increase backend search limit (`max_results`) to use display limits — otherwise CPU search caps at 20/10 results +- Keep existing `MAX_VISIBLE_ITEMS` constants for height reference only +- Keyboard navigation (`on_keyboard_move`) auto-scrolls via `set_scroll_pos()` with manual position calculation — `scroll_bars_obj` is private +- Add `reset_list_scroll()` called from `clear_popup()` and new-search-start — NOT from `clear_items()` (which runs on every streaming refresh and would cause scroll jumping) +- `clip_y: true` on `list_scroll` to prevent content leaking past rounded corners + +## Boundaries + +### Allowed Changes +- `src/shared/command_text_input.rs` — modify List widget to support scrolling, adjust DSL +- `src/shared/mentionable_text_input.rs` — adjust constants, popup DSL overrides, scroll-on-keyboard logic +- No new cargo dependencies + +### Forbidden +- Do NOT change the trigger mechanism or search logic +- Do NOT change the mention insertion or tracking behavior +- Do NOT change the highlight/Animator system +- Do NOT use `PortalList` (virtual scrolling) — overkill for 50 items, and would require rewriting item instantiation +- Do NOT run `cargo fmt` + +## Completion Criteria + +- [ ] User list scrolls vertically when items exceed visible area +- [ ] Keyboard ArrowUp/Down auto-scrolls to keep focused item visible +- [ ] Mouse wheel/trackpad scrolling works on the list +- [ ] Popup height is bounded (does not grow unbounded) +- [ ] More results are accessible than before (not capped at 10) +- [ ] No visual regression: rounded corners, shadow, header still look correct +- [ ] `cargo build` passes diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index 3e4eb442f..4a61bd535 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -111,8 +111,13 @@ script_mod! { } } - list := mod.widgets.CommandTextInputList{ + list_scroll := ScrollYView { + width: Fill height: Fit + clip_y: true + list := mod.widgets.CommandTextInputList{ + height: Fit + } } } @@ -208,6 +213,11 @@ pub struct CommandTextInput { /// Remember which was the last cursor position handled, to support `inline_search`. #[rust] prev_cursor_position: usize, + + /// Tracked Y scroll position of the list scroll view. + /// We maintain this ourselves because ViewRef has no public get_scroll_pos(). + #[rust] + list_scroll_y: f64, } impl Widget for CommandTextInput { @@ -522,6 +532,7 @@ impl CommandTextInput { false, ); self.clear_items(cx); + self.reset_list_scroll(cx); } /// Clears the list of items. @@ -545,6 +556,15 @@ impl CommandTextInput { self.keyboard_focus_index = self.keyboard_focus_index.or(Some(0)); } + /// Resets the list scroll position to the top. + /// + /// Call this when starting a new search or closing the popup, + /// NOT on every streaming result refresh (which would cause scroll jumping). + pub fn reset_list_scroll(&mut self, cx: &mut Cx) { + self.list_scroll_y = 0.0; + self.list_scroll_view(cx).set_scroll_pos(cx, DVec2 { x: 0.0, y: 0.0 }); + } + /// Add a custom unselectable item to the list. /// /// Ex: Headers, dividers, etc. @@ -708,6 +728,11 @@ impl CommandTextInput { self.child_by_path(ids!(search_input)).as_text_input() } + /// Returns a reference to the scroll view wrapping the list. + pub fn list_scroll_view(&self, cx: &Cx) -> ViewRef { + self.view(cx, ids!(list_scroll)) + } + fn trigger_grapheme(&self) -> Option<&str> { self.trigger.as_ref().and_then(|t| graphemes(t).next()) } @@ -745,9 +770,52 @@ impl CommandTextInput { // This ensures keyboard navigation and mouse hover don't appear simultaneously self.pointer_hover_index = None; + // Auto-scroll to keep the focused item visible within the scroll container + self.scroll_to_focused_item(cx); + self.redraw(cx); } + /// Scrolls the list scroll view so the currently focused item is visible. + /// + /// Uses the tracked `list_scroll_y` instead of deriving scroll offset + /// from item rects (which is biased by list padding and fragile). + fn scroll_to_focused_item(&mut self, cx: &mut Cx) { + let Some(focus_idx) = self.keyboard_focus_index else { return }; + let Some(widget) = self.selectable_widgets.get(focus_idx) else { return }; + + let item_rect = widget.area().rect(cx); + let scroll_view = self.list_scroll_view(cx); + let scroll_rect = scroll_view.area().rect(cx); + + // Rects are invalid before the first draw + if item_rect.size.y <= 0.0 || scroll_rect.size.y <= 0.0 { + return; + } + + let view_height = scroll_rect.size.y; + + // The item's position within the visible viewport: + // When scrolled by `list_scroll_y`, the viewport shows content from + // [list_scroll_y .. list_scroll_y + view_height]. + // Item's screen position relative to scroll view top: + let item_screen_top = item_rect.pos.y - scroll_rect.pos.y; + let item_screen_bottom = item_screen_top + item_rect.size.y; + + let new_scroll_y = if item_screen_bottom > view_height { + // Item extends below the visible area — scroll down + self.list_scroll_y + (item_screen_bottom - view_height) + } else if item_screen_top < 0.0 { + // Item is above the visible area — scroll up + (self.list_scroll_y + item_screen_top).max(0.0) + } else { + return; // Already fully visible, no scroll needed + }; + + self.list_scroll_y = new_scroll_y; + scroll_view.set_scroll_pos(cx, DVec2 { x: 0.0, y: new_scroll_y }); + } + fn update_highlights(&mut self, cx: &mut Cx) { let has_keyboard_focus = self.keyboard_focus_index.is_some(); diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index c4db28df6..5e80a6af4 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -156,10 +156,13 @@ enum MentionSearchState { // Default is derived above; Idle is marked as the default variant -// Constants for mention popup height calculations -// Constants for search behavior -const DESKTOP_MAX_VISIBLE_ITEMS: usize = 10; -const MOBILE_MAX_VISIBLE_ITEMS: usize = 5; +// Constants for mention popup sizing and search behavior. +// MAX_DISPLAY_ITEMS: total items loaded into the scrollable list. +// MAX_SCROLL_HEIGHT: maximum pixel height of the scroll viewport. +const DESKTOP_MAX_DISPLAY_ITEMS: usize = 50; +const MOBILE_MAX_DISPLAY_ITEMS: usize = 25; +const DESKTOP_MAX_SCROLL_HEIGHT: f64 = 360.0; // ~10 user items +const MOBILE_MAX_SCROLL_HEIGHT: f64 = 216.0; // ~6 user items const SEARCH_BUFFER_MULTIPLIER: usize = 2; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -425,11 +428,13 @@ script_mod! { } } - list +: { - height: Fit - clip_y: true - spacing: 0.0 - padding: Inset{top: 2 bottom: 2 left: 0 right: 0} + list_scroll +: { + height: 360 + list +: { + height: Fit + spacing: 0.0 + padding: Inset{top: 2 bottom: 2 left: 0 right: 0} + } } } @@ -1421,10 +1426,10 @@ impl MentionableTextInput { self.loading_indicator_ref = None; let is_desktop = cx.display_context.is_desktop(); - let max_visible_items: usize = if is_desktop { - DESKTOP_MAX_VISIBLE_ITEMS + let max_display_items: usize = if is_desktop { + DESKTOP_MAX_DISPLAY_ITEMS } else { - MOBILE_MAX_VISIBLE_ITEMS + MOBILE_MAX_DISPLAY_ITEMS }; let mut items_added = 0; @@ -1447,7 +1452,7 @@ impl MentionableTextInput { // Add user mention items using the results if !results_to_display.is_empty() { - let user_items_limit = max_visible_items.saturating_sub(has_room_item as usize); + let user_items_limit = max_display_items.saturating_sub(has_room_item as usize); let user_items_added = self.add_user_mention_items_from_results( cx, &results_to_display, @@ -1479,6 +1484,42 @@ impl MentionableTextInput { } } + // Dynamically adjust the scroll container height based on item count. + // ScrollYView needs a fixed height viewport. We default to 360px in the DSL, + // but shrink it when there are fewer items to keep the popup compact. + { + const USER_ITEM_HEIGHT: f64 = 36.0; + const ROOM_ITEM_HEIGHT: f64 = 40.0; + const STATUS_ITEM_HEIGHT: f64 = 48.0; + const LIST_PADDING: f64 = 4.0; // top: 2 + bottom: 2 + + let max_scroll_height = if is_desktop { + DESKTOP_MAX_SCROLL_HEIGHT + } else { + MOBILE_MAX_SCROLL_HEIGHT + }; + + // Estimate content height from items added + let content_height = if items_added == 0 { + 0.0 + } else { + let user_count = items_added.saturating_sub(has_room_item as usize) + .saturating_sub(if room_props.room_members_sync_pending { 1 } else { 0 }); + let room_count = has_room_item as usize; + let loading_count = if room_props.room_members_sync_pending { 1 } else { 0 }; + (user_count as f64 * USER_ITEM_HEIGHT) + + (room_count as f64 * ROOM_ITEM_HEIGHT) + + (loading_count as f64 * STATUS_ITEM_HEIGHT) + + LIST_PADDING + }; + + let scroll_height = content_height.min(max_scroll_height).max(0.0); + let scroll_view = self.cmd_text_input.list_scroll_view(cx); + if let Some(mut inner) = scroll_view.borrow_mut() { + inner.walk.height = Size::Fixed(scroll_height); + } + } + // Update popup visibility based on whether we have items self.update_popup_visibility(cx, scope, items_added > 0); @@ -1529,11 +1570,14 @@ impl MentionableTextInput { self.last_search_text = Some(search_text.to_string()); + // Reset scroll to top for a new search round + self.cmd_text_input.reset_list_scroll(cx); + let is_desktop = cx.display_context.is_desktop(); - let max_visible_items = if is_desktop { - DESKTOP_MAX_VISIBLE_ITEMS + let max_display_items = if is_desktop { + DESKTOP_MAX_DISPLAY_ITEMS } else { - MOBILE_MAX_VISIBLE_ITEMS + MOBILE_MAX_DISPLAY_ITEMS }; let members_sync_pending = room_props.room_members_sync_pending; let cached_member_count = room_props @@ -1596,7 +1640,7 @@ impl MentionableTextInput { // Prepare background search job parameters let search_text_clone = search_text.to_string(); - let max_results = max_visible_items * SEARCH_BUFFER_MULTIPLIER; + let max_results = max_display_items * SEARCH_BUFFER_MULTIPLIER; let search_id = self.allocate_search_id(); // Transition to Searching state with new receiver @@ -1738,8 +1782,12 @@ impl MentionableTextInput { // Ensure header is visible header_view.set_visible(cx, true); - // Don't manually set popup height for loading - let it auto-size based on content - // This avoids conflicts with list +: { height: Fill } + // Set scroll container height to fit the loading indicator (48px + 4px padding) + let scroll_view = self.cmd_text_input.list_scroll_view(cx); + if let Some(mut inner) = scroll_view.borrow_mut() { + inner.walk.height = Size::Fixed(52.0); + } + popup.set_visible(cx, true); // Maintain text input focus only if it currently has focus @@ -1770,8 +1818,11 @@ impl MentionableTextInput { // Ensure header is visible header_view.set_visible(cx, true); - // Let popup auto-size based on content - + // Set scroll container height to fit the no-matches indicator (48px + 4px padding) + let scroll_view = self.cmd_text_input.list_scroll_view(cx); + if let Some(mut inner) = scroll_view.borrow_mut() { + inner.walk.height = Size::Fixed(52.0); + } // Maintain text input focus so user can continue typing, but only if currently focused let text_input_area = self.cmd_text_input.text_input_ref().area(); From fe7aeec54d8996045215f9857522cece1f4b0080 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 07:21:20 +0800 Subject: [PATCH 090/283] Harden keyboard auto-scroll and add MAKEPAD.md skill routing - Remove stale list_scroll_y tracking; derive scroll offset from actual rendered list position (works after wheel/trackpad scroll) - Extract set_list_scroll_height() helper (3 call sites consolidated) - Add list_rect size guard in scroll_to_focused_item - Change ScrollYView DSL default from Fit to fixed 200px (compliance with Makepad layout rule: ScrollYView needs fixed viewport) - Add DSL comment linking hardcoded height to Rust constants - Add platform-specific scroll viewport heights (360px desktop, 216px mobile) - Add MAKEPAD.md with design judgment anchors and skill routing - Reference MAKEPAD.md from CLAUDE.md and AGENTS.md Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 1 + CLAUDE.md | 1 + MAKEPAD.md | 136 +++++++++++++++++++++++++++ src/shared/command_text_input.rs | 42 +++++---- src/shared/mentionable_text_input.rs | 25 ++--- 5 files changed, 175 insertions(+), 30 deletions(-) create mode 100644 MAKEPAD.md diff --git a/AGENTS.md b/AGENTS.md index b8cd5decd..8842d383c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ Before starting work, read these documents: 1. [DESIGN.md](DESIGN.md) — architecture overview, module organization, technology stack 2. [specs/project.spec.md](specs/project.spec.md) — project constraints, decisions, forbidden actions 3. [CLAUDE.md](CLAUDE.md) — project workflow rules and Makepad 2.0 guidance +4. [MAKEPAD.md](MAKEPAD.md) — Makepad 2.0 skill routing and design judgment entry point ## Critical Rules diff --git a/CLAUDE.md b/CLAUDE.md index ab6ccc264..46f865f37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,6 +7,7 @@ Before starting any task, read these documents: 1. **[DESIGN.md](DESIGN.md)** — Architecture overview, module organization, technology stack 2. **[specs/project.spec.md](specs/project.spec.md)** — Project-level constraints, decisions, and forbidden actions 3. **[AGENTS.md](AGENTS.md)** — Makepad 2.0 patterns and DSL syntax reference +4. **[MAKEPAD.md](MAKEPAD.md)** — Makepad 2.0 skill routing and design judgment entry point; **For ALL Makepad questions, FIRST load `makepad-2.0-design-judgment`.** ## Critical Rules diff --git a/MAKEPAD.md b/MAKEPAD.md new file mode 100644 index 000000000..f5d40ad5e --- /dev/null +++ b/MAKEPAD.md @@ -0,0 +1,136 @@ +# Makepad 2.0 Skills - Claude Instructions + +## Design Judgment Anchors (Liberation Layer) + +These concept anchors provide design judgment for Makepad 2.0 architecture questions. Use them when facing "how should I organize state / split components / handle complex interactions" — questions without a single correct answer. The specific DSL, API, and widget patterns come from the compliance-layer skills below; this section is the liberation layer. + +### Data Flow +Reference **Elm Architecture** (Evan Czaplicki): + +- State is centralized, UI is a projection of state, events trigger updates +- Makepad's event handlers are Elm's `update` function +- If you find state scattered across components that need to observe each other — stop, lift the state to a common ancestor + +### Component Decomposition +Reference **Dan Abramov**'s presentational vs container distinction: + +- Presentational components: receive props, hold no state, no side effects +- Container components: hold state, handle events, coordinate children +- Use Makepad's delegation patterns to separate Widget rendering from business logic + +### Rendering Mindset +Reference **Casey Muratori** (Handmade Hero): + +- This is not a DOM, it's a GPU-rendered frame every tick +- Don't think "mutate the node", think "what does the next frame look like" +- `redraw(cx)` is not "mark node dirty" — it's "tell the GPU to repaint this region next frame" + +### Layout +Reference **CSS Flexbox** as a mental model (but simpler): + +- `Flow.Down` = flex-direction: column +- `Flow.Right` = flex-direction: row +- `align`, `spacing`, `padding`, `margin` — same semantics as CSS +- Difference: Makepad has no CSS cascade or inheritance. Each component's style is self-contained — **this is a feature, not a bug** + +### Animation and Shaders +Reference the **Shadertoy** community's "everything is math" mindset: + +- Makepad shader fields contain real GPU shader code, not CSS-equivalents +- `Sdf2d` is a signed distance field — describe shapes with math, not bitmaps +- Animation is a shader uniform changing over time, not a CSS transition +- When you want a rounded button, the answer is an SDF function, not `border-radius` + +### Cross-Platform Philosophy +Reference **Flutter**'s "own every pixel" philosophy: + +- Makepad draws everything itself, does not use native platform controls +- Benefit: pixel-perfect cross-platform consistency +- Cost: accessibility support is a known weakness +- Don't try to mimic native control appearance — embrace Makepad's own design language + +### When Anchors Conflict with Compliance-Layer Skills + +If a design judgment from these anchors contradicts the actual Makepad 2.0 API documented in a compliance-layer skill, **the compliance-layer skill wins**. Those skills are the external reality. Anchors help you navigate within that reality, not override it. + +--- + +## Entry Point + +**For ALL Makepad questions, FIRST load `makepad-2.0-design-judgment`.** +This is the liberation layer — it provides design judgment anchors and routes +to the correct compliance-layer skill. Then co-load the specific skill below. + +## Skill Routing + +For Makepad 2.0 questions, route based on keywords: + +| Keywords | Skill | +|----------|-------| +| architecture, design, "how should I", component split, state management | makepad-2.0-design-judgment | +| getting started, app structure, `app_main!`, `ScriptVm`, Cargo setup | makepad-2.0-app-structure | +| DSL syntax, `script_mod!`, property, colon syntax, `mod.widgets` | makepad-2.0-dsl | +| layout, width, height, Flow, Fill, Fit, Inset, spacing, align | makepad-2.0-layout | +| View, Button, Label, TextInput, PortalList, Dock, Modal, widget | makepad-2.0-widgets | +| event, action, `handle_event`, `on_click`, `on_render`, Hit, ids! | makepad-2.0-events | +| animation, animator, state, transition, Forward, Snap, Loop | makepad-2.0-animation | +| shader, `draw_bg`, Sdf2d, GPU, pixel fn, vertex fn, DrawQuad | makepad-2.0-shaders | +| splash, script, `script_mod!`, hot reload, streaming evaluation | makepad-2.0-splash | +| theme, color, font, dark mode, light mode, `mod.themes` | makepad-2.0-theme | +| vector, SVG, path, gradient, tween, DropShadow, Group transform | makepad-2.0-vector | +| performance, debug, profiling, GC, `new_batch`, ViewOptimize | makepad-2.0-performance | +| troubleshooting, error, bug, widget not showing, text invisible | makepad-2.0-troubleshooting | +| migration, 1.x to 2.0, `live_design` to `script_mod`, upgrade | makepad-2.0-migration | + +## Usage Examples + +### App Structure +``` +User: "How do I create a Makepad 2.0 app?" +-> Load: makepad-2.0-app-structure +-> Answer with app_main!, ScriptVm, from_script_mod, MatchEvent +``` + +### DSL / Splash +``` +User: "How does the new Makepad DSL work?" +-> Load: makepad-2.0-dsl +-> Answer with script_mod!, colon syntax, mod.widgets, let bindings +``` + +### Layout +``` +User: "How do I center a widget in Makepad 2.0?" +-> Load: makepad-2.0-layout +-> Answer with Flow.Down, align, Fill, Fit +``` + +### Migration +``` +User: "How do I migrate from Makepad 1.x to 2.0?" +-> Load: makepad-2.0-migration +-> Answer with live_design→script_mod, LiveHook→ScriptHook changes +``` + +## Default Project Settings + +When creating Makepad 2.0 projects: + +```toml +[package] +edition = "2024" + +[dependencies] +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev" } + +[features] +default = [] +nightly = ["makepad-widgets/nightly"] +``` + +## Legacy + +Makepad 1.x skills (including Robius and MolyKit patterns) are archived on the `v1/makepad-1.0` branch. + +## Source +- **Makepad**: https://github.com/makepad/makepad diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index 4a61bd535..32ada4af7 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -113,7 +113,7 @@ script_mod! { list_scroll := ScrollYView { width: Fill - height: Fit + height: 200 clip_y: true list := mod.widgets.CommandTextInputList{ height: Fit @@ -214,10 +214,6 @@ pub struct CommandTextInput { #[rust] prev_cursor_position: usize, - /// Tracked Y scroll position of the list scroll view. - /// We maintain this ourselves because ViewRef has no public get_scroll_pos(). - #[rust] - list_scroll_y: f64, } impl Widget for CommandTextInput { @@ -561,7 +557,6 @@ impl CommandTextInput { /// Call this when starting a new search or closing the popup, /// NOT on every streaming result refresh (which would cause scroll jumping). pub fn reset_list_scroll(&mut self, cx: &mut Cx) { - self.list_scroll_y = 0.0; self.list_scroll_view(cx).set_scroll_pos(cx, DVec2 { x: 0.0, y: 0.0 }); } @@ -778,8 +773,9 @@ impl CommandTextInput { /// Scrolls the list scroll view so the currently focused item is visible. /// - /// Uses the tracked `list_scroll_y` instead of deriving scroll offset - /// from item rects (which is biased by list padding and fragile). + /// Derives the current scroll offset from the list content's screen position + /// relative to the scroll view, so it works correctly even after manual + /// wheel/trackpad scrolling (no stale tracked state). fn scroll_to_focused_item(&mut self, cx: &mut Cx) { let Some(focus_idx) = self.keyboard_focus_index else { return }; let Some(widget) = self.selectable_widgets.get(focus_idx) else { return }; @@ -795,24 +791,34 @@ impl CommandTextInput { let view_height = scroll_rect.size.y; - // The item's position within the visible viewport: - // When scrolled by `list_scroll_y`, the viewport shows content from - // [list_scroll_y .. list_scroll_y + view_height]. - // Item's screen position relative to scroll view top: + // Item's screen position relative to scroll view top. + // This accounts for any scroll state (programmatic or manual). let item_screen_top = item_rect.pos.y - scroll_rect.pos.y; let item_screen_bottom = item_screen_top + item_rect.size.y; + if item_screen_bottom <= view_height && item_screen_top >= 0.0 { + return; // Already fully visible, no scroll needed + } + + // Derive the current scroll offset from the list content's position. + // The list widget (inner content) is drawn at its full height inside the scroll view. + // When scroll=0, list top = scroll view top. When scrolled down by S, + // list top is S pixels above scroll view top. + let list_ref = self.list(cx, ids!(list)); + let list_rect = list_ref.as_view().area().rect(cx); + if list_rect.size.y <= 0.0 { + return; // List hasn't been drawn yet + } + let current_scroll = scroll_rect.pos.y - list_rect.pos.y; + let new_scroll_y = if item_screen_bottom > view_height { // Item extends below the visible area — scroll down - self.list_scroll_y + (item_screen_bottom - view_height) - } else if item_screen_top < 0.0 { - // Item is above the visible area — scroll up - (self.list_scroll_y + item_screen_top).max(0.0) + current_scroll + (item_screen_bottom - view_height) } else { - return; // Already fully visible, no scroll needed + // Item is above the visible area — scroll up + (current_scroll + item_screen_top).max(0.0) }; - self.list_scroll_y = new_scroll_y; scroll_view.set_scroll_pos(cx, DVec2 { x: 0.0, y: new_scroll_y }); } diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 5e80a6af4..ddb9d54ab 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -428,6 +428,8 @@ script_mod! { } } + // height below is the DSL default; Rust dynamically adjusts it + // per platform via set_list_scroll_height() and DESKTOP/MOBILE_MAX_SCROLL_HEIGHT. list_scroll +: { height: 360 list +: { @@ -929,6 +931,14 @@ impl MentionableTextInput { matches!(self.search_state, MentionSearchState::JustCancelled) } + /// Sets the scroll container's viewport height. + fn set_list_scroll_height(&self, cx: &Cx, height: f64) { + let scroll_view = self.cmd_text_input.list_scroll_view(cx); + if let Some(mut inner) = scroll_view.borrow_mut() { + inner.walk.height = Size::Fixed(height); + } + } + /// Tries to add the `@room` mention item to the list of selectable popup mentions. /// /// Returns true if @room item was added to the list and will be displayed in the popup. @@ -1514,10 +1524,7 @@ impl MentionableTextInput { }; let scroll_height = content_height.min(max_scroll_height).max(0.0); - let scroll_view = self.cmd_text_input.list_scroll_view(cx); - if let Some(mut inner) = scroll_view.borrow_mut() { - inner.walk.height = Size::Fixed(scroll_height); - } + self.set_list_scroll_height(cx, scroll_height); } // Update popup visibility based on whether we have items @@ -1783,10 +1790,7 @@ impl MentionableTextInput { header_view.set_visible(cx, true); // Set scroll container height to fit the loading indicator (48px + 4px padding) - let scroll_view = self.cmd_text_input.list_scroll_view(cx); - if let Some(mut inner) = scroll_view.borrow_mut() { - inner.walk.height = Size::Fixed(52.0); - } + self.set_list_scroll_height(cx, 52.0); popup.set_visible(cx, true); @@ -1819,10 +1823,7 @@ impl MentionableTextInput { header_view.set_visible(cx, true); // Set scroll container height to fit the no-matches indicator (48px + 4px padding) - let scroll_view = self.cmd_text_input.list_scroll_view(cx); - if let Some(mut inner) = scroll_view.borrow_mut() { - inner.walk.height = Size::Fixed(52.0); - } + self.set_list_scroll_height(cx, 52.0); // Maintain text input focus so user can continue typing, but only if currently focused let text_input_area = self.cmd_text_input.text_input_ref().area(); From 6e5214a21911b26c5a8ebbb648678eb96565d663 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 07:26:06 +0800 Subject: [PATCH 091/283] Fix stale viewport height between searches Reset scroll viewport height to zero in clear_popup() so a new search doesn't briefly show a blank tall popup left over from the previous search's viewport size. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/command_text_input.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index 32ada4af7..a3fd3fad6 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -529,6 +529,7 @@ impl CommandTextInput { ); self.clear_items(cx); self.reset_list_scroll(cx); + self.reset_list_scroll_height(cx); } /// Clears the list of items. @@ -560,6 +561,16 @@ impl CommandTextInput { self.list_scroll_view(cx).set_scroll_pos(cx, DVec2 { x: 0.0, y: 0.0 }); } + /// Resets the scroll viewport height to zero. + /// Prevents a stale tall viewport from showing as a blank box + /// between searches. + pub fn reset_list_scroll_height(&self, cx: &Cx) { + let scroll_view = self.list_scroll_view(cx); + if let Some(mut inner) = scroll_view.borrow_mut() { + inner.walk.height = Size::Fixed(0.0); + } + } + /// Add a custom unselectable item to the list. /// /// Ex: Headers, dividers, etc. From 3724068d2e3c1ea1368bc0f7e078cbdb012c4b8c Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 07:41:57 +0800 Subject: [PATCH 092/283] Remove hot-path debug logs and lower display limits for scroll perf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove 5 DEBUG log! calls from KeyDown, Actions iteration, finger_down, clear_items, and add_item — these fire every frame during scrolling and hurt fluidity - Lower display limits from 50/25 to 30/15 (desktop/mobile) to reduce per-frame draw cost of the non-virtualized List widget - Fix List.area access: use borrow() to read List's own area field instead of as_view().area() which returns the empty View deref area Co-Authored-By: Claude Opus 4.6 (1M context) --- src/shared/command_text_input.rs | 23 +++++++++++++---------- src/shared/mentionable_text_input.rs | 4 ++-- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/shared/command_text_input.rs b/src/shared/command_text_input.rs index a3fd3fad6..28d9d90e3 100644 --- a/src/shared/command_text_input.rs +++ b/src/shared/command_text_input.rs @@ -251,8 +251,6 @@ impl Widget for CommandTextInput { if cx.has_key_focus(self.key_controller_text_input_ref().area()) { if let Event::KeyDown(key_event) = event { let popup_visible = self.view(cx, ids!(popup)).visible(); - log!("DEBUG CommandTextInput::KeyDown: key={:?}, popup_visible={}, selectable_count={}, kb_focus={:?}", - key_event.key_code, popup_visible, self.selectable_widgets.len(), self.keyboard_focus_index); if popup_visible { let mut eat_the_event = true; @@ -316,14 +314,12 @@ impl Widget for CommandTextInput { let mut selected_by_click = None; let mut should_redraw = false; - log!("DEBUG CommandTextInput::Actions: self_ptr={:p}, selectable_count={}", self as *const _, self.selectable_widgets.len()); for (idx, item) in self.selectable_widgets.iter().enumerate() { let item = item.as_view(); let fd = item.finger_down(actions); if fd.as_ref().map(|fe| fe.tap_count == 1).unwrap_or(false) { - log!("DEBUG CommandTextInput: finger_down on item {}", idx); selected_by_click = Some((*item).clone()); // Clear keyboard focus when mouse is clicked @@ -536,7 +532,6 @@ impl CommandTextInput { /// /// Normally called as response to `should_build_items`. pub fn clear_items(&mut self, cx: &Cx) { - log!("DEBUG CommandTextInput::clear_items: self_ptr={:p}, was_count={}", self as *const _, self.selectable_widgets.len()); self.list(cx, ids!(list)).clear(); self.selectable_widgets.clear(); self.keyboard_focus_index = None; @@ -549,7 +544,6 @@ impl CommandTextInput { pub fn add_item(&mut self, cx: &Cx, widget: WidgetRef) { self.list(cx, ids!(list)).add(widget.clone()); self.selectable_widgets.push(widget); - log!("DEBUG CommandTextInput::add_item: self_ptr={:p}, new_count={}", self as *const _, self.selectable_widgets.len()); self.keyboard_focus_index = self.keyboard_focus_index.or(Some(0)); } @@ -815,11 +809,20 @@ impl CommandTextInput { // The list widget (inner content) is drawn at its full height inside the scroll view. // When scroll=0, list top = scroll view top. When scrolled down by S, // list top is S pixels above scroll view top. + // + // NOTE: Must access List.area directly via borrow(), NOT via .as_view().area(). + // List stores its drawn area in its own `area` field (set by end_turtle_with_area), + // while the deref View's area is never populated. let list_ref = self.list(cx, ids!(list)); - let list_rect = list_ref.as_view().area().rect(cx); - if list_rect.size.y <= 0.0 { - return; // List hasn't been drawn yet - } + let list_rect = if let Some(inner) = list_ref.borrow() { + let r = inner.area.rect(cx); + if r.size.y <= 0.0 { + return; // List hasn't been drawn yet + } + r + } else { + return; + }; let current_scroll = scroll_rect.pos.y - list_rect.pos.y; let new_scroll_y = if item_screen_bottom > view_height { diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index ddb9d54ab..d8fdf5b87 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -159,8 +159,8 @@ enum MentionSearchState { // Constants for mention popup sizing and search behavior. // MAX_DISPLAY_ITEMS: total items loaded into the scrollable list. // MAX_SCROLL_HEIGHT: maximum pixel height of the scroll viewport. -const DESKTOP_MAX_DISPLAY_ITEMS: usize = 50; -const MOBILE_MAX_DISPLAY_ITEMS: usize = 25; +const DESKTOP_MAX_DISPLAY_ITEMS: usize = 30; +const MOBILE_MAX_DISPLAY_ITEMS: usize = 15; const DESKTOP_MAX_SCROLL_HEIGHT: f64 = 360.0; // ~10 user items const MOBILE_MAX_SCROLL_HEIGHT: f64 = 216.0; // ~6 user items const SEARCH_BUFFER_MULTIPLIER: usize = 2; From 1f3860080b0a7a1fdff3404a51bd77b67c5e179a Mon Sep 17 00:00:00 2001 From: AlexZ Date: Mon, 6 Apr 2026 07:43:21 +0800 Subject: [PATCH 093/283] Update spec to match final implementation - Fixed height viewport (not Fit{max}), dynamically sized in Rust - Display limits 30/15 (not 50/25) for scroll performance - Document List.area access pattern (borrow vs as_view) - Document scroll offset derivation from rendered positions - Note hot-path log removal as a decision Co-Authored-By: Claude Opus 4.6 (1M context) --- specs/task-mention-list-scrolling.spec.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/specs/task-mention-list-scrolling.spec.md b/specs/task-mention-list-scrolling.spec.md index 641bde5a4..e6ea2c2cc 100644 --- a/specs/task-mention-list-scrolling.spec.md +++ b/specs/task-mention-list-scrolling.spec.md @@ -43,19 +43,23 @@ Keyboard navigation (ArrowUp/Down) is managed via `keyboard_focus_index` in `Com ### Scenario: Popup height is bounded - **Given** a room with 500+ members - **When** the popup shows unfiltered results -- **Then** the popup height does not exceed a reasonable maximum (~400px desktop, ~250px mobile) +- **Then** the popup height does not exceed a reasonable maximum (~360px desktop, ~216px mobile) - **And** the popup does not overflow the screen ## Decisions - Wrap the `List` widget in a `ScrollYView` named `list_scroll` in the CommandTextInput DSL — do NOT rewrite List's draw logic -- Use `height: Fit{max: Abs(360)}` on `list_scroll` in MentionableTextInput DSL for auto-sizing with a cap (native Makepad bounded Fit) -- Add separate `DESKTOP_MAX_DISPLAY_ITEMS = 50` / `MOBILE_MAX_DISPLAY_ITEMS = 25` constants for the scrollable list item limit -- Also increase backend search limit (`max_results`) to use display limits — otherwise CPU search caps at 20/10 results -- Keep existing `MAX_VISIBLE_ITEMS` constants for height reference only -- Keyboard navigation (`on_keyboard_move`) auto-scrolls via `set_scroll_pos()` with manual position calculation — `scroll_bars_obj` is private -- Add `reset_list_scroll()` called from `clear_popup()` and new-search-start — NOT from `clear_items()` (which runs on every streaming refresh and would cause scroll jumping) +- ScrollYView requires a fixed height viewport (NOT `Fit` or `Fit{max}`). DSL default is `height: 200`; Rust dynamically sets the height via `walk.height = Size::Fixed(...)` based on item count and platform +- `DESKTOP_MAX_DISPLAY_ITEMS = 30` / `MOBILE_MAX_DISPLAY_ITEMS = 15` — display limits for the scrollable list. Kept at 30/15 (not 50) for scroll performance since List is non-virtualized +- `DESKTOP_MAX_SCROLL_HEIGHT = 360.0` / `MOBILE_MAX_SCROLL_HEIGHT = 216.0` — platform-specific viewport height caps +- Backend search limit (`max_results`) uses `max_display_items * SEARCH_BUFFER_MULTIPLIER` to ensure enough results +- Old `MAX_VISIBLE_ITEMS` constants removed — no longer needed +- Keyboard auto-scroll derives current scroll offset from `List.area` rendered position (not a tracked field), so it works correctly after manual wheel/trackpad scrolling +- Must access `List.area` via `borrow()`, NOT via `.as_view().area()` — List stores its drawn area in its own `area` field, not in the deref View's area +- `reset_list_scroll()` and `reset_list_scroll_height()` called from `clear_popup()` — NOT from `clear_items()` (which runs on every streaming refresh) +- `reset_list_scroll()` also called at new-search-start in `start_background_search()` - `clip_y: true` on `list_scroll` to prevent content leaking past rounded corners +- Hot-path `log!` calls removed from KeyDown, Actions, clear_items, add_item for scroll performance ## Boundaries @@ -68,7 +72,7 @@ Keyboard navigation (ArrowUp/Down) is managed via `keyboard_focus_index` in `Com - Do NOT change the trigger mechanism or search logic - Do NOT change the mention insertion or tracking behavior - Do NOT change the highlight/Animator system -- Do NOT use `PortalList` (virtual scrolling) — overkill for 50 items, and would require rewriting item instantiation +- Do NOT use `PortalList` (virtual scrolling) — would require rewriting item instantiation; deferred as future optimization if 30 items is still too slow - Do NOT run `cargo fmt` ## Completion Criteria From 7c97396a5c6cd0991bfba0ce65239ffee58bde54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 13:52:52 +0800 Subject: [PATCH 094/283] Update i18n --- resources/i18n/en.json | 75 +++++++++ resources/i18n/zh-CN.json | 73 +++++++++ src/app.rs | 12 +- src/home/navigation_tab_bar.rs | 19 ++- src/home/new_message_context_menu.rs | 42 ++++- src/join_leave_room_modal.rs | 229 ++++++++++++++------------- src/login/login_screen.rs | 12 +- src/shared/verification_badge.rs | 10 +- 8 files changed, 336 insertions(+), 136 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ef2daeda5..2f19d9ed6 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -37,6 +37,7 @@ "login.status.auto_logging_in_as_user": "Auto-logging in as user {user_id}...", "login.status.account_creation_failed": "Account Creation Failed.", "login.status.login_failed": "Login Failed.", + "login.status.account_switch_failed": "Account Switch Failed.", "login.status.okay": "Okay", "login.status.cancel": "Cancel", "login_status_modal.title": "Login Status", @@ -215,6 +216,59 @@ "invite_modal.status.invalid_user_id": "Invalid User ID. Expected format: @user:server.xyz", "invite_modal.status.success_invited": "Successfully invited {user_id}!", "invite_modal.status.send_failed": "Failed to send invite: {error}", + + "join_leave_modal.button.cancel": "Cancel", + "join_leave_modal.button.yes": "Yes", + "join_leave_modal.button.okay": "Okay", + "join_leave_modal.button.join": "Join", + "join_leave_modal.button.reject": "Reject", + "join_leave_modal.button.leave": "Leave", + "join_leave_modal.button.joining": "Joining...", + "join_leave_modal.button.rejecting": "Rejecting...", + "join_leave_modal.button.leaving": "Leaving...", + "join_leave_modal.word.room": "room", + "join_leave_modal.word.space": "space", + "join_leave_modal.title.accepting_invite": "Accepting this invite...", + "join_leave_modal.description.accepting_invite": "Accepting an invitation to join \"{room_name}\".\n\nWaiting for confirmation from the homeserver...", + "join_leave_modal.title.rejecting_invite": "Rejecting this invite...", + "join_leave_modal.description.rejecting_invite": "Rejecting an invitation to join \"{room_name}\".\n\nWaiting for confirmation from the homeserver...", + "join_leave_modal.title.joining_target": "Joining this {target}...", + "join_leave_modal.description.joining": "Joining \"{room_name}\".\n\nWaiting for confirmation from the homeserver...", + "join_leave_modal.title.leaving_room": "Leaving this room...", + "join_leave_modal.title.leaving_space": "Leaving this space...", + "join_leave_modal.description.leaving": "Leaving \"{room_name}\".\n\nWaiting for confirmation from the homeserver...", + "join_leave_modal.popup.leave_space_request_failed": "Failed to send leave space request.\n\nPlease restart Robrix.", + "join_leave_modal.popup.joined_success": "Successfully joined room.", + "join_leave_modal.title.joined_room": "Joined room!", + "join_leave_modal.description.joined_room": "Successfully joined \"{room_name}\".", + "join_leave_modal.title.error_joining_room": "Error joining room!", + "join_leave_modal.title.rejected_invite": "Rejected invite!", + "join_leave_modal.description.rejected_invite": "Successfully rejected invite to \"{room_name}\".", + "join_leave_modal.popup.rejected_success": "Successfully rejected invite.", + "join_leave_modal.title.left_room": "Left room!", + "join_leave_modal.description.left_room": "Successfully left \"{room_name}\".", + "join_leave_modal.popup.left_room_success": "Successfully left room.", + "join_leave_modal.title.error_rejecting_invite": "Error rejecting invite!", + "join_leave_modal.popup.reject_failed": "Failed to reject invite.", + "join_leave_modal.title.error_leaving_room": "Error leaving room!", + "join_leave_modal.popup.leave_failed": "Failed to leave room.", + "join_leave_modal.title.left_space": "Left space!", + "join_leave_modal.description.left_space": "Successfully left \"{space_name}\".", + "join_leave_modal.title.error_leaving_space": "Error leaving space!", + "join_leave_modal.description.error_leaving_space": "Failed to leave space \"{space_name}\".\n\nError: {error}", + "join_leave_modal.title.confirm_accept_invite": "Accept this invite?", + "join_leave_modal.description.confirm_accept_invite": "Are you sure you want to accept this invite to join \"{room_name}\"?", + "join_leave_modal.title.confirm_reject_invite": "Reject this invite?", + "join_leave_modal.description.confirm_reject_invite": "Are you sure you want to reject this invite to join \"{room_name}\"?\n\nIf this is a private room, you won't be able to join this room without being re-invited to it.", + "join_leave_modal.title.confirm_join_space": "Join this space?", + "join_leave_modal.title.confirm_join_room": "Join this room?", + "join_leave_modal.description.confirm_join": "Are you sure you want to join \"{room_name}\"?", + "join_leave_modal.title.confirm_leave_room": "Leave this room?", + "join_leave_modal.description.confirm_leave_room": "Are you sure you want to leave \"{room_name}\"?\n\nIf this is a private room, you won't be able to join this room without being re-invited to it.", + "join_leave_modal.title.confirm_leave_space": "Leave this space?", + "join_leave_modal.description.confirm_leave_space": "Are you sure you want to leave \"{room_name}\"?\n\nIf you leave this space, you will also leave any joined rooms within this space.\n\nIf this is a private space, you won't be able to join this space without being re-invited to it.", + "join_leave_modal.tip.with_button": "Tip: hold Shift when clicking the \"{button_text}\" button to bypass this prompt.", + "rooms_list_entry.invited.by_name_and_user": "Invited by {display_name} ({user_id})", "rooms_list_entry.invited.by_user": "Invited by {user_id}", "rooms_list_entry.invited.generic": "You were invited", @@ -234,6 +288,25 @@ "room_filter_input.placeholder": "Filter rooms & spaces...", "search_messages.button.todo": "Search (TODO)", + "verification_badge.tooltip.verified": "This device is fully verified.", + "verification_badge.tooltip.unverified": "This device is unverified. To view your encrypted message history, please verify Robrix from another client.", + "verification_badge.tooltip.unknown": "Verification state is unknown.", + "navigation_tab_bar.profile.tooltip.not_logged_in": "Not logged in.\n\n{verification}", + "navigation_tab_bar.profile.tooltip.logged_in_as": "Logged in as \"{display_name}\".\n\n{verification}", + "new_message_context_menu.button.add_reaction": "Add Reaction", + "new_message_context_menu.input.reaction_placeholder": "Enter reaction...", + "new_message_context_menu.button.reply": "Reply", + "new_message_context_menu.button.open_thread": "Open Thread", + "new_message_context_menu.button.reply_in_thread": "Reply in Thread", + "new_message_context_menu.button.edit_message": "Edit Message", + "new_message_context_menu.button.pin_message": "Pin Message", + "new_message_context_menu.button.unpin_message": "Unpin Message", + "new_message_context_menu.button.copy_text": "Copy Text", + "new_message_context_menu.button.copy_text_html": "Copy Text as HTML", + "new_message_context_menu.button.copy_link": "Copy Link to Message", + "new_message_context_menu.button.view_source": "View Source", + "new_message_context_menu.button.jump_related": "Jump to Related Event", + "new_message_context_menu.button.delete": "Delete", "welcome_screen.title": "Welcome to Robrix!", "welcome_screen.body_html": "

Our Matrix client is under heavy development. Currently, you can access the rooms and spaces that you've joined in other clients.


But don't worry, we're constantly expanding the featureset of Robrix!


Look for the latest announcements in our Matrix channel:

#robrix:matrix.org

", @@ -388,6 +461,8 @@ "app.room_filter.search_results_title": "Search Results", "app.room_filter.empty_hint": "Type to search rooms and spaces...", "app.room_filter.no_local_results": "No local results for \"{keywords}\". Choose a type below to search server.", + "app.room_filter.no_server_results": "No server results for \"{query}\".", + "app.room_filter.search_remote_failed": "Server search failed: {error}", "app.room_filter.searching_remote": "Searching {kind} on server...", "app.room_filter.remote.people": "People", "app.room_filter.remote.rooms": "Rooms", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index f7f4ee455..5c45a9992 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -37,6 +37,7 @@ "login.status.auto_logging_in_as_user": "正在以用户 {user_id} 自动登录...", "login.status.account_creation_failed": "账号创建失败。", "login.status.login_failed": "登录失败。", + "login.status.account_switch_failed": "切换账号失败。", "login.status.okay": "确定", "login.status.cancel": "取消", "login_status_modal.title": "登录状态", @@ -215,6 +216,57 @@ "invite_modal.status.invalid_user_id": "无效的用户 ID。应为格式:@user:server.xyz", "invite_modal.status.success_invited": "已成功邀请 {user_id}!", "invite_modal.status.send_failed": "发送邀请失败:{error}", + "join_leave_modal.button.cancel": "取消", + "join_leave_modal.button.yes": "是", + "join_leave_modal.button.okay": "确定", + "join_leave_modal.button.join": "加入", + "join_leave_modal.button.reject": "拒绝", + "join_leave_modal.button.leave": "离开", + "join_leave_modal.button.joining": "加入中...", + "join_leave_modal.button.rejecting": "拒绝中...", + "join_leave_modal.button.leaving": "离开中...", + "join_leave_modal.word.room": "房间", + "join_leave_modal.word.space": "空间", + "join_leave_modal.title.accepting_invite": "正在接受邀请...", + "join_leave_modal.description.accepting_invite": "正在接受加入“{room_name}”的邀请。\n\n正在等待服务器确认...", + "join_leave_modal.title.rejecting_invite": "正在拒绝邀请...", + "join_leave_modal.description.rejecting_invite": "正在拒绝加入“{room_name}”的邀请。\n\n正在等待服务器确认...", + "join_leave_modal.title.joining_target": "正在加入此{target}...", + "join_leave_modal.description.joining": "正在加入“{room_name}”。\n\n正在等待服务器确认...", + "join_leave_modal.title.leaving_room": "正在离开此房间...", + "join_leave_modal.title.leaving_space": "正在离开此空间...", + "join_leave_modal.description.leaving": "正在离开“{room_name}”。\n\n正在等待服务器确认...", + "join_leave_modal.popup.leave_space_request_failed": "发送离开空间请求失败。\n\n请重启 Robrix。", + "join_leave_modal.popup.joined_success": "已成功加入房间。", + "join_leave_modal.title.joined_room": "已加入房间!", + "join_leave_modal.description.joined_room": "已成功加入“{room_name}”。", + "join_leave_modal.title.error_joining_room": "加入房间出错!", + "join_leave_modal.title.rejected_invite": "已拒绝邀请!", + "join_leave_modal.description.rejected_invite": "已成功拒绝加入“{room_name}”的邀请。", + "join_leave_modal.popup.rejected_success": "已成功拒绝邀请。", + "join_leave_modal.title.left_room": "已离开房间!", + "join_leave_modal.description.left_room": "已成功离开“{room_name}”。", + "join_leave_modal.popup.left_room_success": "已成功离开房间。", + "join_leave_modal.title.error_rejecting_invite": "拒绝邀请出错!", + "join_leave_modal.popup.reject_failed": "拒绝邀请失败。", + "join_leave_modal.title.error_leaving_room": "离开房间出错!", + "join_leave_modal.popup.leave_failed": "离开房间失败。", + "join_leave_modal.title.left_space": "已离开空间!", + "join_leave_modal.description.left_space": "已成功离开“{space_name}”。", + "join_leave_modal.title.error_leaving_space": "离开空间出错!", + "join_leave_modal.description.error_leaving_space": "离开空间“{space_name}”失败。\n\n错误:{error}", + "join_leave_modal.title.confirm_accept_invite": "接受此邀请?", + "join_leave_modal.description.confirm_accept_invite": "确定要接受加入“{room_name}”的邀请吗?", + "join_leave_modal.title.confirm_reject_invite": "拒绝此邀请?", + "join_leave_modal.description.confirm_reject_invite": "确定要拒绝加入“{room_name}”的邀请吗?\n\n如果这是私密房间,除非再次被邀请,否则你将无法加入。", + "join_leave_modal.title.confirm_join_space": "加入此空间?", + "join_leave_modal.title.confirm_join_room": "加入此房间?", + "join_leave_modal.description.confirm_join": "确定要加入“{room_name}”吗?", + "join_leave_modal.title.confirm_leave_room": "离开此房间?", + "join_leave_modal.description.confirm_leave_room": "确定要离开“{room_name}”吗?\n\n如果这是私密房间,除非再次被邀请,否则你将无法加入。", + "join_leave_modal.title.confirm_leave_space": "离开此空间?", + "join_leave_modal.description.confirm_leave_space": "确定要离开“{room_name}”吗?\n\n离开该空间后,你也会离开该空间下所有已加入的房间。\n\n如果这是私密空间,除非再次被邀请,否则你将无法加入。", + "join_leave_modal.tip.with_button": "提示:点击“{button_text}”按钮时按住 Shift 可跳过此确认。", "rooms_list_entry.invited.by_name_and_user": "由 {display_name} ({user_id}) 邀请", "rooms_list_entry.invited.by_user": "由 {user_id} 邀请", "rooms_list_entry.invited.generic": "你收到了邀请", @@ -234,6 +286,25 @@ "room_filter_input.placeholder": "筛选房间与空间...", "search_messages.button.todo": "搜索(待实现)", + "verification_badge.tooltip.verified": "此设备已完全验证。", + "verification_badge.tooltip.unverified": "此设备尚未验证。若要查看加密消息历史,请在其他客户端中验证 Robrix。", + "verification_badge.tooltip.unknown": "验证状态未知。", + "navigation_tab_bar.profile.tooltip.not_logged_in": "尚未登录。\n\n{verification}", + "navigation_tab_bar.profile.tooltip.logged_in_as": "当前登录为“{display_name}”。\n\n{verification}", + "new_message_context_menu.button.add_reaction": "添加表情反应", + "new_message_context_menu.input.reaction_placeholder": "输入表情反应...", + "new_message_context_menu.button.reply": "回复", + "new_message_context_menu.button.open_thread": "打开线程", + "new_message_context_menu.button.reply_in_thread": "在线程中回复", + "new_message_context_menu.button.edit_message": "编辑消息", + "new_message_context_menu.button.pin_message": "置顶消息", + "new_message_context_menu.button.unpin_message": "取消置顶消息", + "new_message_context_menu.button.copy_text": "复制文本", + "new_message_context_menu.button.copy_text_html": "复制文本为 HTML", + "new_message_context_menu.button.copy_link": "复制消息链接", + "new_message_context_menu.button.view_source": "查看源码", + "new_message_context_menu.button.jump_related": "跳转到关联事件", + "new_message_context_menu.button.delete": "删除", "welcome_screen.title": "欢迎来到 Robrix!", "welcome_screen.body_html": "

我们的 Matrix 客户端仍在快速开发中。目前,你可以访问你在其他客户端中已加入的房间和空间。


不过别担心,我们正在持续扩展 Robrix 的功能!


欢迎在我们的 Matrix 频道查看最新公告:

#robrix:matrix.org

", @@ -388,6 +459,8 @@ "app.room_filter.search_results_title": "搜索结果", "app.room_filter.empty_hint": "输入关键词以搜索房间和空间...", "app.room_filter.no_local_results": "本地未找到“{keywords}”相关结果。请选择下方类型以搜索服务器。", + "app.room_filter.no_server_results": "服务器中没有“{query}”的结果。", + "app.room_filter.search_remote_failed": "服务器搜索失败:{error}", "app.room_filter.searching_remote": "正在服务器上搜索{kind}...", "app.room_filter.remote.people": "联系人", "app.room_filter.remote.rooms": "房间", diff --git a/src/app.rs b/src/app.rs index 8c263d991..f16d0838d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -890,7 +890,9 @@ impl MatchEvent for App { if self.room_filter_modal_results.is_empty() { self.set_room_filter_modal_empty_state( cx, - &format!("No server results for \"{}\".", query), + &tr_fmt(self.app_state.app_language, "app.room_filter.no_server_results", &[ + ("query", query), + ]), true, ); } else { @@ -908,7 +910,9 @@ impl MatchEvent for App { self.refresh_room_filter_modal_result_buttons(cx); self.set_room_filter_modal_empty_state( cx, - &format!("Server search failed: {}", error), + &tr_fmt(self.app_state.app_language, "app.room_filter.search_remote_failed", &[ + ("error", error), + ]), true, ); continue; @@ -928,7 +932,7 @@ impl MatchEvent for App { if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); - let expected_dimensions = new_message_context_menu.show(cx, details); + let expected_dimensions = new_message_context_menu.show(cx, details, self.app_state.app_language); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(abs_pos.x, rect.size.x - expected_dimensions.x); @@ -1152,7 +1156,7 @@ impl MatchEvent for App { Some(JoinLeaveRoomModalAction::Open { kind, show_tip }) => { self.ui .join_leave_room_modal(cx, ids!(join_leave_modal_inner)) - .set_kind(cx, kind.clone(), *show_tip); + .set_kind(cx, kind.clone(), *show_tip, self.app_state.app_language); self.ui.modal(cx, ids!(join_leave_modal)).open(cx); continue; } diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index 1766fa943..0d6b6bedd 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -31,7 +31,7 @@ use makepad_widgets::*; use serde::{Deserialize, Serialize}; use crate::{ - avatar_cache::{self, AvatarCacheEntry}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, profile::{ + app::AppState, avatar_cache::{self, AvatarCacheEntry}, i18n::{AppLanguage, tr_fmt}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, profile::{ user_profile::UserProfile, user_profile_cache::{self, UserProfileUpdate}, }, home::spaces_bar::SpacesBarWidgetExt, shared::{ @@ -230,6 +230,7 @@ script_mod! { pub struct ProfileIcon { #[deref] view: View, #[rust] own_profile: Option, + #[rust] app_language: AppLanguage, } impl ScriptHook for ProfileIcon { @@ -244,6 +245,11 @@ impl ScriptHook for ProfileIcon { impl Widget for ProfileIcon { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.app_language = app_language; + if self.own_profile.is_none() { self.own_profile = get_own_profile(cx); } @@ -348,10 +354,15 @@ impl Widget for ProfileIcon { Hit::FingerLongPress(_) | Hit::FingerHoverIn(_) => { let (verification_str, bg_color) = self.view .verification_badge(cx, ids!(verification_badge)) - .tooltip_content(); + .tooltip_content(self.app_language); let text = self.own_profile.as_ref().map_or_else( - || format!("Not logged in.\n\n{}", verification_str), - |p| format!("Logged in as \"{}\".\n\n{}", p.displayable_name(), verification_str) + || tr_fmt(self.app_language, "navigation_tab_bar.profile.tooltip.not_logged_in", &[ + ("verification", verification_str.as_str()), + ]), + |p| tr_fmt(self.app_language, "navigation_tab_bar.profile.tooltip.logged_in_as", &[ + ("display_name", p.displayable_name()), + ("verification", verification_str.as_str()), + ]), ); let mut options = CalloutTooltipOptions { position: if cx.display_context.is_desktop() { TooltipPosition::Right} else { TooltipPosition::Top}, diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index b7552b733..21df36931 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -6,7 +6,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedEventId; use matrix_sdk_ui::timeline::{EventTimelineItem, MsgLikeContent, TimelineEventItemId}; -use crate::sliding_sync::UserPowerLevels; +use crate::{i18n::{AppLanguage, tr_key}, sliding_sync::UserPowerLevels}; use super::room_screen::MessageAction; @@ -300,6 +300,7 @@ pub struct NewMessageContextMenu { #[deref] view: View, #[source] source: ScriptObjectRef, #[rust] details: Option, + #[rust] app_language: AppLanguage, } impl Widget for NewMessageContextMenu { @@ -485,6 +486,30 @@ impl WidgetMatchEvent for NewMessageContextMenu { } impl NewMessageContextMenu { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.view.button(cx, ids!(react_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.add_reaction")); + self.view.text_input(cx, ids!(reaction_input_view.reaction_text_input)) + .set_empty_text(cx, tr_key(self.app_language, "new_message_context_menu.input.reaction_placeholder").to_string()); + self.view.button(cx, ids!(reply_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.reply")); + self.view.button(cx, ids!(edit_message_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.edit_message")); + self.view.button(cx, ids!(copy_text_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.copy_text")); + self.view.button(cx, ids!(copy_html_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.copy_text_html")); + self.view.button(cx, ids!(copy_link_to_message_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.copy_link")); + self.view.button(cx, ids!(view_source_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.view_source")); + self.view.button(cx, ids!(jump_to_related_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.jump_related")); + self.view.button(cx, ids!(delete_button)) + .set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.delete")); + } + /// Returns `true` if this menu is currently being shown. pub fn is_currently_shown(&self, _cx: &mut Cx) -> bool { self.visible @@ -494,7 +519,8 @@ impl NewMessageContextMenu { /// /// Returns the expected (approximate) dimensions of the context menu, /// which can be used to proactively reposition it such that it fits on screen. - pub fn show(&mut self, cx: &mut Cx, details: MessageDetails) -> DVec2 { + pub fn show(&mut self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage) -> DVec2 { + self.set_app_language(cx, app_language); self.details = Some(details); self.visible = true; cx.set_key_focus(self.view.area()); @@ -550,15 +576,15 @@ impl NewMessageContextMenu { self.view.view(cx, ids!(divider_after_react_reply)).set_visible(cx, show_divider_after_react_reply); edit_button.set_visible(cx, show_edit); if details.thread_root_event_id.is_some() { - thread_button.set_text(cx, "Open Thread"); + thread_button.set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.open_thread")); } else { - thread_button.set_text(cx, "Reply in Thread"); + thread_button.set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.reply_in_thread")); } if details.abilities.contains(MessageAbilities::CanPin) { - pin_button.set_text(cx, "Pin Message"); + pin_button.set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.pin_message")); show_pin = true; } else if details.abilities.contains(MessageAbilities::CanUnpin) { - pin_button.set_text(cx, "Unpin Message"); + pin_button.set_text(cx, tr_key(self.app_language, "new_message_context_menu.button.unpin_message")); show_pin = true; } else { show_pin = false; @@ -628,8 +654,8 @@ impl NewMessageContextMenuRef { } /// See [`NewMessageContextMenu::show()`]. - pub fn show(&self, cx: &mut Cx, details: MessageDetails) -> DVec2 { + pub fn show(&self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; - inner.show(cx, details) + inner.show(cx, details, app_language) } } diff --git a/src/join_leave_room_modal.rs b/src/join_leave_room_modal.rs index eb8f5632c..b7ffed55e 100644 --- a/src/join_leave_room_modal.rs +++ b/src/join_leave_room_modal.rs @@ -8,7 +8,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use tokio::sync::mpsc::UnboundedSender; -use crate::{home::invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, room::BasicRoomDetails, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_negative_button_style, apply_neutral_button_style, apply_positive_button_style, apply_primary_button_style}}, sliding_sync::{MatrixRequest, submit_async_request}, space_service_sync::{SpaceRequest, SpaceRoomListAction}, utils::{self, RoomNameId}}; +use crate::{app::AppState, home::invite_screen::{InviteDetails, JoinRoomResultAction, LeaveRoomResultAction}, i18n::{AppLanguage, tr_fmt, tr_key}, room::BasicRoomDetails, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_negative_button_style, apply_neutral_button_style, apply_positive_button_style, apply_primary_button_style}}, sliding_sync::{MatrixRequest, submit_async_request}, space_service_sync::{SpaceRequest, SpaceRoomListAction}, utils::{self, RoomNameId}}; script_mod! { use mod.prelude.widgets.* @@ -116,6 +116,7 @@ script_mod! { pub struct JoinLeaveRoomModal { #[deref] view: View, #[rust] kind: Option, + #[rust] app_language: AppLanguage, /// Whether the modal is in a final state, meaning the user can only click "Okay" to close it. /// /// * Set to `Some(true)` after a successful action (e.g., joining or leaving a room). @@ -205,6 +206,10 @@ pub enum JoinLeaveRoomModalAction { impl Widget for JoinLeaveRoomModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + self.app_language = app_language; self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } @@ -244,66 +249,68 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let accept_button_text: &str; match kind { JoinLeaveModalKind::AcceptInvite(invite) => { - title = "Accepting this invite...".into(); - description = format!( - "Accepting an invitation to join \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - invite.room_name_id(), - ); - accept_button_text = "Joining..."; + let room_name = invite.room_name_id().to_string(); + title = tr_key(self.app_language, "join_leave_modal.title.accepting_invite").into(); + description = tr_fmt(self.app_language, "join_leave_modal.description.accepting_invite", &[ + ("room_name", room_name.as_str()), + ]); + accept_button_text = tr_key(self.app_language, "join_leave_modal.button.joining"); submit_async_request(MatrixRequest::JoinRoom { room_id: invite.room_id().clone(), }); } JoinLeaveModalKind::RejectInvite(invite) => { - title = "Rejecting this invite...".into(); - description = format!( - "Rejecting an invitation to join \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - invite.room_name_id(), - ); - accept_button_text = "Rejecting..."; + let room_name = invite.room_name_id().to_string(); + title = tr_key(self.app_language, "join_leave_modal.title.rejecting_invite").into(); + description = tr_fmt(self.app_language, "join_leave_modal.description.rejecting_invite", &[ + ("room_name", room_name.as_str()), + ]); + accept_button_text = tr_key(self.app_language, "join_leave_modal.button.rejecting"); submit_async_request(MatrixRequest::LeaveRoom { room_id: invite.room_id().clone(), }); } JoinLeaveModalKind::JoinRoom { details, is_space } => { - title = format!("Joining this {}...", if *is_space { "space" } else { "room" }).into(); - description = format!( - "Joining \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - details.room_name_id(), - ); - accept_button_text = "Joining..."; + let room_name = details.room_name_id().to_string(); + let target = if *is_space { + tr_key(self.app_language, "join_leave_modal.word.space") + } else { + tr_key(self.app_language, "join_leave_modal.word.room") + }; + title = tr_fmt(self.app_language, "join_leave_modal.title.joining_target", &[ + ("target", target), + ]).into(); + description = tr_fmt(self.app_language, "join_leave_modal.description.joining", &[ + ("room_name", room_name.as_str()), + ]); + accept_button_text = tr_key(self.app_language, "join_leave_modal.button.joining"); submit_async_request(MatrixRequest::JoinRoom { room_id: details.room_id().clone(), }); } JoinLeaveModalKind::LeaveRoom(room) => { - title = "Leaving this room...".into(); - description = format!( - "Leaving \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - room.room_name_id(), - ); - accept_button_text = "Leaving..."; + let room_name = room.room_name_id().to_string(); + title = tr_key(self.app_language, "join_leave_modal.title.leaving_room").into(); + description = tr_fmt(self.app_language, "join_leave_modal.description.leaving", &[ + ("room_name", room_name.as_str()), + ]); + accept_button_text = tr_key(self.app_language, "join_leave_modal.button.leaving"); submit_async_request(MatrixRequest::LeaveRoom { room_id: room.room_id().clone(), }); } JoinLeaveModalKind::LeaveSpace { details, space_request_sender } => { - title = "Leaving this space...".into(); - description = format!( - "Leaving \"{}\".\n\n\ - Waiting for confirmation from the homeserver...", - details.room_name_id(), - ); - accept_button_text = "Leaving..."; + let room_name = details.room_name_id().to_string(); + title = tr_key(self.app_language, "join_leave_modal.title.leaving_space").into(); + description = tr_fmt(self.app_language, "join_leave_modal.description.leaving", &[ + ("room_name", room_name.as_str()), + ]); + accept_button_text = tr_key(self.app_language, "join_leave_modal.button.leaving"); if space_request_sender.send( SpaceRequest::LeaveSpace { space_name_id: details.room_name_id().clone() } ).is_err() { enqueue_popup_notification( - "Failed to send leave space request.\n\nPlease restart Robrix.", + tr_key(self.app_language, "join_leave_modal.popup.leave_space_request_failed"), PopupKind::Error, None, ); @@ -325,19 +332,19 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { match action.downcast_ref() { Some(JoinRoomResultAction::Joined { room_id }) if room_id == kind.room_id() => { enqueue_popup_notification( - "Successfully joined room.", + tr_key(self.app_language, "join_leave_modal.popup.joined_success"), PopupKind::Success, Some(3.0), ); - self.view.label(cx, ids!(title)).set_text(cx, "Joined room!"); - self.view.label(cx, ids!(description)).set_text(cx, &format!( - "Successfully joined \"{}\".", - kind.room_name(), - )); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "join_leave_modal.title.joined_room")); + let room_name = kind.room_name().to_string(); + self.view.label(cx, ids!(description)).set_text(cx, &tr_fmt(self.app_language, "join_leave_modal.description.joined_room", &[ + ("room_name", room_name.as_str()), + ])); new_final_success = Some(true); } Some(JoinRoomResultAction::Failed { room_id, error }) if room_id == kind.room_id() => { - self.view.label(cx, ids!(title)).set_text(cx, "Error joining room!"); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "join_leave_modal.title.error_joining_room")); let was_invite = matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)); let msg = utils::stringify_join_leave_error(error, kind.room_name(), true, was_invite); self.view.label(cx, ids!(description)).set_text(cx, &msg); @@ -357,19 +364,19 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let description: String; let popup_msg: Cow<'static, str>; if matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)) { - title = "Rejected invite!"; - description = format!( - "Successfully rejected invite to \"{}\".", - kind.room_name(), - ); - popup_msg = "Successfully rejected invite.".into(); + title = tr_key(self.app_language, "join_leave_modal.title.rejected_invite"); + let room_name = kind.room_name().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.rejected_invite", &[ + ("room_name", room_name.as_str()), + ]); + popup_msg = tr_key(self.app_language, "join_leave_modal.popup.rejected_success").into(); } else { - title = "Left room!"; - description = format!( - "Successfully left \"{}\".", - kind.room_name(), - ); - popup_msg = "Successfully left room.".into(); + title = tr_key(self.app_language, "join_leave_modal.title.left_room"); + let room_name = kind.room_name().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.left_room", &[ + ("room_name", room_name.as_str()), + ]); + popup_msg = tr_key(self.app_language, "join_leave_modal.popup.left_room_success").into(); } self.view.label(cx, ids!(title)).set_text(cx, title); self.view.label(cx, ids!(description)).set_text(cx, &description); @@ -381,13 +388,13 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let description: String; let popup_msg: Cow<'static, str>; if matches!(kind, JoinLeaveModalKind::AcceptInvite(_) | JoinLeaveModalKind::RejectInvite(_)) { - title = "Error rejecting invite!"; + title = tr_key(self.app_language, "join_leave_modal.title.error_rejecting_invite"); description = utils::stringify_join_leave_error(error, kind.room_name(), false, true); - popup_msg = "Failed to reject invite.".into(); + popup_msg = tr_key(self.app_language, "join_leave_modal.popup.reject_failed").into(); } else { - title = "Error leaving room!"; + title = tr_key(self.app_language, "join_leave_modal.title.error_leaving_room"); description = utils::stringify_join_leave_error(error, kind.room_name(), false, false); - popup_msg = "Failed to leave room.".into(); + popup_msg = tr_key(self.app_language, "join_leave_modal.popup.leave_failed").into(); } self.view.label(cx, ids!(title)).set_text(cx, title); @@ -404,13 +411,21 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { let description: String; match result { Ok(()) => { - title = "Left space!"; - description = format!("Successfully left \"{space_name_id}\"."); + title = tr_key(self.app_language, "join_leave_modal.title.left_space"); + let space_name = space_name_id.to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.left_space", &[ + ("space_name", space_name.as_str()), + ]); new_final_success = Some(true); } Err(e) => { - title = "Error leaving space!"; - description = format!("Failed to leave space \"{space_name_id}\".\n\nError: {e}"); + title = tr_key(self.app_language, "join_leave_modal.title.error_leaving_space"); + let space_name = space_name_id.to_string(); + let error = e.to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.error_leaving_space", &[ + ("space_name", space_name.as_str()), + ("error", error.as_str()), + ]); new_final_success = Some(false); } } @@ -424,7 +439,7 @@ impl WidgetMatchEvent for JoinLeaveRoomModal { self.final_success = Some(success); needs_redraw = true; accept_button.set_enabled(cx, true); - accept_button.set_text(cx, "Okay"); + accept_button.set_text(cx, tr_key(self.app_language, "join_leave_modal.button.okay")); apply_primary_button_style(cx, &mut accept_button); accept_button.reset_hover(cx); cancel_button.set_visible(cx, false); @@ -448,7 +463,9 @@ impl JoinLeaveRoomModal { cx: &mut Cx, kind: JoinLeaveModalKind, show_tip: bool, + app_language: AppLanguage, ) { + self.app_language = app_language; log!("Showing JoinLeaveRoomModal for {kind:?}"); let title: &str; let description: String; @@ -456,55 +473,48 @@ impl JoinLeaveRoomModal { match &kind { JoinLeaveModalKind::AcceptInvite(invite) => { - title = "Accept this invite?"; - description = format!( - "Are you sure you want to accept this invite to join \"{}\"?", - invite.room_name_id(), - ); - tip_button = "Join"; + title = tr_key(self.app_language, "join_leave_modal.title.confirm_accept_invite"); + let room_name = invite.room_name_id().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.confirm_accept_invite", &[ + ("room_name", room_name.as_str()), + ]); + tip_button = tr_key(self.app_language, "join_leave_modal.button.join"); } JoinLeaveModalKind::RejectInvite(invite) => { - title = "Reject this invite?"; - description = format!( - "Are you sure you want to reject this invite to join \"{}\"?\n\n\ - If this is a private room, you won't be able to join this room \ - without being re-invited to it.", - invite.room_name_id() - ); - tip_button = "Reject"; + title = tr_key(self.app_language, "join_leave_modal.title.confirm_reject_invite"); + let room_name = invite.room_name_id().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.confirm_reject_invite", &[ + ("room_name", room_name.as_str()), + ]); + tip_button = tr_key(self.app_language, "join_leave_modal.button.reject"); } JoinLeaveModalKind::JoinRoom { details, is_space } => { title = if *is_space { - "Join this space?" + tr_key(self.app_language, "join_leave_modal.title.confirm_join_space") } else { - "Join this room?" + tr_key(self.app_language, "join_leave_modal.title.confirm_join_room") }; - description = format!( - "Are you sure you want to join \"{}\"?", - details.room_name_id() - ); - tip_button = "Join"; + let room_name = details.room_name_id().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.confirm_join", &[ + ("room_name", room_name.as_str()), + ]); + tip_button = tr_key(self.app_language, "join_leave_modal.button.join"); } JoinLeaveModalKind::LeaveRoom(room) => { - title = "Leave this room?"; - description = format!( - "Are you sure you want to leave \"{}\"?\n\n\ - If this is a private room, you won't be able to join this room \ - without being re-invited to it.", - room.room_name_id() - ); - tip_button = "Leave"; + title = tr_key(self.app_language, "join_leave_modal.title.confirm_leave_room"); + let room_name = room.room_name_id().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.confirm_leave_room", &[ + ("room_name", room_name.as_str()), + ]); + tip_button = tr_key(self.app_language, "join_leave_modal.button.leave"); } JoinLeaveModalKind::LeaveSpace { details, .. } => { - title = "Leave this space?"; - description = format!( - "Are you sure you want to leave \"{}\"?\n\n\ - If you leave this space, you will also leave any joined rooms within this space.\n\n\ - If this is a private space, you won't be able to join this space \ - without being re-invited to it.", - details.room_name_id() - ); - tip_button = "Leave"; + title = tr_key(self.app_language, "join_leave_modal.title.confirm_leave_space"); + let room_name = details.room_name_id().to_string(); + description = tr_fmt(self.app_language, "join_leave_modal.description.confirm_leave_space", &[ + ("room_name", room_name.as_str()), + ]); + tip_button = tr_key(self.app_language, "join_leave_modal.button.leave"); } } @@ -512,16 +522,16 @@ impl JoinLeaveRoomModal { self.view.label(cx, ids!(description)).set_text(cx, &description); if show_tip { self.view.view(cx, ids!(tip_view)).set_visible(cx, true); - self.view.label(cx, ids!(tip)).set_text(cx, &format!( - "Tip: hold Shift when clicking the \"{tip_button}\" button to bypass this prompt.", - )); + self.view.label(cx, ids!(tip)).set_text(cx, &tr_fmt(self.app_language, "join_leave_modal.tip.with_button", &[ + ("button_text", tip_button), + ])); } else { self.view.view(cx, ids!(tip_view)).set_visible(cx, false); } let mut accept_button = self.button(cx, ids!(accept_button)); let mut cancel_button = self.button(cx, ids!(cancel_button)); - accept_button.set_text(cx, "Yes"); + accept_button.set_text(cx, tr_key(self.app_language, "join_leave_modal.button.yes")); let is_negative = matches!(kind, JoinLeaveModalKind::RejectInvite(_) @@ -542,7 +552,7 @@ impl JoinLeaveRoomModal { accept_button.set_enabled(cx, true); accept_button.set_visible(cx, true); accept_button.reset_hover(cx); - cancel_button.set_text(cx, "Cancel"); + cancel_button.set_text(cx, tr_key(self.app_language, "join_leave_modal.button.cancel")); cancel_button.set_enabled(cx, true); cancel_button.set_visible(cx, true); cancel_button.reset_hover(cx); @@ -559,8 +569,9 @@ impl JoinLeaveRoomModalRef { cx: &mut Cx, kind: JoinLeaveModalKind, show_tip: bool, + app_language: AppLanguage, ) { let Some(mut inner) = self.borrow_mut() else { return }; - inner.set_kind(cx, kind, show_tip); + inner.set_kind(cx, kind, show_tip, app_language); } } diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 3296db520..dc46def41 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -418,7 +418,7 @@ impl WidgetMatchEvent for LoginScreen { if cancel_button.clicked(actions) { self.adding_account = false; // Reset the UI back to normal login mode - self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "login.title.login_to_robrix")); cancel_button.set_visible(cx, false); self.view.view(cx, ids!(sso_view)).set_visible(cx, true); mode_toggle_button.set_visible(cx, true); @@ -541,7 +541,7 @@ impl WidgetMatchEvent for LoginScreen { confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); // Reset title and buttons in case we were in add-account mode - self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "login.title.login_to_robrix")); cancel_button.set_visible(cx, false); mode_toggle_button.set_visible(cx, true); login_status_modal.close(cx); @@ -584,7 +584,7 @@ impl WidgetMatchEvent for LoginScreen { Some(LoginAction::ShowAddAccountScreen) => { self.adding_account = true; // Update UI to "add account" mode - self.view.label(cx, ids!(title)).set_text(cx, "Add Another Account"); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "settings.account.button.add_another_account")); cancel_button.set_visible(cx, true); // Hide signup button in add-account mode (user already has an account) mode_toggle_button.set_visible(cx, false); @@ -597,7 +597,7 @@ impl WidgetMatchEvent for LoginScreen { password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); // Reset title and buttons - self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "login.title.login_to_robrix")); cancel_button.set_visible(cx, false); mode_toggle_button.set_visible(cx, true); login_status_modal.close(cx); @@ -613,10 +613,10 @@ impl WidgetMatchEvent for LoginScreen { self.redraw(cx); } Some(AccountSwitchAction::Failed(error)) => { - login_status_modal_inner.set_title(cx, "Account Switch Failed"); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.account_switch_failed")); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); - login_status_modal_button.set_text(cx, "Okay"); + login_status_modal_button.set_text(cx, tr_key(self.app_language, "login.status.okay")); login_status_modal_button.set_enabled(cx, true); self.redraw(cx); } diff --git a/src/shared/verification_badge.rs b/src/shared/verification_badge.rs index 2a0ef3588..b58f7ac1a 100644 --- a/src/shared/verification_badge.rs +++ b/src/shared/verification_badge.rs @@ -2,6 +2,7 @@ use makepad_widgets::*; use matrix_sdk::encryption::VerificationState; use crate::{ + i18n::{AppLanguage, tr_key}, shared::styles::{COLOR_FG_ACCEPT_GREEN, COLOR_FG_DANGER_RED}, sliding_sync::get_client, verification::VerificationStateAction, @@ -148,19 +149,18 @@ impl VerificationBadge { impl VerificationBadgeRef { /// Returns verification-related string content and background color for a tooltip. - pub fn tooltip_content(&self) -> (&'static str, Option) { + pub fn tooltip_content(&self, app_language: AppLanguage) -> (String, Option) { match self.borrow().map(|v| v.verification_state) { Some(VerificationState::Verified) => ( - "This device is fully verified.", + tr_key(app_language, "verification_badge.tooltip.verified").to_string(), Some(COLOR_FG_ACCEPT_GREEN), ), Some(VerificationState::Unverified) => ( - "This device is unverified. To view your encrypted message history, \ - please verify Robrix from another client.", + tr_key(app_language, "verification_badge.tooltip.unverified").to_string(), Some(COLOR_FG_DANGER_RED), ), _ => ( - "Verification state is unknown.", + tr_key(app_language, "verification_badge.tooltip.unknown").to_string(), None, ), } From 17c4011f4ae76887f04d4b011b97d1d69c3107da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 15:01:05 +0800 Subject: [PATCH 095/283] fix: create unencrypted DM for BotFather targets Add create_encrypted to OpenOrCreateDirectMessage and route DM creation through create_room when target is the configured BotFather account. Wire this flag through app, add-room, and user-profile DM entry points so BotFather DMs avoid E2EE while regular DMs remain encrypted. --- src/app.rs | 23 +++++++++++++++++++++++ src/home/add_room.rs | 11 +++++++++++ src/profile/user_profile.rs | 13 ++++++++++++- src/sliding_sync.rs | 14 ++++++++++++-- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c263d991..ef80083dc 100644 --- a/src/app.rs +++ b/src/app.rs @@ -697,6 +697,10 @@ impl MatchEvent for App { } RoomFilterResultTarget::RemoteUser(user_profile) => { submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + create_encrypted: self.app_state.bot_settings.should_create_encrypted_dm( + user_profile.user_id.as_ref(), + current_user_id().as_deref(), + ), user_profile, allow_create: false, }); @@ -1283,6 +1287,10 @@ impl MatchEvent for App { } Some(DirectMessageRoomAction::DidNotExist { user_profile }) => { let user_profile = user_profile.clone(); + let create_encrypted = self.app_state.bot_settings.should_create_encrypted_dm( + user_profile.user_id.as_ref(), + current_user_id().as_deref(), + ); let body_text = match &user_profile.username { Some(un) if !un.is_empty() => format!( "You don't have an existing direct message room with {} ({}).\n\n\ @@ -1304,6 +1312,7 @@ impl MatchEvent for App { accept_button_text: Some("Create DM".into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + create_encrypted, user_profile, allow_create: true, }); @@ -2033,6 +2042,20 @@ impl BotSettingsState { self.resolved_bot_user_id(current_user_id) } + + /// Returns `true` if new DM rooms for this target user should be encrypted. + /// + /// BotFather DM rooms are created unencrypted so that appservice bots that do + /// not support E2EE can still receive and reply to messages. + pub fn should_create_encrypted_dm( + &self, + target_user_id: &UserId, + current_user_id: Option<&UserId>, + ) -> bool { + self.resolved_bot_user_id(current_user_id) + .map(|bot_user_id| bot_user_id.as_str() != target_user_id.as_str()) + .unwrap_or(true) + } } /// A snapshot of the main dock: all state needed to restore the dock tabs/layout. diff --git a/src/home/add_room.rs b/src/home/add_room.rs index bac89be74..4a83eb14e 100644 --- a/src/home/add_room.rs +++ b/src/home/add_room.rs @@ -1150,9 +1150,20 @@ impl Widget for AddRoomScreen { Some(4.0), ); } else { + let create_encrypted = scope + .data + .get::() + .map(|app_state| { + app_state.bot_settings.should_create_encrypted_dm( + user_id.as_ref(), + current_user_id().as_deref(), + ) + }) + .unwrap_or(true); self.adding_friend = true; add_friend_button.set_enabled(cx, false); submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + create_encrypted, user_profile: UserProfile { user_id, username: None, diff --git a/src/profile/user_profile.rs b/src/profile/user_profile.rs index cedbbeba3..fd3389116 100644 --- a/src/profile/user_profile.rs +++ b/src/profile/user_profile.rs @@ -4,7 +4,7 @@ use std::{borrow::Cow, ops::{Deref, DerefMut}}; use makepad_widgets::*; use matrix_sdk::{room::{RoomMember, RoomMemberRole}, ruma::{events::room::member::MembershipState, OwnedRoomId, OwnedUserId}}; use crate::{ - avatar_cache, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, current_user_id, is_user_ignored, submit_async_request}, utils + app::AppState, avatar_cache, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, current_user_id, is_user_ignored, submit_async_request}, utils }; use super::user_profile_cache; @@ -464,7 +464,18 @@ impl Widget for UserProfileSlidingPane { if let Event::Actions(actions) = event { if self.button(cx, ids!(direct_message_button)).clicked(actions) { + let create_encrypted = scope + .data + .get::() + .map(|app_state| { + app_state.bot_settings.should_create_encrypted_dm( + info.user_profile.user_id.as_ref(), + current_user_id().as_deref(), + ) + }) + .unwrap_or(true); submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + create_encrypted, user_profile: info.user_profile.clone(), // Don't just create a new DM room; we want to first get confirmation from the user. allow_create: false, diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 039fd9587..9b11d5728 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -803,6 +803,7 @@ pub enum MatrixRequest { OpenOrCreateDirectMessage { user_profile: UserProfile, allow_create: bool, + create_encrypted: bool, }, /// Request to create a new room, optionally underneath a selected parent space. CreateRoom { @@ -1905,7 +1906,7 @@ async fn matrix_worker_task( ); } - MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create } => { + MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create, create_encrypted } => { 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) { @@ -1921,7 +1922,16 @@ async fn matrix_worker_task( return; } log!("Creating new DM room with {user_profile:?}..."); - match client.create_dm(&user_profile.user_id).await { + let create_dm_result = if create_encrypted { + client.create_dm(&user_profile.user_id).await + } else { + let mut request = CreateRoomRequest::new(); + request.invite = vec![user_profile.user_id.clone()]; + request.is_direct = true; + request.preset = Some(RoomPreset::TrustedPrivateChat); + client.create_room(request).await + }; + match create_dm_result { Ok(room) => { log!("Successfully created DM room: {}", room.room_id()); Cx::post_action(DirectMessageRoomAction::NewlyCreated { From ae25d461ac26ccfaeb8a8bfc45af3505df335b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 15:50:44 +0800 Subject: [PATCH 096/283] feat(room-info): improve info panel and invite flow UX - rename room info label to info and add mobile navbar info action - add room id copy button and keep wrapping to avoid clipping - add invite entry in info panel and strengthen invite notifications - upgrade invite modal search to local+remote results with avatar/name/user-id layout - prevent room info pane from closing while invite modal is active --- src/app.rs | 39 +++- src/home/home_screen.rs | 47 ++-- src/home/invite_modal.rs | 367 ++++++++++++++++++++++++++++-- src/home/room_screen.rs | 125 ++++++++-- src/profile/user_profile_cache.rs | 38 ++++ src/room/room_input_bar.rs | 7 +- 6 files changed, 570 insertions(+), 53 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c263d991..6a05a9856 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt}, - 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef + event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt, mark_invite_modal_closed}, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ @@ -730,6 +730,14 @@ impl MatchEvent for App { return; } + if let Some(room_screen_id) = self.clicked_mobile_room_info_button(cx, actions) { + let room_screen_widget_uid = self.ui.room_screen(cx, &[room_screen_id]).widget_uid(); + cx.widget_action( + room_screen_widget_uid, + MessageAction::ShowRoomInfoPane, + ); + } + for action in actions { match action.downcast_ref() { Some(LogoutConfirmModalAction::Open) => { @@ -1242,6 +1250,7 @@ impl MatchEvent for App { continue; } Some(InviteModalAction::Close) => { + mark_invite_modal_closed(); self.ui.modal(cx, ids!(invite_modal)).close(cx); continue; } @@ -1519,6 +1528,22 @@ impl App { None } + fn clicked_mobile_room_info_button(&self, cx: &mut Cx, actions: &Actions) -> Option { + for (view_id, room_screen_id) in Self::ROOM_VIEW_IDS.iter().zip(Self::ROOM_SCREEN_IDS.iter()) { + let button_path = &[ + *view_id, + live_id!(header), + live_id!(content), + live_id!(button_container), + live_id!(right_button), + ]; + if self.ui.button(cx, button_path).clicked(actions) { + return Some(*room_screen_id); + } + } + None + } + fn set_room_filter_modal_empty_state( &self, cx: &mut Cx, @@ -1860,6 +1885,18 @@ impl App { // 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()); + let right_button_path = &[view_id, live_id!(header), live_id!(content), live_id!(button_container), live_id!(right_button)]; + let show_info_button = matches!( + selected_room, + SelectedRoom::JoinedRoom { .. } + | SelectedRoom::Thread { .. } + ); + let right_button = self.ui.button(cx, right_button_path); + right_button.set_visible(cx, show_info_button); + if show_info_button { + right_button.set_text(cx, ""); + right_button.reset_hover(cx); + } // Save the current selected_room onto the navigation stack before replacing it. if let Some(prev) = self.app_state.selected_room.take() { diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index b13c2f0c3..3b7fc9d05 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -100,22 +100,39 @@ script_mod! { padding: Inset{top: 30, bottom: 0} height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT), - content +: { - height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) - button_container +: { - padding: 0, - margin: 0 - left_button +: { - width: Fit, height: Fit, - padding: Inset{left: 20, right: 23, top: 10, bottom: 10} - margin: Inset{left: 8, right: 0, top: 0, bottom: 0} - draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } - icon_walk: Walk{width: 13, height: Fit} - spacing: 0 - text: "" + content +: { + height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) + button_container +: { + width: Fill + flow: Overlay + padding: 0, + margin: 0 + left_button +: { + align: Align{x: 0.0, y: 0.5} + width: Fit, height: Fit, + padding: Inset{left: 20, right: 23, top: 10, bottom: 10} + margin: Inset{left: 8, right: 0, top: 0, bottom: 0} + draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } + icon_walk: Walk{width: 13, height: Fit} + spacing: 0 + text: "" + } + right_button := ButtonFlatterIcon { + visible: false + align: Align{x: 1.0, y: 0.5} + width: Fit, height: Fit, + padding: Inset{left: 23, right: 20, top: 10, bottom: 10} + margin: Inset{left: 0, right: 8, top: 0, bottom: 0} + draw_icon +: { + color: (ROOM_NAME_TEXT_COLOR) + svg: (ICON_INFO) + } + icon_walk: Walk{width: 14, height: Fit} + spacing: 0 + text: "" + } } - } - title_container +: { + title_container +: { padding: Inset{top: 8} title +: { draw_text +: { diff --git a/src/home/invite_modal.rs b/src/home/invite_modal.rs index 2bcd32850..a181e8d2b 100644 --- a/src/home/invite_modal.rs +++ b/src/home/invite_modal.rs @@ -1,19 +1,98 @@ //! A modal dialog for inviting a user to a room. +use std::cell::Cell; use makepad_widgets::*; use ruma::OwnedUserId; -use crate::app::AppState; +use crate::app::{AppState, RoomFilterRemoteSearchAction}; +use crate::avatar_cache::{self, AvatarCacheEntry}; use crate::i18n::{AppLanguage, tr_fmt, tr_key}; use crate::home::room_screen::InviteResultAction; -use crate::sliding_sync::{MatrixRequest, submit_async_request}; +use crate::profile::{user_profile::UserProfile, user_profile_cache}; +use crate::shared::avatar::AvatarWidgetRefExt; +use crate::sliding_sync::{MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, submit_async_request}; use crate::utils::RoomNameId; +thread_local! { + static INVITE_MODAL_OPEN: Cell = const { Cell::new(false) }; +} + +fn set_invite_modal_open(open: bool) { + INVITE_MODAL_OPEN.with(|state| state.set(open)); +} + +pub fn is_invite_modal_open() -> bool { + INVITE_MODAL_OPEN.with(|state| state.get()) +} + +pub fn mark_invite_modal_closed() { + set_invite_modal_open(false); +} + script_mod! { use mod.prelude.widgets.* use mod.widgets.* + let InviteSearchResultItem = View { + visible: false + width: Fill + height: 48 + flow: Overlay + + row := View { + width: Fill + height: Fill + flow: Right + align: Align{y: 0.5} + spacing: 8 + padding: Inset{left: 8, right: 8, top: 5, bottom: 5} + + avatar := Avatar { width: 30, height: 30 } + + text_col := View { + width: Fill + height: Fit + flow: Down + spacing: 0 + + name_label := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (COLOR_TEXT) + text_style: REGULAR_TEXT {font_size: 10} + } + text: "" + } + + id_label := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (COLOR_TEXT_INPUT_IDLE) + text_style: REGULAR_TEXT {font_size: 8.5} + } + text: "" + } + } + } + + click_button := RobrixNeutralIconButton { + width: Fill + height: Fill + text: "" + icon_walk: Walk{width: 0, height: 0} + draw_bg +: { + color: #0000 + color_hover: #FFFFFF22 + color_down: #FFFFFF11 + } + } + } + mod.widgets.InviteModal = #(InviteModal::register_widget(vm)) { width: Fit @@ -59,6 +138,41 @@ script_mod! { empty_text: "", } + search_status := Label { + visible: false + width: Fill + height: Fit + margin: Inset{top: 10, left: 1} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 9.5} + color: #6D7682 + } + text: "" + } + + search_results_scroll := ScrollYView { + visible: false + width: Fill + height: 200 + margin: Inset{top: 6} + + search_results := View { + width: Fill + height: Fit + flow: Down + spacing: 3 + + result_item_0 := InviteSearchResultItem {} + result_item_1 := InviteSearchResultItem {} + result_item_2 := InviteSearchResultItem {} + result_item_3 := InviteSearchResultItem {} + result_item_4 := InviteSearchResultItem {} + result_item_5 := InviteSearchResultItem {} + result_item_6 := InviteSearchResultItem {} + result_item_7 := InviteSearchResultItem {} + } + } + View { width: Fill, height: Fit flow: Right, @@ -140,6 +254,11 @@ enum InviteModalState { InviteError, } +#[derive(Clone, Debug)] +struct InviteSearchResult { + user_profile: UserProfile, +} + #[derive(Script, ScriptHook, Widget)] pub struct InviteModal { @@ -147,6 +266,8 @@ pub struct InviteModal { #[rust] state: InviteModalState, #[rust] room_name_id: Option, #[rust] app_language: AppLanguage, + #[rust] current_search_query: String, + #[rust] search_results: Vec, } impl Widget for InviteModal { @@ -178,6 +299,7 @@ impl WidgetMatchEvent for InviteModal { if cancel_clicked || actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) { + set_invite_modal_open(false); // If the modal was dismissed by clicking outside of it, we MUST NOT emit // a `InviteModalAction::Close` action, as that would cause // an infinite action feedback loop. @@ -190,6 +312,7 @@ impl WidgetMatchEvent for InviteModal { // Handle the okay button (shown after invite success). let okay_button = self.view.button(cx, ids!(okay_button)); if okay_button.clicked(actions) { + set_invite_modal_open(false); cx.action(InviteModalAction::Close); return; } @@ -199,6 +322,22 @@ impl WidgetMatchEvent for InviteModal { let status_view = self.view.view(cx, ids!(status_label_view)); let mut status_label = self.view.label(cx, ids!(status_label_view.status_label)); + if let Some(new_query) = user_id_input.changed(actions) + && self.state == InviteModalState::WaitingForUserInput + { + self.update_search_results(cx, &new_query, true); + } + + if self.state == InviteModalState::WaitingForUserInput + && let Some(result_index) = self.clicked_search_result_index(cx, actions) + && let Some(search_result) = self.search_results.get(result_index).cloned() + { + user_id_input.set_text(cx, search_result.user_profile.user_id.as_str()); + self.submit_invite_for_user(cx, search_result.user_profile.user_id, &confirm_button, &user_id_input, &status_view, &mut status_label); + self.view.redraw(cx); + return; + } + // Handle return key or invite button click. if let Some(user_id_str) = confirm_button.clicked(actions) .then(|| user_id_input.text()) @@ -220,22 +359,7 @@ impl WidgetMatchEvent for InviteModal { // Try to parse the user ID match ruma::UserId::parse(&user_id_str) { Ok(user_id) => { - if let Some(room_name_id) = &self.room_name_id { - submit_async_request(MatrixRequest::InviteUser { - room_id: room_name_id.room_id().clone(), - user_id: user_id.to_owned(), - }); - self.state = InviteModalState::WaitingForInvite(user_id.to_owned()); - script_apply_eval!(cx, status_label, { - text: #(tr_key(self.app_language, "invite_modal.status.sending")), - draw_text +: { - color: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER, - }, - }); - status_view.set_visible(cx, true); - confirm_button.set_enabled(cx, false); - user_id_input.set_is_read_only(cx, true); - } + self.submit_invite_for_user(cx, user_id.to_owned(), &confirm_button, &user_id_input, &status_view, &mut status_label); } Err(_) => { script_apply_eval!(cx, status_label, { @@ -251,6 +375,29 @@ impl WidgetMatchEvent for InviteModal { self.view.redraw(cx); } + if self.state == InviteModalState::WaitingForUserInput { + for action in actions { + match action.downcast_ref() { + Some(RoomFilterRemoteSearchAction::Results { query, kind, results }) + if matches!(kind, RemoteDirectorySearchKind::People) + && self.current_search_query == query.trim() + => { + self.merge_remote_search_results(cx, results); + } + Some(RoomFilterRemoteSearchAction::Failed { query, kind, error }) + if matches!(kind, RemoteDirectorySearchKind::People) + && self.current_search_query == query.trim() + => { + let text = format!("Server search failed: {error}"); + self.view.label(cx, ids!(search_status)).set_text(cx, &text); + self.view.label(cx, ids!(search_status)).set_visible(cx, true); + self.refresh_search_result_buttons(cx); + } + _ => {} + } + } + } + // Handle the result of a previously-sent invite. if let InviteModalState::WaitingForInvite(invited_user_id) = &self.state { for action in actions { @@ -311,6 +458,184 @@ impl WidgetMatchEvent for InviteModal { } impl InviteModal { + const SEARCH_RESULT_ITEM_IDS: [LiveId; 8] = [ + live_id!(result_item_0), live_id!(result_item_1), + live_id!(result_item_2), live_id!(result_item_3), + live_id!(result_item_4), live_id!(result_item_5), + live_id!(result_item_6), live_id!(result_item_7), + ]; + + fn clicked_search_result_index(&self, cx: &mut Cx, actions: &Actions) -> Option { + let results_view = self.view.view(cx, ids!(search_results_scroll.search_results)); + for (index, item_id) in Self::SEARCH_RESULT_ITEM_IDS.iter().enumerate() { + if results_view.button(cx, &[*item_id, live_id!(click_button)]).clicked(actions) { + return Some(index); + } + } + None + } + + fn update_search_results( + &mut self, + cx: &mut Cx, + query: &str, + should_search_remote: bool, + ) { + let query = query.trim(); + self.current_search_query = query.to_owned(); + self.search_results.clear(); + + let search_status = self.view.label(cx, ids!(search_status)); + if query.is_empty() { + search_status.set_visible(cx, false); + search_status.set_text(cx, ""); + self.refresh_search_result_buttons(cx); + return; + } + + for user_profile in user_profile_cache::search_user_profiles(cx, query, Self::SEARCH_RESULT_ITEM_IDS.len()) { + self.search_results.push(InviteSearchResult { + user_profile, + }); + } + + if should_search_remote { + submit_async_request(MatrixRequest::SearchDirectory { + query: query.to_owned(), + kind: RemoteDirectorySearchKind::People, + limit: 24, + }); + let local_count = self.search_results.len(); + let status_text = if local_count > 0 { + format!("Found {local_count} local result(s). Searching server...") + } else { + String::from("Searching local cache and server...") + }; + search_status.set_text(cx, &status_text); + search_status.set_visible(cx, true); + } + + self.refresh_search_result_buttons(cx); + } + + fn merge_remote_search_results( + &mut self, + cx: &mut Cx, + results: &[RemoteDirectorySearchResult], + ) { + for result in results { + let RemoteDirectorySearchResult::User(user_profile) = result else { continue }; + let already_exists = self.search_results.iter() + .any(|existing| existing.user_profile.user_id == user_profile.user_id); + if already_exists { + continue; + } + self.search_results.push(InviteSearchResult { + user_profile: user_profile.clone(), + }); + } + + self.search_results.sort_by(|a, b| { + a.user_profile.displayable_name().to_lowercase() + .cmp(&b.user_profile.displayable_name().to_lowercase()) + .then_with(|| a.user_profile.user_id.as_str().cmp(b.user_profile.user_id.as_str())) + }); + + let status = if self.search_results.is_empty() { + String::from("No users found.") + } else { + format!("Found {} user(s).", self.search_results.len()) + }; + self.view.label(cx, ids!(search_status)).set_text(cx, &status); + self.view.label(cx, ids!(search_status)).set_visible(cx, true); + self.refresh_search_result_buttons(cx); + } + + fn refresh_search_result_buttons(&mut self, cx: &mut Cx) { + let results_view = self.view.view(cx, ids!(search_results_scroll.search_results)); + let visible_count = self.search_results.len().min(Self::SEARCH_RESULT_ITEM_IDS.len()); + for (index, item_id) in Self::SEARCH_RESULT_ITEM_IDS.iter().enumerate() { + let item = results_view.view(cx, &[*item_id]); + if let Some(result) = self.search_results.get(index) { + item.label(cx, ids!(row.text_col.name_label)) + .set_text(cx, result.user_profile.displayable_name()); + item.label(cx, ids!(row.text_col.id_label)) + .set_text(cx, result.user_profile.user_id.as_str()); + self.set_search_result_avatar(cx, &item, result); + item.set_visible(cx, true); + item.button(cx, ids!(click_button)).reset_hover(cx); + } else { + item.set_visible(cx, false); + } + } + self.view.view(cx, ids!(search_results_scroll)).set_visible(cx, visible_count > 0); + } + + fn set_search_result_avatar( + &self, + cx: &mut Cx, + item: &WidgetRef, + result: &InviteSearchResult, + ) { + let avatar = item.avatar(cx, ids!(row.avatar)); + let fallback_text = result.user_profile.displayable_name(); + let mut avatar_state = result.user_profile.avatar_state.clone(); + + if let Some(image_data) = avatar_state.update_from_cache(cx) { + let res = avatar.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_ok() { + return; + } + } + + if let Some(uri) = avatar_state.uri() + && let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) + { + let res = avatar.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_ok() { + return; + } + } + + avatar.show_text(cx, None, None, fallback_text); + } + + fn submit_invite_for_user( + &mut self, + cx: &mut Cx, + user_id: OwnedUserId, + confirm_button: &ButtonRef, + user_id_input: &TextInputRef, + status_view: &ViewRef, + status_label: &mut LabelRef, + ) { + if let Some(room_name_id) = &self.room_name_id { + submit_async_request(MatrixRequest::InviteUser { + room_id: room_name_id.room_id().clone(), + user_id: user_id.clone(), + }); + self.state = InviteModalState::WaitingForInvite(user_id); + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "invite_modal.status.sending")), + draw_text +: { + color: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER, + }, + }); + status_view.set_visible(cx, true); + confirm_button.set_enabled(cx, false); + user_id_input.set_is_read_only(cx, true); + self.view.view(cx, ids!(search_results_scroll)).set_visible(cx, false); + } + } + fn set_invite_title(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { let room_name = room_name_id.to_string(); let title = tr_fmt( @@ -333,11 +658,14 @@ impl InviteModal { } pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId, app_language: AppLanguage) { + set_invite_modal_open(true); self.app_language = app_language; self.set_invite_title(cx, &room_name_id); self.update_static_texts(cx); self.state = InviteModalState::WaitingForUserInput; self.room_name_id = Some(room_name_id); + self.current_search_query.clear(); + self.search_results.clear(); // Reset the UI state let confirm_button = self.view.button(cx, ids!(confirm_button)); @@ -356,6 +684,9 @@ impl InviteModal { user_id_input.set_text(cx, ""); self.view.view(cx, ids!(status_label_view)).set_visible(cx, false); self.view.label(cx, ids!(status_label_view.status_label)).set_text(cx, ""); + self.view.label(cx, ids!(search_status)).set_visible(cx, false); + self.view.label(cx, ids!(search_status)).set_text(cx, ""); + self.refresh_search_result_buttons(cx); self.view.redraw(cx); user_id_input.set_key_focus(cx); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index d5f22216b..084ffd2dc 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{ use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, i18n::{AppLanguage, tr_fmt, tr_key}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, invite_modal::InviteModalAction, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, i18n::{AppLanguage, tr_fmt, tr_key}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, @@ -43,7 +43,7 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction; use rangemap::RangeSet; -use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; +use super::{event_reaction_list::ReactionData, invite_modal::is_invite_modal_open, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; /// The maximum number of timeline items to search through /// when looking for a particular event. @@ -1045,8 +1045,9 @@ script_mod! { height: Fit, spacing: 0, padding: 12, - icon_walk: Walk{width: 0, height: 0} - text: "Back" + draw_icon.svg: (ICON_JUMP) + icon_walk: Walk{width: 14, height: 14} + text: "" } title := Label { @@ -1056,7 +1057,7 @@ script_mod! { text_style: USERNAME_TEXT_STYLE { font_size: 12.5 } color: #000 } - text: "Room Info" + text: "Info" } spacer := View { @@ -1125,15 +1126,33 @@ script_mod! { text: "" } - room_id_value := Label { + room_id_row := View { width: Fill height: Fit - flow: Flow.Right{wrap: true} - draw_text +: { - text_style: MESSAGE_TEXT_STYLE { font_size: 9.5 } - color: #6A6A6A + flow: Right + align: Align{y: 0.5} + spacing: 5 + + room_id_value := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: MESSAGE_TEXT_STYLE { font_size: 9.5 } + color: #6A6A6A + } + text: "" + } + + copy_room_id_button := RobrixNeutralIconButton { + width: 24 + height: 22 + padding: 4 + spacing: 0 + draw_icon.svg: (ICON_COPY) + icon_walk: Walk{width: 11, height: 11} + text: "" } - text: "" } } } @@ -1260,6 +1279,15 @@ script_mod! { flow: Down spacing: 8 + invite_button := RobrixNeutralIconButton { + width: Fill + height: 40 + padding: 10 + draw_icon.svg: (ICON_ADD_USER) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Invite" + } + people_button := RobrixNeutralIconButton { width: Fill height: 40 @@ -1798,6 +1826,7 @@ impl ActionDefaultRef for ThreadsPaneAction { #[derive(Clone, Default, Debug)] pub enum RoomInfoPaneAction { + InviteUser, ShowPeoplePage, OpenPeopleProfile(OwnedUserId), ReportRoom, @@ -2175,7 +2204,12 @@ impl Widget for RoomInfoSlidingPane { } let area = self.view.area(); - let close_pane = { + let close_pane = if is_invite_modal_open() { + matches!( + event, + Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) + ) + } else { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) @@ -2207,6 +2241,22 @@ impl Widget for RoomInfoSlidingPane { self.topic_expanded = !self.topic_expanded; self.redraw(cx); } + if self.button(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_id_row.copy_room_id_button)).clicked(actions) + && let Some(info) = self.info.as_ref() + { + cx.copy_to_clipboard(&info.room_id); + enqueue_popup_notification( + "Room ID copied.", + PopupKind::Success, + Some(2.0), + ); + } + if self.button(cx, ids!(content_scroll.info_view.actions_row.invite_button)).clicked(actions) { + cx.widget_action( + self.widget_uid(), + RoomInfoPaneAction::InviteUser, + ); + } if self.button(cx, ids!(content_scroll.info_view.actions_row.people_button)).clicked(actions) { self.show_people_page = true; self.people_display_count = self.info.as_ref() @@ -2267,13 +2317,13 @@ impl Widget for RoomInfoSlidingPane { }); self.button(cx, ids!(header.back_button)).set_visible(cx, self.show_people_page); - self.label(cx, ids!(header.title)).set_text(cx, if self.show_people_page { "People" } else { "Room Info" }); + self.label(cx, ids!(header.title)).set_text(cx, if self.show_people_page { "People" } else { "Info" }); self.view(cx, ids!(content_scroll)).set_visible(cx, !self.show_people_page); self.view(cx, ids!(content_scroll.info_view)).set_visible(cx, !self.show_people_page); self.view(cx, ids!(people_view)).set_visible(cx, self.show_people_page); self.label(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_name_value)).set_text(cx, &info.room_name); - self.label(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_id_value)).set_text(cx, &info.room_id); + self.label(cx, ids!(content_scroll.info_view.summary_card.room_meta.room_id_row.room_id_value)).set_text(cx, &info.room_id); self.label(cx, ids!(content_scroll.info_view.facts_card.visibility_row.visibility_value)).set_text(cx, &info.visibility); self.label(cx, ids!(content_scroll.info_view.facts_card.encryption_row.encryption_value)).set_text(cx, &info.encryption); @@ -2533,6 +2583,7 @@ pub struct RoomScreen { #[rust] threads_pane_state: ThreadsPaneState, #[rust] app_language: AppLanguage, #[rust] app_language_initialized: bool, + #[rust] pending_invited_users: HashSet, } impl Drop for RoomScreen { @@ -2798,19 +2849,21 @@ impl Widget for RoomScreen { } // Handle InviteResultAction to show popup notifications. - if let Some(InviteResultAction::Sent { room_id, .. }) = action.downcast_ref() { + if let Some(InviteResultAction::Sent { room_id, user_id }) = action.downcast_ref() { // Only handle if this is for the current room. if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + self.pending_invited_users.insert(user_id.clone()); enqueue_popup_notification( - tr_key(self.app_language, "room_screen.popup.invite.sent_success"), - PopupKind::Success, + "Invite sent. Waiting for acceptance.", + PopupKind::Info, Some(4.0), ); } } - if let Some(InviteResultAction::Failed { room_id, error, .. }) = action.downcast_ref() { + if let Some(InviteResultAction::Failed { room_id, user_id, error }) = action.downcast_ref() { // Only handle if this is for the current room. if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + self.pending_invited_users.remove(user_id); let error_text = error.to_string(); enqueue_popup_notification( tr_fmt(self.app_language, "room_screen.popup.invite.failed", &[ @@ -2859,6 +2912,11 @@ impl Widget for RoomScreen { } match action.as_widget_action().cast_ref() { + RoomInfoPaneAction::InviteUser => { + if let Some(room_name_id) = self.room_name_id.as_ref().cloned() { + cx.action(InviteModalAction::Open(room_name_id)); + } + } RoomInfoPaneAction::ShowPeoplePage => { if let Some(tl) = self.tl_state.as_ref() && tl.room_members.is_none() @@ -4091,6 +4149,36 @@ impl RoomScreen { } } + if !self.pending_invited_users.is_empty() { + let start = changed_indices.start.min(new_items.len()); + let end = changed_indices.end.min(new_items.len()); + let mut accepted_users: Vec = Vec::new(); + for idx in start..end { + let Some(new_item) = new_items.get(idx) else { continue }; + let TimelineItemKind::Event(event_tl_item) = new_item.kind() else { continue }; + let TimelineItemContent::MembershipChange(membership_change) = event_tl_item.content() else { continue }; + let accepted = matches!( + membership_change.change(), + Some(MembershipChange::InvitationAccepted) + | Some(MembershipChange::Joined) + ); + if accepted { + let invited_user_id = event_tl_item.sender().to_owned(); + if self.pending_invited_users.contains(&invited_user_id) { + accepted_users.push(invited_user_id); + } + } + } + for accepted_user in accepted_users { + self.pending_invited_users.remove(&accepted_user); + enqueue_popup_notification( + format!("{accepted_user} accepted the invite and joined."), + PopupKind::Success, + Some(4.0), + ); + } + } + if prior_items_changed { // If this RoomScreen is showing the loading pane and has an ongoing backwards pagination request, // then we should update the status message in that loading pane @@ -5474,6 +5562,7 @@ impl RoomScreen { subscribe: false, }); self.room_avatar_url = None; + self.pending_invited_users.clear(); } /// Removes the current room's visual UI state from this widget diff --git a/src/profile/user_profile_cache.rs b/src/profile/user_profile_cache.rs index a669929e8..0864c6041 100644 --- a/src/profile/user_profile_cache.rs +++ b/src/profile/user_profile_cache.rs @@ -254,6 +254,44 @@ pub fn get_user_display_name_for_room( opt.unwrap_or(CachedName::NotFound) } +/// Returns user profiles from the local cache that match the given query. +/// +/// Matching is case-insensitive against both user ID and display name. +pub fn search_user_profiles( + _cx: &mut Cx, + query: &str, + limit: usize, +) -> Vec { + let query = query.trim().to_lowercase(); + if query.is_empty() || limit == 0 { + return Vec::new(); + } + + let mut results: Vec = USER_PROFILE_CACHE.with_borrow(|cache| { + cache.values() + .filter_map(|entry| match entry { + UserProfileCacheEntry::Loaded { user_profile, .. } => Some(user_profile), + UserProfileCacheEntry::Requested => None, + }) + .filter(|profile| { + profile.user_id.as_str().to_lowercase().contains(&query) + || profile.username.as_deref() + .is_some_and(|name| name.to_lowercase().contains(&query)) + }) + .take(limit) + .cloned() + .collect() + }); + + results.sort_by(|a, b| { + let a_name = a.displayable_name().to_lowercase(); + let b_name = b.displayable_name().to_lowercase(); + a_name.cmp(&b_name) + .then_with(|| a.user_id.as_str().cmp(b.user_id.as_str())) + }); + results +} + /// A user's display name in our cache. pub enum CachedName { /// The user's display name was found for the specified room (most accurate). diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 160ebdf50..9fdd3ad9d 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -22,6 +22,8 @@ use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventIte use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId}; 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}}, i18n::AppLanguage, 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}; +const ROOM_INFO_CARD_MOBILE_BREAKPOINT: f32 = 700.0; + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -133,7 +135,7 @@ script_mod! { text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } } icon_walk: Walk{width: 20, height: 20} - text: "room info", + text: "info", } location_card_button := RobrixIconButton { @@ -367,6 +369,9 @@ impl Widget for RoomInputBar { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let width = self.view.area().rect(cx).size.x as f32; + let show_room_info_card = !(width > 1.0 && width < ROOM_INFO_CARD_MOBILE_BREAKPOINT); + self.button(cx, ids!(room_info_card_button)).set_visible(cx, show_room_info_card); self.view.draw_walk(cx, scope, walk) } } From 1156784d7b1a60cecd8231bc56ea2bbb0cb8da4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 16:24:00 +0800 Subject: [PATCH 097/283] fix(app-service): auto-bind invited botfather and tag bots in room info --- src/home/room_screen.rs | 32 +++++++++++++++++++++++++++++++- src/sliding_sync.rs | 17 +++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 084ffd2dc..a9d448861 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1880,6 +1880,7 @@ struct RoomInfoPeopleEntryInfo { user_id: OwnedUserId, display_name: String, level: String, + is_bot: bool, avatar_uri: Option, avatar_fallback_text: String, } @@ -1972,7 +1973,12 @@ impl Widget for RoomInfoPeopleEntry { impl RoomInfoPeopleEntry { fn set_entry(&mut self, cx: &mut Cx, entry: &RoomInfoPeopleEntryInfo) { self.user_id = Some(entry.user_id.clone()); - self.label(cx, ids!(display_name)).set_text(cx, &entry.display_name); + let display_name = if entry.is_bot { + format!("{} [bot]", entry.display_name) + } else { + entry.display_name.clone() + }; + self.label(cx, ids!(display_name)).set_text(cx, &display_name); self.label(cx, ids!(level)).set_text(cx, &entry.level); self.label(cx, ids!(level)).set_visible(cx, !entry.level.is_empty()); @@ -2858,6 +2864,28 @@ impl Widget for RoomScreen { PopupKind::Info, Some(4.0), ); + if let Some(app_state) = scope.data.get::() + && app_state.bot_settings.enabled + { + if let Ok(bot_user_id) = app_state + .bot_settings + .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) + { + if &bot_user_id == user_id + && !app_state + .bot_settings + .bound_bot_user_id(room_id.as_ref()) + .is_some_and(|existing_bot_user_id| existing_bot_user_id.as_str() == user_id.as_str()) + { + cx.action(AppStateAction::BotRoomBindingUpdated { + room_id: room_id.clone(), + bound: true, + bot_user_id: Some(user_id.clone()), + warning: None, + }); + } + } + } } } if let Some(InviteResultAction::Failed { room_id, user_id, error }) = action.downcast_ref() { @@ -5217,6 +5245,7 @@ impl RoomScreen { let display_name = member.display_name() .map(ToOwned::to_owned) .unwrap_or_else(|| member.user_id().to_string()); + let is_bot = is_likely_bot_member(member, None); let level = match member.suggested_role_for_power_level() { RoomMemberRole::Creator => String::from("Creator"), RoomMemberRole::Administrator => String::from("Admin"), @@ -5230,6 +5259,7 @@ impl RoomScreen { user_id: member.user_id().to_owned(), display_name, level, + is_bot, avatar_uri: member.avatar_url().map(ToOwned::to_owned), avatar_fallback_text, } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 0ee60d57b..65af6a991 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1645,8 +1645,21 @@ async fn matrix_worker_task( }); } Err(error) => { - let membership_exists = - room.get_member_no_sync(&bot_user_id).await.ok().flatten().is_some(); + let membership_exists = if bound { + room.get_member_no_sync(&bot_user_id).await.ok().flatten().is_some() + || room + .members_no_sync(RoomMemberships::ACTIVE) + .await + .ok() + .is_some_and(|members| members.iter().any(|member| member.user_id().as_str() == bot_user_id.as_str())) + || room + .members(RoomMemberships::ACTIVE) + .await + .ok() + .is_some_and(|members| members.iter().any(|member| member.user_id().as_str() == bot_user_id.as_str())) + } else { + false + }; let should_mark_bound = if bound { membership_exists } else { false }; if should_mark_bound != bound { From e6e3527dc5fcac8d75d3c7a22058864ef3d86b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 18:15:21 +0800 Subject: [PATCH 098/283] feat(bot): add multi-bot room management modal and app service switch UX --- resources/i18n/en.json | 29 ++ resources/i18n/zh-CN.json | 29 ++ src/app.rs | 197 ++++++++-- src/home/bot_binding_modal.rs | 664 ++++++++++++++++++++++++++++++++++ src/home/mod.rs | 2 + src/home/room_context_menu.rs | 59 +-- src/home/room_screen.rs | 319 +++++++++++----- src/room/room_input_bar.rs | 5 - src/settings/bot_settings.rs | 108 +----- 9 files changed, 1133 insertions(+), 279 deletions(-) create mode 100644 src/home/bot_binding_modal.rs diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ef2daeda5..c4dc5d713 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -52,6 +52,7 @@ "room_context_menu.button.settings": "Settings", "room_context_menu.button.notifications": "Notifications", "room_context_menu.button.invite": "Invite", + "room_context_menu.button.manage_bots": "Manage Bots", "room_context_menu.button.bind_botfather": "Bind BotFather", "room_context_menu.button.unbind_botfather": "Unbind BotFather", "room_context_menu.button.leave_room": "Leave Room", @@ -61,6 +62,30 @@ "room_context_menu.popup.inviting_botfather": "Inviting BotFather {bot_user_id} into this room...", "room_context_menu.popup.bot_settings_unavailable": "Bot settings are unavailable right now.", + "bot_binding_modal.title": "Manage Room Bots", + "bot_binding_modal.body": "Add or remove bots for {room_name}.", + "bot_binding_modal.label.current_room_bots": "Current Room Bots", + "bot_binding_modal.label.known_bots": "Known Bots", + "bot_binding_modal.label.user_id": "Bot Matrix User ID", + "bot_binding_modal.label.remark": "Bot Remark", + "bot_binding_modal.dropdown.custom": "Custom bot user ID", + "bot_binding_modal.input.placeholder": "@bot_weather:server or bot_weather", + "bot_binding_modal.input.remark_placeholder": "What is this bot used for?", + "bot_binding_modal.hint.current_bound_none": "No bots are currently bound to this room.", + "bot_binding_modal.hint.current_bound": "Currently bound bots: {bot_user_ids}", + "bot_binding_modal.button.cancel": "Cancel", + "bot_binding_modal.button.bind": "Add", + "bot_binding_modal.button.unbind": "Remove", + "bot_binding_modal.button.save_remark": "Save Remark", + "bot_binding_modal.status.enter_user_id": "Enter a bot Matrix user ID or localpart.", + "bot_binding_modal.status.current_user_unavailable": "Current user ID is unavailable, cannot resolve bot localpart.", + "bot_binding_modal.status.invalid_user_id": "Invalid bot user ID: {full_user_id}", + "bot_binding_modal.status.state_unavailable": "App state is unavailable right now.", + "bot_binding_modal.status.remark_saved": "Bot remark saved.", + "bot_binding_modal.status.remark_requires_added_bot": "Add this bot to the room before saving a remark.", + "bot_binding_modal.popup.inviting": "Adding bot {bot_user_id} to this room...", + "bot_binding_modal.popup.removing": "Removing bot {bot_user_id} from this room...", + "add_room.title": "Add/Explore Rooms and Spaces", "add_room.section.create_new_room": "Create a new room:", "add_room.section.add_friend": "Add a friend:", @@ -187,10 +212,13 @@ "settings.labs.app_service.title": "App Service", "settings.labs.app_service.description": "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands.", "settings.labs.app_service.enable_label": "Enable App Service", + "settings.labs.app_service.manage_hint": "Manage BotFather and child bots in DM and room bind dialogs. Settings here only control whether App Service features are enabled.", "settings.labs.app_service.botfather_user_id": "BotFather User ID:", "settings.labs.app_service.botfather_placeholder": "bot or @bot:server", "settings.labs.app_service.button.enable": "Enable App Service", "settings.labs.app_service.button.disable": "Disable App Service", + "settings.labs.app_service.status.enabled": "Enabled", + "settings.labs.app_service.status.disabled": "Disabled", "settings.labs.app_service.button.save": "Save", "settings.labs.app_service.popup.saved": "Saved Matrix app service settings.", @@ -340,6 +368,7 @@ "room_screen.app_service.button.list_bots": "List Bots", "room_screen.app_service.button.delete_bot": "Delete Bot", "room_screen.app_service.button.bot_help": "Bot Help", + "room_screen.app_service.button.bots": "Bots", "room_screen.app_service.button.unbind": "Unbind", "spaces_bar.tooltip.unknown_space_name": "Unknown Space Name", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index f7f4ee455..49a385f19 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -52,6 +52,7 @@ "room_context_menu.button.settings": "设置", "room_context_menu.button.notifications": "通知", "room_context_menu.button.invite": "邀请", + "room_context_menu.button.manage_bots": "管理机器人", "room_context_menu.button.bind_botfather": "绑定 BotFather", "room_context_menu.button.unbind_botfather": "解绑 BotFather", "room_context_menu.button.leave_room": "离开房间", @@ -61,6 +62,30 @@ "room_context_menu.popup.inviting_botfather": "正在邀请 BotFather {bot_user_id} 加入该房间...", "room_context_menu.popup.bot_settings_unavailable": "当前无法获取机器人设置。", + "bot_binding_modal.title": "管理房间机器人", + "bot_binding_modal.body": "为 {room_name} 添加或移除机器人。", + "bot_binding_modal.label.current_room_bots": "当前房间机器人", + "bot_binding_modal.label.known_bots": "已知机器人", + "bot_binding_modal.label.user_id": "机器人 Matrix 用户 ID", + "bot_binding_modal.label.remark": "机器人备注", + "bot_binding_modal.dropdown.custom": "自定义机器人用户 ID", + "bot_binding_modal.input.placeholder": "@bot_weather:server 或 bot_weather", + "bot_binding_modal.input.remark_placeholder": "这个机器人是用来做什么的?", + "bot_binding_modal.hint.current_bound_none": "该房间当前没有已绑定机器人。", + "bot_binding_modal.hint.current_bound": "当前已绑定机器人:{bot_user_ids}", + "bot_binding_modal.button.cancel": "取消", + "bot_binding_modal.button.bind": "添加", + "bot_binding_modal.button.unbind": "移除", + "bot_binding_modal.button.save_remark": "保存备注", + "bot_binding_modal.status.enter_user_id": "请输入机器人 Matrix 用户 ID 或 localpart。", + "bot_binding_modal.status.current_user_unavailable": "当前用户 ID 不可用,无法解析机器人 localpart。", + "bot_binding_modal.status.invalid_user_id": "机器人用户 ID 无效:{full_user_id}", + "bot_binding_modal.status.state_unavailable": "当前无法读取应用状态。", + "bot_binding_modal.status.remark_saved": "机器人备注已保存。", + "bot_binding_modal.status.remark_requires_added_bot": "请先将该机器人添加到房间,再保存备注。", + "bot_binding_modal.popup.inviting": "正在将机器人 {bot_user_id} 添加到该房间...", + "bot_binding_modal.popup.removing": "正在将机器人 {bot_user_id} 从该房间移除...", + "add_room.title": "添加/探索房间与空间", "add_room.section.create_new_room": "创建新房间:", "add_room.section.add_friend": "添加好友:", @@ -187,10 +212,13 @@ "settings.labs.app_service.title": "应用服务", "settings.labs.app_service.description": "在这里启用 Matrix 应用服务支持。Robrix 仍然是普通 Matrix 客户端:它会把 BotFather 绑定到房间,并发送对应的斜杠命令。", "settings.labs.app_service.enable_label": "启用应用服务", + "settings.labs.app_service.manage_hint": "BotFather 与子机器人请在 DM 和房间绑定弹窗中管理;这里仅控制是否启用 App Service 功能。", "settings.labs.app_service.botfather_user_id": "BotFather 用户 ID:", "settings.labs.app_service.botfather_placeholder": "bot 或 @bot:server", "settings.labs.app_service.button.enable": "启用应用服务", "settings.labs.app_service.button.disable": "禁用应用服务", + "settings.labs.app_service.status.enabled": "已启用", + "settings.labs.app_service.status.disabled": "未启用", "settings.labs.app_service.button.save": "保存", "settings.labs.app_service.popup.saved": "已保存 Matrix 应用服务设置。", @@ -340,6 +368,7 @@ "room_screen.app_service.button.list_bots": "列出机器人", "room_screen.app_service.button.delete_bot": "删除机器人", "room_screen.app_service.button.bot_help": "Bot 帮助", + "room_screen.app_service.button.bots": "机器人", "room_screen.app_service.button.unbind": "解绑", "spaces_bar.tooltip.unknown_space_name": "未知空间名称", diff --git a/src/app.rs b/src/app.rs index ef80083dc..b4860caf0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt}, + bot_binding_modal::{BotBindingModalAction, BotBindingModalWidgetRefExt}, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt @@ -162,6 +163,11 @@ script_mod! { invite_modal_inner := InviteModal {} } } + bot_binding_modal := Modal { + content +: { + bot_binding_modal_inner := BotBindingModal {} + } + } room_filter_modal := Modal { content +: { room_filter_modal_inner := RoundedShadowView { @@ -1048,28 +1054,28 @@ impl MatchEvent for App { } let message = match (*bound, bot_user_id.as_ref(), warning.as_deref()) { (true, Some(bot_user_id), Some(warning)) => { - format!("BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}") + format!("Bot {bot_user_id} is available for room {room_id}, but adding it reported a warning: {warning}") } (true, Some(bot_user_id), None) => { - format!("Bound room {room_id} to BotFather {bot_user_id}.") + format!("Added bot {bot_user_id} to room {room_id}.") } (false, Some(bot_user_id), Some(warning)) => { - format!("Unbound BotFather {bot_user_id} from room {room_id}, with warning: {warning}") + format!("Removed bot {bot_user_id} from room {room_id}, with warning: {warning}") } (false, Some(bot_user_id), None) => { - format!("Unbound BotFather {bot_user_id} from room {room_id}.") + format!("Removed bot {bot_user_id} from room {room_id}.") } (false, None, Some(warning)) => { - format!("Unbound room {room_id} from BotFather, with warning: {warning}") + format!("Removed bot from room {room_id}, with warning: {warning}") } (false, None, None) => { - format!("Unbound room {room_id} from BotFather.") + format!("Removed bot from room {room_id}.") } (true, None, Some(warning)) => { - format!("BotFather is available for room {room_id}, with warning: {warning}") + format!("Bot is available for room {room_id}, with warning: {warning}") } (true, None, None) => { - format!("Bound room {room_id} to BotFather.") + format!("Added bot to room {room_id}.") } }; submit_async_request(MatrixRequest::SendMessage { @@ -1108,6 +1114,20 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } + Some(AppStateAction::KnownBotUserIdsDiscovered { bot_user_ids }) => { + if self + .app_state + .bot_settings + .record_known_bot_user_ids(bot_user_ids.iter().cloned()) + { + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(self.app_state.clone(), user_id) { + error!("Failed to persist discovered bot user IDs. Error: {e}"); + } + } + } + continue; + } Some(AppStateAction::NavigateToRoom { room_to_close, destination_room }) => { self.navigate_to_room(cx, room_to_close.as_ref(), destination_room); continue; @@ -1252,6 +1272,27 @@ impl MatchEvent for App { _ => {} } + // Handle BotBindingModalAction to open/close the bot binding modal. + match action.downcast_ref() { + Some(BotBindingModalAction::Open(room_name_id)) => { + self.ui + .bot_binding_modal(cx, ids!(bot_binding_modal_inner)) + .show( + cx, + room_name_id.clone(), + &self.app_state.bot_settings, + self.app_state.app_language, + ); + self.ui.modal(cx, ids!(bot_binding_modal)).open(cx); + continue; + } + Some(BotBindingModalAction::Close) => { + self.ui.modal(cx, ids!(bot_binding_modal)).close(cx); + continue; + } + _ => {} + } + match action.downcast_ref() { Some(CreateRoomModalAction::Open { parent_space_id }) => { self.ui.create_room_modal(cx, ids!(create_room_modal_inner)).show(cx, parent_space_id.clone()); @@ -1926,16 +1967,20 @@ pub struct BotSettingsState { pub enabled: bool, /// The configured botfather user, either as a full MXID or localpart. pub botfather_user_id: String, - /// Rooms that Robrix currently considers bound to BotFather, - /// paired with the exact BotFather MXID used for that room. + /// Bots discovered from BotFather `/listbots` replies. + pub known_bot_user_ids: Vec, + /// Rooms that Robrix currently considers bot-bound, + /// paired with the exact bot MXID used for that room. pub room_bindings: Vec, } -/// A persisted room-level BotFather binding. +/// A persisted room-level bot binding. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct RoomBotBindingState { pub room_id: OwnedRoomId, pub bot_user_id: OwnedUserId, + #[serde(default)] + pub remark: String, } impl Default for BotSettingsState { @@ -1943,6 +1988,7 @@ impl Default for BotSettingsState { Self { enabled: false, botfather_user_id: Self::DEFAULT_BOTFATHER_LOCALPART.to_string(), + known_bot_user_ids: Vec::new(), room_bindings: Vec::new(), } } @@ -1951,21 +1997,102 @@ impl Default for BotSettingsState { impl BotSettingsState { pub const DEFAULT_BOTFATHER_LOCALPART: &'static str = "bot"; - fn room_binding_index(&self, room_id: &RoomId) -> Result { + fn room_binding_index( + &self, + room_id: &RoomId, + bot_user_id: &UserId, + ) -> Result { self.room_bindings - .binary_search_by(|binding| binding.room_id.as_str().cmp(room_id.as_str())) + .binary_search_by(|binding| + ( + binding.room_id.as_str(), + binding.bot_user_id.as_str(), + ).cmp(&(room_id.as_str(), bot_user_id.as_str())) + ) + } + + fn room_binding_range(&self, room_id: &RoomId) -> std::ops::Range { + let start = self + .room_bindings + .partition_point(|binding| binding.room_id.as_str() < room_id.as_str()); + let end = self + .room_bindings + .iter() + .skip(start) + .position(|binding| binding.room_id.as_str() != room_id.as_str()) + .map_or(self.room_bindings.len(), |offset| start + offset); + start..end } /// Returns `true` if the given room is currently marked as bound locally. pub fn is_room_bound(&self, room_id: &RoomId) -> bool { - self.room_binding_index(room_id).is_ok() + !self.bound_bot_user_ids(room_id).is_empty() } /// Returns the persisted BotFather MXID for the given room, if any. pub fn bound_bot_user_id(&self, room_id: &RoomId) -> Option<&UserId> { - self.room_binding_index(room_id) - .ok() - .map(|index| self.room_bindings[index].bot_user_id.as_ref()) + let room_binding_range = self.room_binding_range(room_id); + self.room_bindings + .get(room_binding_range.start) + .map(|binding| binding.bot_user_id.as_ref()) + } + + /// Returns all persisted bot MXIDs for the given room. + pub fn bound_bot_user_ids(&self, room_id: &RoomId) -> Vec { + self.room_bindings[self.room_binding_range(room_id)] + .iter() + .map(|binding| binding.bot_user_id.clone()) + .collect() + } + + /// Returns all bot bindings for the given room. + pub fn room_bindings_for(&self, room_id: &RoomId) -> Vec { + self.room_bindings[self.room_binding_range(room_id)] + .to_vec() + } + + /// Returns all known bound bot MXIDs across every room, deduplicated. + pub fn all_bound_bot_user_ids(&self) -> Vec { + let mut all_bots = self + .room_bindings + .iter() + .map(|binding| binding.bot_user_id.clone()) + .collect::>(); + all_bots.sort_by(|a, b| a.as_str().cmp(b.as_str())); + all_bots.dedup_by(|a, b| a.as_str() == b.as_str()); + all_bots + } + + /// Returns bot MXIDs discovered from BotFather `/listbots` replies. + pub fn known_bot_user_ids(&self) -> Vec { + self.known_bot_user_ids.clone() + } + + /// Merges the given discovered bot IDs into the known bot list. + /// + /// Returns `true` if the list changed. + pub fn record_known_bot_user_ids( + &mut self, + discovered_bot_user_ids: impl IntoIterator, + ) -> bool { + let mut changed = false; + for bot_user_id in discovered_bot_user_ids { + if !self + .known_bot_user_ids + .iter() + .any(|existing| existing.as_str() == bot_user_id.as_str()) + { + self.known_bot_user_ids.push(bot_user_id); + changed = true; + } + } + if changed { + self.known_bot_user_ids + .sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + self.known_bot_user_ids + .dedup_by(|lhs, rhs| lhs.as_str() == rhs.as_str()); + } + changed } /// Updates the local bound/unbound state for the given room. @@ -1977,24 +2104,44 @@ impl BotSettingsState { ) { if bound { let Some(bot_user_id) = bot_user_id else { return }; - match self.room_binding_index(room_id.as_ref()) { - Ok(existing_index) => { - self.room_bindings[existing_index].bot_user_id = bot_user_id; - } + match self.room_binding_index(room_id.as_ref(), bot_user_id.as_ref()) { + Ok(_) => {} Err(insert_index) => { self.room_bindings.insert(insert_index, RoomBotBindingState { room_id, bot_user_id, + remark: String::new(), }); } } } else { - if let Ok(existing_index) = self.room_binding_index(room_id.as_ref()) { - self.room_bindings.remove(existing_index); + if let Some(bot_user_id) = bot_user_id { + if let Ok(existing_index) = self.room_binding_index(room_id.as_ref(), bot_user_id.as_ref()) { + self.room_bindings.remove(existing_index); + } + } else { + self.room_bindings.retain(|binding| binding.room_id != room_id); } } } + /// Updates the remark for a specific room bot binding. + /// + /// Returns `true` if a binding existed and was updated. + pub fn set_room_bot_remark( + &mut self, + room_id: &RoomId, + bot_user_id: &UserId, + remark: String, + ) -> bool { + if let Ok(index) = self.room_binding_index(room_id, bot_user_id) { + self.room_bindings[index].remark = remark; + true + } else { + false + } + } + /// Returns the configured botfather user ID, resolving a localpart against /// the current user's homeserver when needed. pub fn resolved_bot_user_id(&self, current_user_id: Option<&UserId>) -> Result { @@ -2211,6 +2358,10 @@ pub enum AppStateAction { room_id: OwnedRoomId, bot_user_id: OwnedUserId, }, + /// Bot IDs discovered from BotFather replies (for example, `/listbots`). + KnownBotUserIdsDiscovered { + bot_user_ids: Vec, + }, /// The given room was successfully loaded from the homeserver /// and is now known to our client. /// diff --git a/src/home/bot_binding_modal.rs b/src/home/bot_binding_modal.rs new file mode 100644 index 000000000..1316b0465 --- /dev/null +++ b/src/home/bot_binding_modal.rs @@ -0,0 +1,664 @@ +//! A modal dialog for binding or unbinding bots to a room. + +use makepad_widgets::*; +use ruma::{OwnedUserId, UserId}; + +use crate::{ + app::{AppState, BotSettingsState, RoomBotBindingState}, + i18n::{AppLanguage, tr_fmt, tr_key}, + persistence, + shared::popup_list::{PopupKind, enqueue_popup_notification}, + sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, + utils::RoomNameId, +}; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.BotBindingModalLabel = Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #333 + } + text: "" + } + + mod.widgets.BotBindingModal = #(BotBindingModal::register_widget(vm)) { + width: Fit + height: Fit + + RoundedView { + width: 448 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 28, right: 24, bottom: 20, left: 24} + spacing: 16 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT { font_size: 13 } + color: #000 + } + text: "Manage Room Bots" + } + + body := mod.widgets.BotBindingModalLabel { + text: "" + } + + form := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 12 + padding: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + } + + current_room_bots_label := mod.widgets.BotBindingModalLabel { + text: "Current Room Bots" + } + + current_room_bots_dropdown := DropDownFlat { + width: Fill + height: 40 + labels: ["No bots currently added"] + } + + known_bots_label := mod.widgets.BotBindingModalLabel { + text: "Known Bots" + } + + known_bots_dropdown := DropDownFlat { + width: Fill + height: 40 + labels: ["Custom bot user ID"] + } + + user_id_label := mod.widgets.BotBindingModalLabel { + text: "Bot Matrix User ID" + } + + user_id_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "@bot_weather:server or bot_weather" + } + + remark_label := mod.widgets.BotBindingModalLabel { + text: "Bot Remark" + } + + remark_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "What is this bot used for?" + } + + remark_controls := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + + save_remark_button := RobrixNeutralIconButton { + width: 150 + align: Align{x: 0.5, y: 0.5} + padding: 10 + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Save Remark" + } + } + } + + status_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #000 + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 12 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + unbind_button := RobrixNegativeIconButton { + width: 128 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Unbind" + } + + bind_button := RobrixPositiveIconButton { + width: 128 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_ADD_USER) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Bind" + } + } + } + } +} + +/// Actions emitted by other widgets to show or hide the `BotBindingModal`. +#[derive(Clone, Debug)] +pub enum BotBindingModalAction { + /// Open the modal to bind or unbind bots in the given room. + Open(RoomNameId), + /// Close the modal. + Close, +} + +#[derive(Script, ScriptHook, Widget)] +pub struct BotBindingModal { + #[deref] + view: View, + #[rust] + room_name_id: Option, + #[rust] + known_bot_user_ids: Vec, + #[rust] + room_bound_bots: Vec, + #[rust] + app_language: AppLanguage, +} + +impl Widget for BotBindingModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + if let Some(app_state) = scope.data.get::() + && self.app_language != app_state.app_language + { + self.app_language = app_state.app_language; + self.update_static_texts(cx); + if let Some(room_name_id) = self.room_name_id.clone() { + self.set_title_and_body(cx, &room_name_id); + self.update_room_bound_bots_value(cx); + } + } + self.view.handle_event(cx, event, scope); + self.widget_match_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 WidgetMatchEvent for BotBindingModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + let cancel_button = self.view.button(cx, ids!(buttons.cancel_button)); + let bind_button = self.view.button(cx, ids!(buttons.bind_button)); + let unbind_button = self.view.button(cx, ids!(buttons.unbind_button)); + let current_room_bots_dropdown = self.view.drop_down(cx, ids!(form.current_room_bots_dropdown)); + let known_bots_dropdown = self.view.drop_down(cx, ids!(form.known_bots_dropdown)); + let user_id_input = self.view.text_input(cx, ids!(form.user_id_input)); + let remark_input = self.view.text_input(cx, ids!(form.remark_input)); + let save_remark_button = self.view.button(cx, ids!(form.remark_controls.save_remark_button)); + let mut status_label = self.view.label(cx, ids!(status_label)); + + let cancel_clicked = cancel_button.clicked(actions); + if cancel_clicked + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + { + if cancel_clicked { + cx.action(BotBindingModalAction::Close); + } + return; + } + + if known_bots_dropdown.changed(actions).is_some() { + let selected_item = known_bots_dropdown.selected_item(); + if selected_item == 0 { + user_id_input.set_text(cx, ""); + remark_input.set_text(cx, ""); + user_id_input.set_key_focus(cx); + } else if let Some(bot_user_id) = self.known_bot_user_ids.get(selected_item - 1) { + user_id_input.set_text(cx, bot_user_id.as_str()); + remark_input.set_text( + cx, + self.room_bot_remark(bot_user_id.as_ref()).unwrap_or(""), + ); + } + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if current_room_bots_dropdown.changed(actions).is_some() { + let selected_item = current_room_bots_dropdown.selected_item(); + if let Some(room_bot_binding) = selected_item + .checked_sub(1) + .and_then(|index| self.room_bound_bots.get(index)) + { + user_id_input.set_text(cx, room_bot_binding.bot_user_id.as_str()); + remark_input.set_text(cx, &room_bot_binding.remark); + known_bots_dropdown.set_selected_item( + cx, + self.known_bot_user_ids + .iter() + .position(|bot_user_id| bot_user_id.as_str() == room_bot_binding.bot_user_id.as_str()) + .map_or(0, |index| index + 1), + ); + } + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if save_remark_button.clicked(actions) || remark_input.returned(actions).is_some() { + let Some(room_name_id) = self.room_name_id.as_ref() else { return }; + let raw_user_id = user_id_input.text(); + let raw_user_id = raw_user_id.trim(); + if raw_user_id.is_empty() { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.enter_user_id")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + user_id_input.set_key_focus(cx); + self.view.redraw(cx); + return; + } + + let full_user_id = if raw_user_id.starts_with('@') || raw_user_id.contains(':') { + if raw_user_id.starts_with('@') { + raw_user_id.to_owned() + } else { + format!("@{raw_user_id}") + } + } else { + let Some(current_user_id) = current_user_id() else { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.current_user_unavailable")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + }; + format!("@{raw_user_id}:{}", current_user_id.server_name()) + }; + let Ok(bot_user_id) = UserId::parse(&full_user_id).map(|user_id| user_id.to_owned()) else { + let status = tr_fmt( + self.app_language, + "bot_binding_modal.status.invalid_user_id", + [("full_user_id", full_user_id.as_str())].as_ref(), + ); + script_apply_eval!(cx, status_label, { + text: #(status), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + user_id_input.set_key_focus(cx); + self.view.redraw(cx); + return; + }; + let remark = remark_input.text().trim().to_string(); + let Some(app_state) = scope.data.get_mut::() else { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.state_unavailable")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + }; + if app_state + .bot_settings + .set_room_bot_remark(room_name_id.room_id(), bot_user_id.as_ref(), remark) + { + self.room_bound_bots = app_state.bot_settings.room_bindings_for(room_name_id.room_id()); + self.update_room_bound_bots_value(cx); + let current_room_bots_dropdown = self.view.drop_down(cx, ids!(form.current_room_bots_dropdown)); + current_room_bots_dropdown.set_selected_item( + cx, + self.room_bound_bots + .iter() + .position(|binding| binding.bot_user_id.as_str() == bot_user_id.as_str()) + .map_or(0, |index| index + 1), + ); + persist_bot_settings(app_state); + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.remark_saved")), + draw_text +: { + color: mod.widgets.COLOR_FG_ACCEPT_GREEN + } + }); + } else { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.remark_requires_added_bot")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + } + self.view.redraw(cx); + return; + } + + let mut handle_submit = |bound: bool| { + let Some(room_name_id) = self.room_name_id.as_ref() else { return }; + + let raw_user_id = user_id_input.text(); + let raw_user_id = raw_user_id.trim(); + if raw_user_id.is_empty() { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.enter_user_id")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + user_id_input.set_key_focus(cx); + self.view.redraw(cx); + return; + } + + let full_user_id = if raw_user_id.starts_with('@') || raw_user_id.contains(':') { + if raw_user_id.starts_with('@') { + raw_user_id.to_owned() + } else { + format!("@{raw_user_id}") + } + } else { + let Some(current_user_id) = current_user_id() else { + script_apply_eval!(cx, status_label, { + text: #(tr_key(self.app_language, "bot_binding_modal.status.current_user_unavailable")), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + }; + format!("@{raw_user_id}:{}", current_user_id.server_name()) + }; + + let Ok(bot_user_id) = UserId::parse(&full_user_id).map(|user_id| user_id.to_owned()) else { + let status = tr_fmt( + self.app_language, + "bot_binding_modal.status.invalid_user_id", + [("full_user_id", full_user_id.as_str())].as_ref(), + ); + script_apply_eval!(cx, status_label, { + text: #(status), + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + user_id_input.set_key_focus(cx); + self.view.redraw(cx); + return; + }; + + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id: room_name_id.room_id().clone(), + bound, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + if bound { + tr_fmt( + self.app_language, + "bot_binding_modal.popup.inviting", + [("bot_user_id", bot_user_id.as_str())].as_ref(), + ) + } else { + tr_fmt( + self.app_language, + "bot_binding_modal.popup.removing", + [("bot_user_id", bot_user_id.as_str())].as_ref(), + ) + }, + PopupKind::Info, + Some(4.0), + ); + cx.action(BotBindingModalAction::Close); + }; + + if bind_button.clicked(actions) || user_id_input.returned(actions).is_some() { + handle_submit(true); + return; + } + if unbind_button.clicked(actions) { + handle_submit(false); + } + } +} + +impl BotBindingModal { + fn set_title_and_body(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { + self.view + .label(cx, ids!(title)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.title")); + self.view + .label(cx, ids!(body)) + .set_text( + cx, + &tr_fmt( + self.app_language, + "bot_binding_modal.body", + [("room_name", room_name_id.to_string().as_str())].as_ref(), + ), + ); + } + + fn known_bot_labels(&self) -> Vec { + let mut labels = Vec::with_capacity(self.known_bot_user_ids.len() + 1); + labels.push(tr_key(self.app_language, "bot_binding_modal.dropdown.custom").to_string()); + labels.extend(self.known_bot_user_ids.iter().map(ToString::to_string)); + labels + } + + fn room_bot_remark(&self, bot_user_id: &UserId) -> Option<&str> { + self.room_bound_bots + .iter() + .find(|binding| binding.bot_user_id.as_str() == bot_user_id.as_str()) + .map(|binding| binding.remark.as_str()) + } + + fn room_bound_bot_labels(&self) -> Vec { + if self.room_bound_bots.is_empty() { + return vec![ + tr_key(self.app_language, "bot_binding_modal.hint.current_bound_none").to_string() + ]; + } + self.room_bound_bots + .iter() + .map(|binding| { + let remark = binding.remark.trim(); + if remark.is_empty() { + binding.bot_user_id.as_str().to_string() + } else { + format!("{} ({})", binding.bot_user_id.as_str(), remark) + } + }) + .collect() + } + + fn update_room_bound_bots_value(&mut self, cx: &mut Cx) { + self.view + .drop_down(cx, ids!(form.current_room_bots_dropdown)) + .set_labels(cx, self.room_bound_bot_labels()); + } + + fn update_static_texts(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(form.current_room_bots_label)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.label.current_room_bots")); + self.view + .label(cx, ids!(form.known_bots_label)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.label.known_bots")); + self.view + .label(cx, ids!(form.user_id_label)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.label.user_id")); + self.view + .label(cx, ids!(form.remark_label)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.label.remark")); + self.view + .text_input(cx, ids!(form.user_id_input)) + .set_empty_text( + cx, + tr_key(self.app_language, "bot_binding_modal.input.placeholder").to_string(), + ); + self.view + .text_input(cx, ids!(form.remark_input)) + .set_empty_text( + cx, + tr_key(self.app_language, "bot_binding_modal.input.remark_placeholder").to_string(), + ); + self.view + .button(cx, ids!(form.remark_controls.save_remark_button)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.button.save_remark")); + self.view + .button(cx, ids!(buttons.cancel_button)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.button.cancel")); + self.view + .button(cx, ids!(buttons.bind_button)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.button.bind")); + self.view + .button(cx, ids!(buttons.unbind_button)) + .set_text(cx, tr_key(self.app_language, "bot_binding_modal.button.unbind")); + self.view + .drop_down(cx, ids!(form.known_bots_dropdown)) + .set_labels(cx, self.known_bot_labels()); + } + + pub fn show( + &mut self, + cx: &mut Cx, + room_name_id: RoomNameId, + bot_settings: &BotSettingsState, + app_language: AppLanguage, + ) { + self.app_language = app_language; + self.room_bound_bots = bot_settings.room_bindings_for(room_name_id.room_id()); + self.known_bot_user_ids = bot_settings.known_bot_user_ids(); + for bound_bot_user_id in bot_settings.all_bound_bot_user_ids() { + if !self + .known_bot_user_ids + .iter() + .any(|known_bot_user_id| known_bot_user_id.as_str() == bound_bot_user_id.as_str()) + { + self.known_bot_user_ids.push(bound_bot_user_id); + } + } + self.known_bot_user_ids + .sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + self.known_bot_user_ids + .dedup_by(|lhs, rhs| lhs.as_str() == rhs.as_str()); + self.room_name_id = Some(room_name_id.clone()); + + self.set_title_and_body(cx, &room_name_id); + self.update_static_texts(cx); + self.update_room_bound_bots_value(cx); + + let current_room_bots_dropdown = self.view.drop_down(cx, ids!(form.current_room_bots_dropdown)); + let known_bots_dropdown = self.view.drop_down(cx, ids!(form.known_bots_dropdown)); + let user_id_input = self.view.text_input(cx, ids!(form.user_id_input)); + let remark_input = self.view.text_input(cx, ids!(form.remark_input)); + let selected_item = self + .room_bound_bots + .first() + .and_then(|binding| + self.known_bot_user_ids + .iter() + .position(|known_bot_user_id| known_bot_user_id.as_str() == binding.bot_user_id.as_str()) + ) + .map_or(0, |index| index + 1); + current_room_bots_dropdown.set_selected_item( + cx, + if self.room_bound_bots.is_empty() { 0 } else { 1 }, + ); + known_bots_dropdown.set_selected_item(cx, selected_item); + if let Some(bound_bot) = self.room_bound_bots.first() { + user_id_input.set_text(cx, bound_bot.bot_user_id.as_str()); + remark_input.set_text(cx, &bound_bot.remark); + } else { + user_id_input.set_text(cx, ""); + remark_input.set_text(cx, ""); + } + user_id_input.set_is_read_only(cx, false); + user_id_input.set_key_focus(cx); + self.view.label(cx, ids!(status_label)).set_text(cx, ""); + self.view.button(cx, ids!(buttons.bind_button)).set_enabled(cx, true); + self.view.button(cx, ids!(buttons.unbind_button)).set_enabled(cx, true); + self.view.button(cx, ids!(buttons.cancel_button)).set_enabled(cx, true); + self.view.button(cx, ids!(form.remark_controls.save_remark_button)).set_enabled(cx, true); + self.view.button(cx, ids!(buttons.bind_button)).reset_hover(cx); + self.view.button(cx, ids!(buttons.unbind_button)).reset_hover(cx); + self.view.button(cx, ids!(buttons.cancel_button)).reset_hover(cx); + self.view.button(cx, ids!(form.remark_controls.save_remark_button)).reset_hover(cx); + self.view.redraw(cx); + } +} + +impl BotBindingModalRef { + pub fn show( + &self, + cx: &mut Cx, + room_name_id: RoomNameId, + bot_settings: &BotSettingsState, + app_language: AppLanguage, + ) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.show(cx, room_name_id, bot_settings, app_language); + } +} + +fn persist_bot_settings(app_state: &AppState) { + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(app_state.clone(), user_id) { + error!("Failed to persist bot settings. Error: {e}"); + } + } +} diff --git a/src/home/mod.rs b/src/home/mod.rs index 1176c3723..bcc8008fe 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -1,6 +1,7 @@ use makepad_widgets::ScriptVm; pub mod add_room; +pub mod bot_binding_modal; pub mod create_bot_modal; pub mod delete_bot_modal; pub mod edited_indicator; @@ -38,6 +39,7 @@ pub fn script_mod(vm: &mut ScriptVm) { loading_pane::script_mod(vm); location_preview::script_mod(vm); add_room::script_mod(vm); + bot_binding_modal::script_mod(vm); create_bot_modal::script_mod(vm); delete_bot_modal::script_mod(vm); space_lobby::script_mod(vm); diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 06d2abcc8..1ee4c4fce 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,7 +3,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{app::AppState, home::invite_modal::InviteModalAction, i18n::{AppLanguage, tr_fmt, tr_key}, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId}; +use crate::{app::AppState, home::{bot_binding_modal::BotBindingModalAction, invite_modal::InviteModalAction}, i18n::{AppLanguage, tr_key}, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; @@ -101,7 +101,7 @@ script_mod! { bot_binding_button := mod.widgets.RoomContextMenuButton { draw_icon +: { svg: (ICON_HIERARCHY) } - text: "Bind BotFather" + text: "Manage Bots" } divider2 := LineH { @@ -194,7 +194,7 @@ impl Widget for RoomContextMenu { } impl WidgetMatchEvent for RoomContextMenu { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { let Some(details) = self.details.as_ref() else { return }; let mut close_menu = false; @@ -251,52 +251,7 @@ impl WidgetMatchEvent for RoomContextMenu { close_menu = true; } else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { - if let Some(app_state) = scope.data.get::() { - let room_id = details.room_name_id.room_id().clone(); - match app_state.bot_settings.resolved_bot_user_id_for_room( - &room_id, - current_user_id().as_deref(), - ) { - Ok(bot_user_id) => { - if details.is_bot_bound { - submit_async_request(MatrixRequest::SetRoomBotBinding { - room_id, - bound: false, - bot_user_id: bot_user_id.clone(), - }); - enqueue_popup_notification( - tr_fmt(self.app_language, "room_context_menu.popup.removing_botfather", &[ - ("bot_user_id", bot_user_id.as_str()), - ]), - PopupKind::Info, - Some(4.0), - ); - } else { - submit_async_request(MatrixRequest::SetRoomBotBinding { - room_id, - bound: true, - bot_user_id: bot_user_id.clone(), - }); - enqueue_popup_notification( - tr_fmt(self.app_language, "room_context_menu.popup.inviting_botfather", &[ - ("bot_user_id", bot_user_id.as_str()), - ]), - PopupKind::Info, - Some(5.0), - ); - } - } - Err(error) => { - enqueue_popup_notification(error, PopupKind::Error, Some(5.0)); - } - } - } else { - enqueue_popup_notification( - tr_key(self.app_language, "room_context_menu.popup.bot_settings_unavailable"), - PopupKind::Error, - Some(5.0), - ); - } + cx.action(BotBindingModalAction::Open(details.room_name_id.clone())); close_menu = true; } else if self.button(cx, ids!(leave_button)).clicked(actions) { @@ -365,11 +320,7 @@ impl RoomContextMenu { let bot_binding_button = self.button(cx, ids!(bot_binding_button)); bot_binding_button.set_visible(cx, details.app_service_enabled); - if details.is_bot_bound { - bot_binding_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.unbind_botfather")); - } else { - bot_binding_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.bind_botfather")); - } + bot_binding_button.set_text(cx, tr_key(self.app_language, "room_context_menu.button.manage_bots")); // Reset hover states mark_unread_button.reset_hover(cx); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 306a4d463..64ca67a43 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{ use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, i18n::{AppLanguage, tr_fmt, tr_key}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{bot_binding_modal::BotBindingModalAction, create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, i18n::{AppLanguage, tr_fmt, tr_key}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, @@ -262,49 +262,131 @@ fn detected_bot_binding_for_members( return None; } - let Ok(bot_user_id) = app_state + let own_user_id = current_user_id(); + let mut non_self_members = members + .iter() + .filter(|room_member| + own_user_id + .as_deref() + .is_none_or(|own_user_id| room_member.user_id() != own_user_id) + ) + .collect::>(); + non_self_members.sort_by(|lhs, rhs| lhs.user_id().as_str().cmp(rhs.user_id().as_str())); + + if let Ok(configured_bot_user_id) = app_state .bot_settings - .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) - else { - return None; - }; + .resolved_bot_user_id(current_user_id().as_deref()) + { + if non_self_members + .iter() + .any(|room_member| room_member.user_id().as_str() == configured_bot_user_id.as_str()) + { + return Some(configured_bot_user_id); + } + } - members + let known_bot_user_ids = app_state.bot_settings.known_bot_user_ids(); + if let Some(bot_member) = non_self_members .iter() - .any(|room_member| room_member.user_id() == bot_user_id) - .then_some(bot_user_id) -} + .find(|room_member| + known_bot_user_ids + .iter() + .any(|known_bot_user_id| known_bot_user_id.as_str() == room_member.user_id().as_str()) + ) + { + return Some(bot_member.user_id().to_owned()); + } -fn is_likely_bot_user_id( - user_id: &UserId, - resolved_parent_bot_user_id: Option<&UserId>, -) -> bool { - if resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) { - return true; + if non_self_members.len() == 1 { + let dm_counterparty = non_self_members[0]; + let localpart = dm_counterparty.user_id().localpart().to_ascii_lowercase(); + let localpart_likely_bot = localpart == "bot" + || localpart == "botfather" + || localpart.starts_with("bot_") + || localpart.starts_with("bot-") + || localpart.starts_with("bot."); + let display_name_likely_bot = dm_counterparty + .display_name() + .is_some_and(|display_name| display_name.to_ascii_lowercase().contains("bot")); + if localpart_likely_bot || display_name_likely_bot { + return Some(dm_counterparty.user_id().to_owned()); + } } - let localpart = user_id.localpart().to_ascii_lowercase(); - localpart == "bot" - || localpart.starts_with("bot_") - || localpart.ends_with("_bot") - || (localpart.ends_with("bot") && localpart.len() > 3) + if non_self_members + .iter() + .any(|room_member| room_member.user_id().localpart().to_ascii_lowercase() == "botfather") + { + return non_self_members + .iter() + .find(|room_member| room_member.user_id().localpart().to_ascii_lowercase() == "botfather") + .map(|room_member| room_member.user_id().to_owned()); + }; + None } -fn is_likely_bot_member( - room_member: &RoomMember, - resolved_parent_bot_user_id: Option<&UserId>, -) -> bool { - if is_likely_bot_user_id(room_member.user_id(), resolved_parent_bot_user_id) { - return true; +fn extract_bot_user_ids_from_listbots_reply( + text: &str, + default_server_name: Option<&OwnedServerName>, +) -> Vec { + let mut bot_user_ids = Vec::::new(); + + let mut push_bot = |bot_user_id: OwnedUserId| { + if !bot_user_ids + .iter() + .any(|existing_bot_user_id| existing_bot_user_id.as_str() == bot_user_id.as_str()) + { + bot_user_ids.push(bot_user_id); + } + }; + + for token in text.split(|ch: char| + !(ch.is_ascii_alphanumeric() || matches!(ch, '@' | ':' | '_' | '-' | '.')) + ) { + let token = token.trim(); + if token.is_empty() { + continue; + } + + if token.starts_with('@') && token.contains(':') { + if let Ok(bot_user_id) = UserId::parse(token).map(|user_id| user_id.to_owned()) { + push_bot(bot_user_id); + } + continue; + } + + if token.contains(':') && !token.starts_with('@') { + let full_user_id = format!("@{token}"); + if let Ok(bot_user_id) = UserId::parse(&full_user_id).map(|user_id| user_id.to_owned()) { + push_bot(bot_user_id); + } + continue; + } + + let localpart_lc = token.to_ascii_lowercase(); + let is_likely_bot_localpart = ( + localpart_lc == "bot" + || localpart_lc.starts_with("bot_") + || localpart_lc.starts_with("bot-") + || localpart_lc.starts_with("bot.") + ) + && localpart_lc != "bots" + && localpart_lc != "botfather" + && token + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' || ch == '.'); + if !is_likely_bot_localpart { + continue; + } + + let Some(default_server_name) = default_server_name else { continue }; + let full_user_id = format!("@{token}:{default_server_name}"); + if let Ok(bot_user_id) = UserId::parse(&full_user_id).map(|user_id| user_id.to_owned()) { + push_bot(bot_user_id); + } } - room_member.display_name().is_some_and(|display_name| { - let display_name = display_name.trim().to_ascii_lowercase(); - display_name == "bot" - || display_name.starts_with("bot ") - || display_name.ends_with(" bot") - || display_name.contains(" bot ") - }) + bot_user_ids } script_mod! { @@ -1996,12 +2078,32 @@ impl Widget for RoomScreen { .get::() .map(|app_state| { let app_service_enabled = app_state.bot_settings.enabled; - let app_service_room_bound = self.is_app_service_room_bound(app_state, &room_id); - let bound_bot_user_id = if app_service_enabled && app_service_room_bound { - app_state.bot_settings.bound_bot_user_id(&room_id).map(ToOwned::to_owned) + let persisted_bound_bot_user_id = + app_state.bot_settings.bound_bot_user_id(&room_id).map(ToOwned::to_owned); + let detected_bound_bot_user_id = room_members + .as_ref() + .and_then(|members| + detected_bot_binding_for_members( + app_state, + &room_id, + members.as_ref(), + ) + ); + if persisted_bound_bot_user_id.is_none() + && detected_bound_bot_user_id.is_some() + && let Some(bot_user_id) = detected_bound_bot_user_id.as_ref() + { + Cx::post_action(AppStateAction::BotRoomBindingDetected { + room_id: room_id.clone(), + bot_user_id: bot_user_id.clone(), + }); + } + let bound_bot_user_id = if app_service_enabled { + persisted_bound_bot_user_id.or(detected_bound_bot_user_id) } else { None }; + let app_service_room_bound = bound_bot_user_id.is_some(); ( app_service_enabled, app_service_room_bound, @@ -2152,65 +2254,10 @@ impl Widget for RoomScreen { return false; } AppServicePanelAction::ShowBoundBots => { - let room_id = room_props.room_name_id.room_id(); - let own_user_id = current_user_id(); - let mut bound_bots = Vec::::new(); - let mut push_unique_bot = |bot_user_id: OwnedUserId| { - if !bound_bots.iter().any(|existing| existing == &bot_user_id) { - bound_bots.push(bot_user_id); - } - }; - - if let Some(bound_bot_user_id) = room_props.bound_bot_user_id.as_ref() { - push_unique_bot(bound_bot_user_id.clone()); - } - - let mut resolved_parent_bot_user_id: Option = None; - if let Some(app_state) = scope.data.get::() { - for room_binding in &app_state.bot_settings.room_bindings { - if &room_binding.room_id == room_id { - push_unique_bot(room_binding.bot_user_id.clone()); - } - } - - resolved_parent_bot_user_id = app_state - .bot_settings - .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) - .ok(); - if let Some(bot_user_id) = resolved_parent_bot_user_id.as_ref() { - push_unique_bot(bot_user_id.clone()); - } - } - - if let Some(room_members) = room_props.room_members.as_ref() { - for room_member in room_members.iter() { - if own_user_id - .as_deref() - .is_some_and(|own_user_id| own_user_id == room_member.user_id()) - { - continue; - } - if is_likely_bot_member( - room_member, - resolved_parent_bot_user_id.as_deref(), - ) { - push_unique_bot(room_member.user_id().to_owned()); - } - } - } - - if bound_bots.is_empty() { - self.send_app_service_feedback_message( - "No bots are currently bound to this room.", - ); - } else { - let mut message = String::from("Bots bound to this room:"); - for bot_user_id in &bound_bots { - message.push('\n'); - message.push_str(bot_user_id.as_str()); - } - self.send_app_service_feedback_message(message); - } + cx.action(BotBindingModalAction::Open( + room_props.room_name_id.clone(), + )); + self.set_app_service_actions_visible(cx, false); return false; } AppServicePanelAction::Unbind => { @@ -2329,10 +2376,6 @@ impl Widget for RoomScreen { self.send_app_service_feedback_message( tr_key(self.app_language, "room_screen.popup.bot.enable_in_settings_before_bot"), ); - } else if !room_props.app_service_room_bound { - self.send_app_service_feedback_message( - tr_key(self.app_language, "room_screen.popup.bot.bind_before_bot"), - ); } else { self.toggle_app_service_actions(cx); } @@ -2649,6 +2692,49 @@ impl RoomScreen { Some(plaintext_body_of_timeline_item(event)) } + fn discover_known_bot_user_ids_from_timeline_items( + app_state: &AppState, + timeline_items: &Vector>, + ) -> Vec { + let Ok(parent_bot_user_id) = app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + else { + return Vec::new(); + }; + + let default_server_name = current_user_id() + .map(|user_id| user_id.server_name().to_owned()); + let mut discovered_bot_user_ids = Vec::::new(); + let mut push_bot_user_id = |bot_user_id: OwnedUserId| { + if bot_user_id.as_str() == parent_bot_user_id.as_str() { + return; + } + if !discovered_bot_user_ids + .iter() + .any(|existing_bot_user_id| existing_bot_user_id.as_str() == bot_user_id.as_str()) + { + discovered_bot_user_ids.push(bot_user_id); + } + }; + + for item in timeline_items { + let TimelineItemKind::Event(event_tl_item) = item.kind() else { continue }; + if event_tl_item.sender().as_str() != parent_bot_user_id.as_str() { + continue; + } + let Some(message_text) = Self::extract_message_text(item) else { continue }; + for bot_user_id in extract_bot_user_ids_from_listbots_reply( + &message_text, + default_server_name.as_ref(), + ) { + push_bot_user_id(bot_user_id); + } + } + + discovered_bot_user_ids + } + fn schedule_stream_timeout(&mut self, cx: &mut Cx) { cx.stop_timer(self.streaming_timeout_timer); self.streaming_timeout_timer = next_stream_timeout( @@ -2869,6 +2955,18 @@ impl RoomScreen { num_updates += 1; match update { TimelineUpdate::FirstUpdate { initial_items } => { + if let Some(app_state) = app_state { + let discovered_bot_user_ids = + Self::discover_known_bot_user_ids_from_timeline_items( + app_state, + &initial_items, + ); + if !discovered_bot_user_ids.is_empty() { + Cx::post_action(AppStateAction::KnownBotUserIdsDiscovered { + bot_user_ids: discovered_bot_user_ids, + }); + } + } tl.content_drawn_since_last_update.clear(); tl.profile_drawn_since_last_update.clear(); tl.fully_paginated = false; @@ -2896,6 +2994,18 @@ impl RoomScreen { done_loading = true; } TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { + if let Some(app_state) = app_state { + let discovered_bot_user_ids = + Self::discover_known_bot_user_ids_from_timeline_items( + app_state, + &new_items, + ); + if !discovered_bot_user_ids.is_empty() { + Cx::post_action(AppStateAction::KnownBotUserIdsDiscovered { + bot_user_ids: discovered_bot_user_ids, + }); + } + } if new_items.is_empty() { if !tl.items.is_empty() { log!("process_timeline_updates(): timeline (had {} items) was cleared for room {}", tl.items.len(), tl.kind.room_id()); @@ -6659,6 +6769,12 @@ impl Widget for AppServicePanel { .props .get::() .expect("BUG: RoomScreenProps should be available in Scope::props for AppServicePanel"); + self.view + .button(cx, ids!(keyboard.third_row.view_bound_button)) + .set_visible(cx, room_screen_props.app_service_enabled); + self.view + .button(cx, ids!(keyboard.third_row.unbind_button)) + .set_visible(cx, room_screen_props.app_service_room_bound); if let Event::Actions(actions) = event { if self @@ -6782,6 +6898,9 @@ impl AppServicePanel { self.view .button(cx, ids!(keyboard.second_row.help_button)) .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.bot_help")); + self.view + .button(cx, ids!(keyboard.third_row.view_bound_button)) + .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.bots")); self.view .button(cx, ids!(keyboard.third_row.unbind_button)) .set_text(cx, tr_key(self.app_language, "room_screen.app_service.button.unbind")); diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index a56b8eae2..abbcfb001 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -777,11 +777,6 @@ impl RoomInputBar { "Enable App Service in Settings before using /bot.", PopupKind::Warning, )) - } else if !room_screen_props.app_service_room_bound { - Some(( - "Bind BotFather to this room before using /bot.", - PopupKind::Warning, - )) } else { None }; diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index 200116764..abe580a11 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -4,7 +4,6 @@ use crate::{ app::{AppState, BotSettingsState}, i18n::{AppLanguage, tr_key}, persistence, - shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::current_user_id, }; @@ -42,58 +41,17 @@ script_mod! { width: Fill height: Fit flow: Right - align: Align{y: 0.5} - spacing: 12 + align: Align{x: 0.0, y: 0.5} margin: Inset{left: 5, bottom: 2} - enable_label := SubsectionLabel { + app_service_switch := Toggle { width: Fit height: Fit - margin: 0 - text: "Enable App Service" - } - - toggle_button := RobrixNeutralIconButton { - width: Fit - height: Fit - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} - draw_icon.svg: (ICON_HIERARCHY) - icon_walk: Walk{width: 16, height: 16} - text: "Enable App Service" - } - } - - bot_details := View { - visible: false - width: Fill - height: Fit - flow: Down - - bot_user_id_label := SubsectionLabel { - text: "BotFather User ID:" - } - - bot_user_id_input := RobrixTextInput { - margin: Inset{top: 2, left: 5, right: 5, bottom: 8} - width: 280 - height: Fit - empty_text: "bot or @bot:server" - } - - buttons := View { - width: Fill - height: Fit - flow: Right - spacing: 10 - - save_button := RobrixPositiveIconButton { - width: Fit - height: Fit - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} - margin: Inset{left: 5} - draw_icon.svg: (ICON_CHECKMARK) - icon_walk: Walk{width: 16, height: 16} - text: "Save" + padding: Inset{top: 8, right: 8, bottom: 8, left: 8} + text: "" + active: false + draw_bg +: { + size: 20.0 } } } @@ -133,34 +91,18 @@ impl Widget for BotSettings { impl WidgetMatchEvent for BotSettings { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let toggle_button = self.view.button(cx, ids!(toggle_button)); - let bot_details = self.view.view(cx, ids!(bot_details)); - let bot_user_id_input = self.view.text_input(cx, ids!(bot_user_id_input)); - let save_button = self.view.button(cx, ids!(buttons.save_button)); + let app_service_switch = self.view.check_box(cx, ids!(app_service_switch)); let Some(app_state) = _scope.data.get_mut::() else { return; }; - if toggle_button.clicked(actions) { - let enabled = !app_state.bot_settings.enabled; + if let Some(enabled) = app_service_switch.changed(actions) { app_state.bot_settings.enabled = enabled; persist_bot_settings(app_state); self.sync_ui(cx, &app_state.bot_settings); - bot_details.set_visible(cx, enabled); self.view.redraw(cx); } - - if save_button.clicked(actions) || bot_user_id_input.returned(actions).is_some() { - app_state.bot_settings.botfather_user_id = bot_user_id_input.text().trim().to_string(); - persist_bot_settings(app_state); - enqueue_popup_notification( - tr_key(self.app_language, "settings.labs.app_service.popup.saved"), - PopupKind::Success, - Some(3.0), - ); - self.sync_ui(cx, &app_state.bot_settings); - } } } @@ -177,41 +119,13 @@ impl BotSettings { self.view .label(cx, ids!(description)) .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.description")); - self.view - .label(cx, ids!(enable_label)) - .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.enable_label")); - self.view - .label(cx, ids!(bot_user_id_label)) - .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.botfather_user_id")); - self.view - .text_input(cx, ids!(bot_user_id_input)) - .set_empty_text(cx, tr_key(self.app_language, "settings.labs.app_service.botfather_placeholder").to_string()); - self.view - .button(cx, ids!(buttons.save_button)) - .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.button.save")); self.view.redraw(cx); } fn sync_ui(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { self.view - .view(cx, ids!(bot_details)) - .set_visible(cx, bot_settings.enabled); - self.view - .text_input(cx, ids!(bot_user_id_input)) - .set_text(cx, &bot_settings.botfather_user_id); - - let toggle_text = if bot_settings.enabled { - tr_key(self.app_language, "settings.labs.app_service.button.disable") - } else { - tr_key(self.app_language, "settings.labs.app_service.button.enable") - }; - self.view - .button(cx, ids!(toggle_button)) - .set_text(cx, toggle_text); - self.view.button(cx, ids!(toggle_button)).reset_hover(cx); - self.view - .button(cx, ids!(buttons.save_button)) - .reset_hover(cx); + .check_box(cx, ids!(app_service_switch)) + .set_active(cx, bot_settings.enabled); self.view.redraw(cx); } From a162b43ae257fddfb7d57b27a8ad491b6e24e466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 18:29:52 +0800 Subject: [PATCH 099/283] refactor(settings): polish app service switch layout and status text --- resources/i18n/en.json | 2 +- resources/i18n/zh-CN.json | 2 +- src/settings/bot_settings.rs | 62 ++++++++++++++++++++++++++++++++---- 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index c4dc5d713..65ff2e596 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -210,7 +210,7 @@ "settings.account.modal.delete_avatar.accept": "Delete", "settings.labs.app_service.title": "App Service", - "settings.labs.app_service.description": "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands.", + "settings.labs.app_service.description": "Enable Matrix app service support here. ", "settings.labs.app_service.enable_label": "Enable App Service", "settings.labs.app_service.manage_hint": "Manage BotFather and child bots in DM and room bind dialogs. Settings here only control whether App Service features are enabled.", "settings.labs.app_service.botfather_user_id": "BotFather User ID:", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 49a385f19..edd15fe85 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -210,7 +210,7 @@ "settings.account.modal.delete_avatar.accept": "删除", "settings.labs.app_service.title": "应用服务", - "settings.labs.app_service.description": "在这里启用 Matrix 应用服务支持。Robrix 仍然是普通 Matrix 客户端:它会把 BotFather 绑定到房间,并发送对应的斜杠命令。", + "settings.labs.app_service.description": "在这里启用 Matrix 应用服务支持。", "settings.labs.app_service.enable_label": "启用应用服务", "settings.labs.app_service.manage_hint": "BotFather 与子机器人请在 DM 和房间绑定弹窗中管理;这里仅控制是否启用 App Service 功能。", "settings.labs.app_service.botfather_user_id": "BotFather 用户 ID:", diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index abe580a11..e7f34013d 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -28,13 +28,28 @@ script_mod! { flow: Down spacing: 10 - app_service_title := TitleLabel { - text: "App Service" - } + app_service_header := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 1.0} + spacing: 8 + margin: Inset{left: 5, right: 8, bottom: 2} + + app_service_title := TitleLabel { + width: Fit + text: "App Service" + } - description := mod.widgets.BotSettingsInfoLabel { - margin: Inset{left: 5, right: 8, bottom: 4} - text: "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands." + description := mod.widgets.BotSettingsInfoLabel { + width: Fill + margin: 0 + draw_text +: { + color: #7A7A7A + text_style: REGULAR_TEXT { font_size: 9.5 } + } + text: "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands." + } } toggle_row := View { @@ -42,6 +57,7 @@ script_mod! { height: Fit flow: Right align: Align{x: 0.0, y: 0.5} + spacing: 8 margin: Inset{left: 5, bottom: 2} app_service_switch := Toggle { @@ -54,6 +70,16 @@ script_mod! { size: 20.0 } } + + switch_state_label := Label { + width: Fit + height: Fit + draw_text +: { + color: #999 + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Disabled" + } } } } @@ -107,6 +133,25 @@ impl WidgetMatchEvent for BotSettings { } impl BotSettings { + fn set_switch_state_label(&mut self, cx: &mut Cx, enabled: bool) { + let mut switch_state_label = self.view.label(cx, ids!(switch_state_label)); + if enabled { + script_apply_eval!(cx, switch_state_label, { + text: #(tr_key(self.app_language, "settings.labs.app_service.status.enabled")), + draw_text +: { + color: mod.widgets.COLOR_FG_ACCEPT_GREEN + } + }); + } else { + script_apply_eval!(cx, switch_state_label, { + text: #(tr_key(self.app_language, "settings.labs.app_service.status.disabled")), + draw_text +: { + color: #999 + } + }); + } + } + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { self.app_language = app_language; self.sync_app_language(cx); @@ -119,6 +164,10 @@ impl BotSettings { self.view .label(cx, ids!(description)) .set_text(cx, tr_key(self.app_language, "settings.labs.app_service.description")); + self.set_switch_state_label( + cx, + self.view.check_box(cx, ids!(app_service_switch)).active(cx), + ); self.view.redraw(cx); } @@ -126,6 +175,7 @@ impl BotSettings { self.view .check_box(cx, ids!(app_service_switch)) .set_active(cx, bot_settings.enabled); + self.set_switch_state_label(cx, bot_settings.enabled); self.view.redraw(cx); } From 03cac245bfd8987b301049789e18ce9cd3e149d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 6 Apr 2026 20:10:30 +0800 Subject: [PATCH 100/283] fix(room-screen): restore bot detection helpers for room info people list --- src/home/room_screen.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 64bf2852c..afca94ed6 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -328,6 +328,42 @@ fn detected_bot_binding_for_members( None } +fn is_likely_bot_user_id( + user_id: &UserId, + resolved_parent_bot_user_id: Option<&UserId>, +) -> bool { + if resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) { + return true; + } + + let localpart = user_id.localpart().to_ascii_lowercase(); + localpart == "bot" + || localpart == "botfather" + || localpart.starts_with("bot_") + || localpart.starts_with("bot-") + || localpart.starts_with("bot.") + || localpart.ends_with("_bot") + || (localpart.ends_with("bot") && localpart.len() > 3) +} + +fn is_likely_bot_member( + room_member: &RoomMember, + resolved_parent_bot_user_id: Option<&UserId>, +) -> bool { + if is_likely_bot_user_id(room_member.user_id(), resolved_parent_bot_user_id) { + return true; + } + + room_member.display_name().is_some_and(|display_name| { + let display_name = display_name.trim().to_ascii_lowercase(); + display_name == "bot" + || display_name == "botfather" + || display_name.starts_with("bot ") + || display_name.ends_with(" bot") + || display_name.contains(" bot ") + }) +} + fn extract_bot_user_ids_from_listbots_reply( text: &str, default_server_name: Option<&OwnedServerName>, From 5e6627452daf906c33f07b51321a9a79d15d6ab6 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 11:57:42 +0800 Subject: [PATCH 101/283] perf: narrow streaming animation redraw to timeline list only - Replace self.redraw(cx) with redraw_timeline_list() that only redraws the timeline PortalList, skipping input bar and typing notice - Skip redraw entirely when streaming messages are not visible in viewport - Apply same optimization to streaming timeout handler Closes #53 --- src/home/room_screen.rs | 49 +++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 692e9913a..7720ef29d 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -139,6 +139,17 @@ where } } +fn any_timeline_indices_visible( + indices: I, + mut is_visible: F, +) -> bool +where + I: IntoIterator>, + F: FnMut(usize) -> bool, +{ + indices.into_iter().flatten().any(|idx| is_visible(idx)) +} + fn streaming_candidates_from_items<'a>( items: &'a Vector>, ) -> impl Iterator + 'a { @@ -2762,24 +2773,25 @@ impl Widget for RoomScreen { let frame_start = std::time::Instant::now(); if let Some(tl) = self.tl_state.as_mut() { - let mut any_active = false; let mut needs_another_frame = false; let mut completed_ids = Vec::new(); + let mut redraw_candidate_indices = Vec::new(); for (event_id, state) in tl.streaming_messages.iter_mut() { if state.needs_frame() { if state.tick() { - any_active = true; // Invalidate draw cache so item gets re-populated if let Some(idx) = state.timeline_index { tl.content_drawn_since_last_update.remove(idx..idx + 1); } + redraw_candidate_indices.push(state.timeline_index); } needs_another_frame |= state.needs_frame(); } if state.is_complete() || state.is_timed_out() { completed_ids.push(event_id.clone()); + redraw_candidate_indices.push(state.timeline_index); } } @@ -2789,12 +2801,12 @@ impl Widget for RoomScreen { // Safety cap: max 50 streaming entries while tl.streaming_messages.len() > 50 { - if let Some(oldest_id) = tl.streaming_messages.iter() + if let Some((oldest_id, oldest_idx)) = tl.streaming_messages.iter() .min_by_key(|(_, s)| s.animation_start_time) - .map(|(id, _)| id.clone()) + .map(|(id, state)| (id.clone(), state.timeline_index)) { tl.streaming_messages.remove(&oldest_id); - any_active = true; + redraw_candidate_indices.push(oldest_idx); } } @@ -2802,8 +2814,11 @@ impl Widget for RoomScreen { self.streaming_next_frame = cx.new_next_frame(); } - if any_active || !completed_ids.is_empty() { - self.redraw(cx); + if any_timeline_indices_visible( + redraw_candidate_indices.iter().copied(), + |idx| portal_list.get_item(idx).is_some(), + ) { + self.redraw_timeline_list(cx); } } @@ -2823,24 +2838,27 @@ impl Widget for RoomScreen { if self.streaming_timeout_timer.is_event(event).is_some() { if let Some(tl) = self.tl_state.as_mut() { - let timed_out_ids: Vec = tl + let timed_out_entries: Vec<(OwnedEventId, Option)> = tl .streaming_messages .iter() .filter_map(|(event_id, state)| { if state.is_timed_out() || state.is_complete() { - Some(event_id.clone()) + Some((event_id.clone(), state.timeline_index)) } else { None } }) .collect(); - for event_id in &timed_out_ids { + for (event_id, _) in &timed_out_entries { tl.streaming_messages.remove(event_id); } - if !timed_out_ids.is_empty() { - self.redraw(cx); + if any_timeline_indices_visible( + timed_out_entries.iter().map(|(_, idx)| *idx), + |idx| portal_list.get_item(idx).is_some(), + ) { + self.redraw_timeline_list(cx); } } @@ -3918,6 +3936,13 @@ impl RoomScreen { self.view.redraw(cx); } + fn redraw_timeline_list(&self, cx: &mut Cx) { + let portal_list = self.portal_list(cx, ids!(timeline.list)); + if let Some(mut list) = portal_list.borrow_mut() { + list.redraw(cx); + } + } + fn room_id(&self) -> Option<&OwnedRoomId> { self.room_name_id.as_ref().map(|r| r.room_id()) } From d810f4fa40e229dd6424f6a3f0741760a8fd177d Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 12:22:45 +0800 Subject: [PATCH 102/283] fix: auto-scroll to bottom when user sends a message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When new items are appended to the timeline, check if the last item was sent by the current user. If so, scroll to the bottom so the user always sees their own sent message — regardless of current scroll position. Closes #55 --- src/home/room_screen.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 692e9913a..7adcac3ae 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -4311,6 +4311,23 @@ impl RoomScreen { // and then replaces the existing timeline in ALL_ROOMS_INFO with the new one. } + // If the last appended item was sent by the current user, + // scroll to bottom so the user always sees their own message. + if is_append && new_items.len() > tl.items.len() { + let sent_by_self = new_items.last() + .and_then(|item| match item.kind() { + TimelineItemKind::Event(ev) => Some(ev.sender()), + _ => None, + }) + .is_some_and(|sender| { + current_user_id().is_some_and(|uid| sender == uid) + }); + if sent_by_self { + portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0); + portal_list.set_tail_range(true); + } + } + let prior_items_changed = clear_cache || changed_indices.start <= curr_first_id; if new_items.len() == tl.items.len() { From 44583ae686c3b017c4623f555032befb3ab0289e Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 13:41:27 +0800 Subject: [PATCH 103/283] refactor: use MessageAction signal instead of sender matching for auto-scroll Replace the indirect "check if last timeline item sender is current user" approach with a direct MessageSubmittedLocally action emitted from room_input_bar at send time. This follows Makepad's widget action communication pattern and avoids async timing dependencies. --- src/home/room_screen.rs | 28 +++++++++++----------------- src/room/room_input_bar.rs | 8 ++++++++ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 7adcac3ae..02ad34284 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -4311,23 +4311,6 @@ impl RoomScreen { // and then replaces the existing timeline in ALL_ROOMS_INFO with the new one. } - // If the last appended item was sent by the current user, - // scroll to bottom so the user always sees their own message. - if is_append && new_items.len() > tl.items.len() { - let sent_by_self = new_items.last() - .and_then(|item| match item.kind() { - TimelineItemKind::Event(ev) => Some(ev.sender()), - _ => None, - }) - .is_some_and(|sender| { - current_user_id().is_some_and(|uid| sender == uid) - }); - if sent_by_self { - portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0); - portal_list.set_tail_range(true); - } - } - let prior_items_changed = clear_cache || changed_indices.start <= curr_first_id; if new_items.len() == tl.items.len() { @@ -5052,6 +5035,15 @@ impl RoomScreen { ); } } + MessageAction::MessageSubmittedLocally => { + let Some(tl) = self.tl_state.as_ref() else { continue }; + let last_item_idx = tl.items.len().saturating_sub(1); + portal_list.set_first_id_and_scroll(last_item_idx, 0.0); + portal_list.set_tail_range(true); + self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)) + .update_visibility(cx, true); + self.redraw(cx); + } MessageAction::Pin(details) => { let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_id) = details.event_id() { @@ -8120,6 +8112,8 @@ pub enum MessageAction { Edit(MessageDetails), /// The user requested to edit their latest message in this room. EditLatest, + /// The user submitted a new local message and the timeline should follow the live tail. + MessageSubmittedLocally, /// The user clicked the "pin" button on a message. Pin(MessageDetails), /// The user clicked the "unpin" button on a message. diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 189bd7991..315fa1d84 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -572,6 +572,10 @@ impl RoomInputBar { #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::MessageSubmittedLocally, + ); self.clear_replying_to(cx); location_preview.clear(); @@ -633,6 +637,10 @@ impl RoomInputBar { #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::MessageSubmittedLocally, + ); self.clear_replying_to(cx); mentionable_text_input.set_text(cx, ""); From 472a229edbac9baed5cd55843951ac80183f22ed Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 14:12:41 +0800 Subject: [PATCH 104/283] fix: prevent context menu from dismissing on trackpad press release Track the opening gesture (digit_id + capture_time) when showing a context menu, and consume the corresponding FingerUp event instead of treating it as a "click outside" dismissal. Fixes #57 --- src/app.rs | 18 +++++++++--- src/home/mod.rs | 44 +++++++++++++++++++++++++++- src/home/new_message_context_menu.rs | 29 ++++++++++++++---- src/home/room_context_menu.rs | 29 +++++++++++++++--- src/home/room_screen.rs | 9 +++++- src/home/rooms_list.rs | 6 ++-- src/home/rooms_list_entry.rs | 16 +++++++--- 7 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4e77f58e5..be54cd2e5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -929,10 +929,15 @@ impl MatchEvent for App { } // Handle an action requesting to open the new message context menu. - if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { + if let MessageAction::OpenMessageContextMenu { details, abs_pos, opening_gesture } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); - let expected_dimensions = new_message_context_menu.show(cx, details, self.app_state.app_language); + let expected_dimensions = new_message_context_menu.show( + cx, + details, + self.app_state.app_language, + opening_gesture, + ); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(abs_pos.x, rect.size.x - expected_dimensions.x); @@ -952,10 +957,15 @@ impl MatchEvent for App { } // Handle an action requesting to open the room context menu. - if let RoomsListAction::OpenRoomContextMenu { details, pos } = action.as_widget_action().cast() { + if let RoomsListAction::OpenRoomContextMenu { details, pos, opening_gesture } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); - let expected_dimensions = room_context_menu.show(cx, details, self.app_state.app_language); + let expected_dimensions = room_context_menu.show( + cx, + details, + self.app_state.app_language, + opening_gesture, + ); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(pos.x, rect.size.x - expected_dimensions.x); diff --git a/src/home/mod.rs b/src/home/mod.rs index bcc8008fe..383f90d0d 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -1,4 +1,4 @@ -use makepad_widgets::ScriptVm; +use makepad_widgets::{ScriptVm, event::{DigitId, FingerDownEvent, FingerLongPressEvent, FingerUpEvent}}; pub mod add_room; pub mod bot_binding_modal; @@ -34,6 +34,48 @@ pub mod link_preview; pub mod room_image_viewer; pub mod streaming_animation; +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ContextMenuOpenGesture { + digit_id: DigitId, + capture_time: f64, +} + +impl ContextMenuOpenGesture { + pub fn from_finger_down(event: &FingerDownEvent) -> Self { + Self { + digit_id: event.digit_id, + capture_time: event.time, + } + } + + pub fn from_long_press(event: &FingerLongPressEvent) -> Self { + Self { + digit_id: event.digit_id, + capture_time: event.capture_time, + } + } + + fn matches_finger_up(&self, event: &FingerUpEvent) -> bool { + self.digit_id == event.digit_id + && self.capture_time == event.capture_time + } +} + +pub fn consume_context_menu_opening_finger_up( + pending_open_gesture: &mut Option, + event: &FingerUpEvent, +) -> bool { + if pending_open_gesture + .as_ref() + .is_some_and(|gesture| gesture.matches_finger_up(event)) + { + *pending_open_gesture = None; + true + } else { + false + } +} + pub fn script_mod(vm: &mut ScriptVm) { search_messages::script_mod(vm); loading_pane::script_mod(vm); diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 21df36931..8f39dff15 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -8,7 +8,7 @@ use matrix_sdk_ui::timeline::{EventTimelineItem, MsgLikeContent, TimelineEventIt use crate::{i18n::{AppLanguage, tr_key}, sliding_sync::UserPowerLevels}; -use super::room_screen::MessageAction; +use super::{ContextMenuOpenGesture, consume_context_menu_opening_finger_up, room_screen::MessageAction}; const BUTTON_HEIGHT: f64 = 35.0; // KEEP IN SYNC WITH BUTTON_HEIGHT BELOW const MENU_WIDTH: f64 = 215.0; // KEEP IN SYNC WITH MENU_WIDTH BELOW @@ -301,6 +301,7 @@ pub struct NewMessageContextMenu { #[source] source: ScriptObjectRef, #[rust] details: Option, #[rust] app_language: AppLanguage, + #[rust] pending_open_gesture: Option, } impl Widget for NewMessageContextMenu { @@ -337,7 +338,11 @@ impl Widget for NewMessageContextMenu { false } Hit::FingerUp(fue) if fue.is_over => { - !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + if consume_context_menu_opening_finger_up(&mut self.pending_open_gesture, &fue) { + false + } else { + !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + } } Hit::FingerScroll(_) => true, _ => false, @@ -519,9 +524,16 @@ impl NewMessageContextMenu { /// /// Returns the expected (approximate) dimensions of the context menu, /// which can be used to proactively reposition it such that it fits on screen. - pub fn show(&mut self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage) -> DVec2 { + pub fn show( + &mut self, + cx: &mut Cx, + details: MessageDetails, + app_language: AppLanguage, + opening_gesture: ContextMenuOpenGesture, + ) -> DVec2 { self.set_app_language(cx, app_language); self.details = Some(details); + self.pending_open_gesture = Some(opening_gesture); self.visible = true; cx.set_key_focus(self.view.area()); @@ -641,6 +653,7 @@ impl NewMessageContextMenu { fn close(&mut self, cx: &mut Cx) { self.visible = false; self.details = None; + self.pending_open_gesture = None; cx.revert_key_focus(); self.redraw(cx); } @@ -654,8 +667,14 @@ impl NewMessageContextMenuRef { } /// See [`NewMessageContextMenu::show()`]. - pub fn show(&self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage) -> DVec2 { + pub fn show( + &self, + cx: &mut Cx, + details: MessageDetails, + app_language: AppLanguage, + opening_gesture: ContextMenuOpenGesture, + ) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; - inner.show(cx, details, app_language) + inner.show(cx, details, app_language, opening_gesture) } } diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 1ee4c4fce..b1771907e 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -5,6 +5,8 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use crate::{app::AppState, home::{bot_binding_modal::BotBindingModalAction, invite_modal::InviteModalAction}, i18n::{AppLanguage, tr_key}, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; +use super::{ContextMenuOpenGesture, consume_context_menu_opening_finger_up}; + const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; @@ -148,6 +150,7 @@ pub struct RoomContextMenu { #[source] source: ScriptObjectRef, #[rust] details: Option, #[rust] app_language: AppLanguage, + #[rust] pending_open_gesture: Option, } impl Widget for RoomContextMenu { @@ -177,7 +180,11 @@ impl Widget for RoomContextMenu { || match event.hits_with_capture_overload(cx, area, true) { Hit::KeyUp(key) => key.key_code == KeyCode::Escape, Hit::FingerUp(fue) if fue.is_over => { - !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + if consume_context_menu_opening_finger_up(&mut self.pending_open_gesture, &fue) { + false + } else { + !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + } } Hit::FingerScroll(_) => true, _ => false, @@ -276,10 +283,17 @@ impl RoomContextMenu { self.visible } - pub fn show(&mut self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage) -> DVec2 { + pub fn show( + &mut self, + cx: &mut Cx, + details: RoomContextMenuDetails, + app_language: AppLanguage, + opening_gesture: ContextMenuOpenGesture, + ) -> DVec2 { self.app_language = app_language; let height = self.update_buttons(cx, &details); self.details = Some(details); + self.pending_open_gesture = Some(opening_gesture); self.visible = true; cx.set_key_focus(self.view.area()); dvec2(MENU_WIDTH, height) @@ -343,6 +357,7 @@ impl RoomContextMenu { fn close(&mut self, cx: &mut Cx) { self.visible = false; self.details = None; + self.pending_open_gesture = None; cx.revert_key_focus(); self.redraw(cx); } @@ -354,8 +369,14 @@ impl RoomContextMenuRef { inner.is_currently_shown(cx) } - pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage) -> DVec2 { + pub fn show( + &self, + cx: &mut Cx, + details: RoomContextMenuDetails, + app_language: AppLanguage, + opening_gesture: ContextMenuOpenGesture, + ) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; - inner.show(cx, details, app_language) + inner.show(cx, details, app_language, opening_gesture) } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 692e9913a..8c4039b76 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -44,7 +44,7 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction; use rangemap::RangeSet; -use super::{event_reaction_list::ReactionData, invite_modal::is_invite_modal_open, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; +use super::{ContextMenuOpenGesture, event_reaction_list::ReactionData, invite_modal::is_invite_modal_open, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; /// The maximum number of timeline items to search through /// when looking for a particular event. @@ -8142,6 +8142,7 @@ pub enum MessageAction { /// The absolute position where we should show the context menu, /// in which the (0,0) origin coordinate is the top left corner of the app window. abs_pos: DVec2, + opening_gesture: ContextMenuOpenGesture, }, /// The user requested opening the message action bar ActionBarOpen { @@ -8382,6 +8383,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, + opening_gesture: ContextMenuOpenGesture::from_finger_down(&fe), } ); } @@ -8392,6 +8394,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, + opening_gesture: ContextMenuOpenGesture::from_long_press(&lp), } ); } @@ -8423,6 +8426,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, + opening_gesture: ContextMenuOpenGesture::from_finger_down(&fe), } ); } @@ -8439,6 +8443,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, + opening_gesture: ContextMenuOpenGesture::from_long_press(&lp), } ); } @@ -8474,6 +8479,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, + opening_gesture: ContextMenuOpenGesture::from_finger_down(&fe), } ); } @@ -8484,6 +8490,7 @@ impl Widget for Message { MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, + opening_gesture: ContextMenuOpenGesture::from_long_press(&lp), } ); } diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index ba1e167b1..ca9c97158 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -26,6 +26,7 @@ use matrix_sdk::{RoomState, ruma::{events::tag::Tags, MilliSecondsSinceUnixEpoch use crate::{ app::{AppState, SelectedRoom}, home::{ + ContextMenuOpenGesture, add_room::CreateRoomAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, room_context_menu::RoomContextMenuDetails, @@ -252,6 +253,7 @@ pub enum RoomsListAction { OpenRoomContextMenu { details: RoomContextMenuDetails, pos: DVec2, + opening_gesture: ContextMenuOpenGesture, }, #[default] None, @@ -1342,7 +1344,7 @@ impl Widget for RoomsList { self.redraw(cx); } // Handle a room being right-clicked or long-pressed by opening the room context menu. - else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = action.as_widget_action().cast() { + else if let RoomsListEntryAction::SecondaryClicked(room_id, pos, opening_gesture) = action.as_widget_action().cast() { // Determine details for the context menu let Some(jr) = self.all_joined_rooms.get(&room_id) else { error!("BUG: couldn't find right-clicked room details for room {room_id}"); @@ -1359,7 +1361,7 @@ impl Widget for RoomsList { }; cx.widget_action( self.widget_uid(), - RoomsListAction::OpenRoomContextMenu { details, pos }, + RoomsListAction::OpenRoomContextMenu { details, pos, opening_gesture }, ); } // Handle the space lobby being clicked. diff --git a/src/home/rooms_list_entry.rs b/src/home/rooms_list_entry.rs index cd45744fd..df9392ced 100644 --- a/src/home/rooms_list_entry.rs +++ b/src/home/rooms_list_entry.rs @@ -10,7 +10,7 @@ use crate::{ }, utils::{self, relative_format} }; -use super::rooms_list::{InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListScopeProps}; +use super::{ContextMenuOpenGesture, rooms_list::{InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListScopeProps}}; script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -223,7 +223,7 @@ pub enum RoomsListEntryAction { /// This RoomsListEntry was primary-clicked or tapped. PrimaryClicked(OwnedRoomId), /// This RoomsListEntry was right-clicked or long-pressed. - SecondaryClicked(OwnedRoomId, DVec2), + SecondaryClicked(OwnedRoomId, DVec2, ContextMenuOpenGesture), #[default] None, } @@ -262,14 +262,22 @@ impl Widget for RoomsListEntry { if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( uid, - RoomsListEntryAction::SecondaryClicked(room_id.clone(), fe.abs), + RoomsListEntryAction::SecondaryClicked( + room_id.clone(), + fe.abs, + ContextMenuOpenGesture::from_finger_down(&fe), + ), ); } } Hit::FingerLongPress(fe) => { cx.widget_action( uid, - RoomsListEntryAction::SecondaryClicked(room_id.clone(), fe.abs), + RoomsListEntryAction::SecondaryClicked( + room_id.clone(), + fe.abs, + ContextMenuOpenGesture::from_long_press(&fe), + ), ); } Hit::FingerUp(fe) if !rooms_list_props.was_scrolling && fe.is_over && fe.is_primary_hit() && fe.was_tap() => { From d442c2671bf77de8446780ed59a8ee32d04f3560 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Tue, 7 Apr 2026 15:28:40 +0800 Subject: [PATCH 105/283] attachment upload --- Cargo.lock | 586 ++++++++++++++++++++++++++++- Cargo.toml | 2 + resources/icons/add_attachment.svg | 3 + resources/icons/file.svg | 3 + src/app.rs | 62 ++- src/home/mod.rs | 3 + src/home/room_screen.rs | 45 +++ src/home/upload_progress.rs | 277 ++++++++++++++ src/image_utils.rs | 104 +++++ src/lib.rs | 1 + src/room/room_input_bar.rs | 255 ++++++++++++- 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, 1912 insertions(+), 7 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 0272c850e..2182661e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,24 @@ dependencies = [ "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -169,6 +187,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.7.1" @@ -181,6 +205,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "argon2" version = "0.5.3" @@ -224,6 +259,15 @@ dependencies = [ "serde", ] +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "as_variant" version = "1.3.0" @@ -590,6 +634,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.14.1" @@ -739,6 +826,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "2.10.0" @@ -759,6 +852,15 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + [[package]] name = "blake2" version = "0.10.6" @@ -873,12 +975,24 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + [[package]] name = "bumpalo" 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" @@ -895,6 +1009,12 @@ name = "byteorder" version = "1.5.0" source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +[[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" @@ -940,7 +1060,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -1100,6 +1220,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1255,6 +1381,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1784,6 +1929,26 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1832,6 +1997,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "zune-inflate", +] + [[package]] name = "eyeball" version = "0.8.8" @@ -1902,6 +2082,35 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32 0.3.9", +] + [[package]] name = "ff" version = "0.13.1" @@ -2195,6 +2404,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "glob" version = "0.3.3" @@ -2724,6 +2943,40 @@ 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", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imbl" version = "6.1.0" @@ -2754,6 +3007,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8b35f3ad95576ac81603375dfe47a0450b70a368aa34d2b6e5bb0a0d7f02428" +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + [[package]] name = "impartial-ord" version = "1.0.6" @@ -2817,6 +3076,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2979,12 +3249,28 @@ dependencies = [ "spin", ] +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.8" @@ -3073,6 +3359,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3470,7 +3765,7 @@ name = "makepad-zune-inflate" version = "0.2.0" source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" dependencies = [ - "simd-adler32", + "simd-adler32 0.3.8", ] [[package]] @@ -3861,6 +4156,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-5" version = "0.10.6" @@ -3903,6 +4208,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" @@ -3916,6 +4231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32 0.3.9", ] [[package]] @@ -3929,6 +4245,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" @@ -4053,6 +4379,21 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -4062,6 +4403,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -4084,6 +4435,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -4104,6 +4466,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4506,6 +4879,19 @@ 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" @@ -4637,6 +5023,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "prost" version = "0.13.5" @@ -4688,6 +5093,27 @@ 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 = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.39.2" @@ -4842,12 +5268,82 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "readlock" version = "0.1.9" @@ -5014,6 +5510,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -5130,6 +5632,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "htmlize", + "image", "imbl", "imghdr", "indexmap 2.13.0", @@ -5140,6 +5643,7 @@ dependencies = [ "matrix-sdk-base", "matrix-sdk-ui", "mime", + "mime_guess", "percent-encoding", "quinn", "rand 0.8.5", @@ -5469,7 +5973,7 @@ version = "0.18.0" source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" dependencies = [ "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", - "bytemuck", + "bytemuck 1.25.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", "makepad-error-log", "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", "ttf-parser", @@ -5878,6 +6382,21 @@ name = "simd-adler32" version = "0.3.8" source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -6317,6 +6836,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -6898,6 +7431,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -7237,6 +7781,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + [[package]] name = "whoami" version = "1.6.1" @@ -7836,6 +8386,12 @@ version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.0" @@ -8021,6 +8577,30 @@ 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-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32 0.3.9", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.10.0" diff --git a/Cargo.toml b/Cargo.toml index 18dc0c4d4..a8597c24b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,9 +43,11 @@ futures-util = "0.3" hashbrown = { version = "0.16", features = ["raw-entry"] } htmlize = "1.0.5" indexmap = "2.6.0" +image = "0.25" imghdr = "0.7.0" linkify = "0.10.0" mime = "0.3" +mime_guess = "2.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 = [ "e2e-encryption", diff --git a/resources/icons/add_attachment.svg b/resources/icons/add_attachment.svg new file mode 100644 index 000000000..9b25e62b6 --- /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 000000000..56e57fc12 --- /dev/null +++ b/resources/icons/file.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/app.rs b/src/app.rs index 4e77f58e5..53eeaad5f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,10 +12,10 @@ use crate::{ avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt}, bot_binding_modal::{BotBindingModalAction, BotBindingModalWidgetRefExt}, - event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt, mark_invite_modal_closed}, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef + event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt, mark_invite_modal_closed}, 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, TimelineUpdate, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, 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::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -129,7 +129,15 @@ script_mod! { image_viewer_modal_inner := ImageViewer {} } } - + + file_upload_modal := Modal { + content +: { + width: Fill, height: Fill, + align: Align{x: 0.5, y: 0.5}, + file_upload_modal_inner := FileUploadModal {} + } + } + // Context menus should be shown in front of other UI elements, // but behind verification modals. new_message_context_menu := NewMessageContextMenu { } @@ -1208,6 +1216,33 @@ impl MatchEvent for App { } _ => {} } + // 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) | Some(FilePreviewerAction::Cancelled) => { + self.ui.modal(cx, ids!(file_upload_modal)).close(cx); + continue; + } + Some(FilePreviewerAction::UploadConfirmed(file_data)) => { + // Send the file upload event to the current room's timeline + 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(); + } + } + } + self.ui.modal(cx, ids!(file_upload_modal)).close(cx); + continue; + } + _ => {} + } // Handle actions to open/close the TSP verification modal. #[cfg(feature = "tsp")] { use std::ops::Deref; @@ -1409,6 +1444,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); @@ -2329,6 +2365,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(TimelineKind::MainRoom { + room_id: room_name_id.room_id().clone(), + }) + } + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + Some(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 bcc8008fe..000db1feb 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -33,6 +33,7 @@ pub mod room_context_menu; pub mod link_preview; pub mod room_image_viewer; pub mod streaming_animation; +pub mod upload_progress; pub fn script_mod(vm: &mut ScriptVm) { search_messages::script_mod(vm); @@ -65,6 +66,8 @@ pub fn script_mod(vm: &mut ScriptVm) { main_desktop_ui::script_mod(vm); spaces_bar::script_mod(vm); navigation_tab_bar::script_mod(vm); + // Note: upload_progress::script_mod is called earlier in app.rs + // because RoomInputBar depends on it. // 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 692e9913a..bc85ea328 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -4738,6 +4738,35 @@ impl RoomScreen { tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} + TimelineUpdate::FileUploadConfirmed(file_data) => { + 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 } => { + println!("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); + } + TimelineUpdate::FileUploadComplete => { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .hide_upload_progress(cx); + } } } @@ -6181,6 +6210,22 @@ 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, + }, + /// File upload completed successfully. + FileUploadComplete, } thread_local! { diff --git a/src/home/upload_progress.rs b/src/home/upload_progress.rs new file mode 100644 index 000000000..d59fba8ae --- /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 { + enabled: 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.widget_action(self.widget_uid(), 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.widget_action(self.widget_uid(), 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_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); + + 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_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 + + 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 000000000..3b866dcb0 --- /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 4bcc78ecf..67d2c9fcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ pub mod utils; pub mod account_manager; 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 189bd7991..5847c0f56 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, OwnedUserId}; -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}}, i18n::AppLanguage, 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}, i18n::AppLanguage, 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}; const ROOM_INFO_CARD_MOBILE_BREAKPOINT: f32 = 700.0; @@ -84,6 +84,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. @@ -226,6 +229,23 @@ script_mod! { margin: Inset{bottom: 9, left: 6, right: 0} } + // Attachment button for uploading files/images + send_attachment_button := RobrixIconButton { + margin: Inset{left: 3, right: 1, top: 4, bottom: 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: "", + } + emoji_picker_button := RobrixIconButton { margin: Inset{left: 3, right: 1, top: 4, bottom: 4} spacing: 0, @@ -335,6 +355,9 @@ pub struct RoomInputBar { /// Cached natural Fit height of the input_bar, used as the animation /// target when the editing pane is being hidden. #[rust] input_bar_natural_height: f64, + /// 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 { @@ -369,6 +392,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); } @@ -458,6 +511,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); + } + let picked_emoji = if self.button(cx, ids!(emoji_smile_button)).clicked(actions) { Some("😀") } else if self.button(cx, ids!(emoji_joy_button)).clicked(actions) { @@ -882,6 +941,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.as_ref()) { + 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 { @@ -1025,6 +1179,105 @@ 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); + } + + /// 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> { + use matrix_sdk::room::reply::{EnforceThread, Reply}; + + 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) + } +} + +/// 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, + } } /// The saved UI state of a `RoomInputBar` widget. diff --git a/src/shared/file_upload_modal.rs b/src/shared/file_upload_modal.rs new file mode 100644 index 000000000..851b6c7b4 --- /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 a92a81fd9..e9a04b020 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 000000000..36f2aaab7 --- /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 29daace62..72503e226 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 351337f0b..a915ba36a 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -919,6 +919,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 @@ -2749,6 +2757,94 @@ 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::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}."); + let _ = sender.send(TimelineUpdate::FileUploadComplete); + } + 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}"); @@ -3303,6 +3399,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 aa3ac8143..636053171 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 12c5e6f8d3914e08d3d0027056772d51735eeff6 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Tue, 7 Apr 2026 15:29:54 +0800 Subject: [PATCH 106/283] remove println --- src/home/room_screen.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index bc85ea328..b02dba9e2 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -4751,7 +4751,6 @@ impl RoomScreen { } } TimelineUpdate::FileUploadUpdate { current, total } => { - println!("TimelineUpdate::FileUploadUpdate: {}/{}", current, total); self.view.room_input_bar(cx, ids!(room_input_bar)) .set_upload_progress(cx, current, total); } From 797d81d6a71d8d25ffdef42b173f8aacb3000491 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 15:39:20 +0800 Subject: [PATCH 107/283] fix: filter zero-scroll FingerScroll and restore capture_time matching The actual root cause of the trackpad press dismissal was FingerScroll(0,0): macOS generates a zero-scroll event on two-finger press, which the menu's unconditional `Hit::FingerScroll(_) => true` treated as a close trigger. - Only close on FingerScroll when scroll delta is non-zero - Restore capture_time field in ContextMenuOpenGesture for precise gesture matching (digit_id + capture_time from Makepad capture chain) Fixes #57 --- src/home/mod.rs | 4 ++++ src/home/new_message_context_menu.rs | 4 +++- src/home/room_context_menu.rs | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/home/mod.rs b/src/home/mod.rs index 383f90d0d..6f02aa8eb 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -37,6 +37,10 @@ pub mod streaming_animation; #[derive(Clone, Copy, Debug, PartialEq)] pub struct ContextMenuOpenGesture { digit_id: DigitId, + /// The time of the original `FingerDown` that opened the menu. + /// Matches `FingerUpEvent.capture_time` for the same capture chain + /// (see Makepad `finger.rs`: `capture_digit` stores `e.time` as `capture.time`, + /// and `FingerUpEvent` reads it back as `capture_time`). capture_time: f64, } diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 8f39dff15..8e0f7ee8e 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -344,7 +344,9 @@ impl Widget for NewMessageContextMenu { !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) } } - Hit::FingerScroll(_) => true, + // Ignore zero-scroll events: macOS trackpad generates FingerScroll(0,0) + // on two-finger press (right-click), which would incorrectly dismiss the menu. + Hit::FingerScroll(fse) => fse.scroll.x != 0.0 || fse.scroll.y != 0.0, _ => false, } }; diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index b1771907e..647d99d40 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -186,7 +186,9 @@ impl Widget for RoomContextMenu { !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) } } - Hit::FingerScroll(_) => true, + // Ignore zero-scroll events: macOS trackpad generates FingerScroll(0,0) + // on two-finger press (right-click), which would incorrectly dismiss the menu. + Hit::FingerScroll(fse) => fse.scroll.x != 0.0 || fse.scroll.y != 0.0, _ => false, } }; From 71c70d9f6d6c2e9cf5007183f0d76684c9ada70d Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 7 Apr 2026 15:49:13 +0800 Subject: [PATCH 108/283] style: restore horizontal parameter formatting for show() calls Keep function signatures and call sites in their original horizontal style to minimize diff noise in the PR. --- src/app.rs | 14 ++------------ src/home/new_message_context_menu.rs | 16 ++-------------- src/home/room_context_menu.rs | 16 ++-------------- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/src/app.rs b/src/app.rs index be54cd2e5..05b8ae359 100644 --- a/src/app.rs +++ b/src/app.rs @@ -932,12 +932,7 @@ impl MatchEvent for App { if let MessageAction::OpenMessageContextMenu { details, abs_pos, opening_gesture } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); - let expected_dimensions = new_message_context_menu.show( - cx, - details, - self.app_state.app_language, - opening_gesture, - ); + let expected_dimensions = new_message_context_menu.show(cx, details, self.app_state.app_language, opening_gesture); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(abs_pos.x, rect.size.x - expected_dimensions.x); @@ -960,12 +955,7 @@ impl MatchEvent for App { if let RoomsListAction::OpenRoomContextMenu { details, pos, opening_gesture } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); - let expected_dimensions = room_context_menu.show( - cx, - details, - self.app_state.app_language, - opening_gesture, - ); + let expected_dimensions = room_context_menu.show(cx, details, self.app_state.app_language, opening_gesture); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); let pos_x = min(pos.x, rect.size.x - expected_dimensions.x); diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 8e0f7ee8e..6fab28a70 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -526,13 +526,7 @@ impl NewMessageContextMenu { /// /// Returns the expected (approximate) dimensions of the context menu, /// which can be used to proactively reposition it such that it fits on screen. - pub fn show( - &mut self, - cx: &mut Cx, - details: MessageDetails, - app_language: AppLanguage, - opening_gesture: ContextMenuOpenGesture, - ) -> DVec2 { + pub fn show(&mut self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage, opening_gesture: ContextMenuOpenGesture) -> DVec2 { self.set_app_language(cx, app_language); self.details = Some(details); self.pending_open_gesture = Some(opening_gesture); @@ -669,13 +663,7 @@ impl NewMessageContextMenuRef { } /// See [`NewMessageContextMenu::show()`]. - pub fn show( - &self, - cx: &mut Cx, - details: MessageDetails, - app_language: AppLanguage, - opening_gesture: ContextMenuOpenGesture, - ) -> DVec2 { + pub fn show(&self, cx: &mut Cx, details: MessageDetails, app_language: AppLanguage, opening_gesture: ContextMenuOpenGesture) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; inner.show(cx, details, app_language, opening_gesture) } diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 647d99d40..cc176491d 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -285,13 +285,7 @@ impl RoomContextMenu { self.visible } - pub fn show( - &mut self, - cx: &mut Cx, - details: RoomContextMenuDetails, - app_language: AppLanguage, - opening_gesture: ContextMenuOpenGesture, - ) -> DVec2 { + pub fn show(&mut self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage, opening_gesture: ContextMenuOpenGesture) -> DVec2 { self.app_language = app_language; let height = self.update_buttons(cx, &details); self.details = Some(details); @@ -371,13 +365,7 @@ impl RoomContextMenuRef { inner.is_currently_shown(cx) } - pub fn show( - &self, - cx: &mut Cx, - details: RoomContextMenuDetails, - app_language: AppLanguage, - opening_gesture: ContextMenuOpenGesture, - ) -> DVec2 { + pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails, app_language: AppLanguage, opening_gesture: ContextMenuOpenGesture) -> DVec2 { let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; inner.show(cx, details, app_language, opening_gesture) } From a70b0066c21f92a6a207d9654ebf663405fbaf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:12:10 +0800 Subject: [PATCH 109/283] Merge pull request #61 from tyreseluo/fix/ci-clippy-typos fix: make clippy and typos checks pass --- src/home/room_screen.rs | 8 ++++---- src/home/streaming_animation.rs | 2 +- src/i18n.rs | 2 +- src/room/member_search.rs | 12 ++++++------ src/shared/mentionable_text_input.rs | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index b02dba9e2..4a4d7e473 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -319,11 +319,11 @@ fn detected_bot_binding_for_members( if non_self_members .iter() - .any(|room_member| room_member.user_id().localpart().to_ascii_lowercase() == "botfather") + .any(|room_member| room_member.user_id().localpart().eq_ignore_ascii_case("botfather")) { return non_self_members .iter() - .find(|room_member| room_member.user_id().localpart().to_ascii_lowercase() == "botfather") + .find(|room_member| room_member.user_id().localpart().eq_ignore_ascii_case("botfather")) .map(|room_member| room_member.user_id().to_owned()); }; None @@ -2995,10 +2995,10 @@ impl Widget for RoomScreen { .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) { if &bot_user_id == user_id - && !app_state + && app_state .bot_settings .bound_bot_user_id(room_id.as_ref()) - .is_some_and(|existing_bot_user_id| existing_bot_user_id.as_str() == user_id.as_str()) + .is_none_or(|existing_bot_user_id| existing_bot_user_id.as_str() != user_id.as_str()) { cx.action(AppStateAction::BotRoomBindingUpdated { room_id: room_id.clone(), diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 2846885d6..4da2bebf2 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -315,7 +315,7 @@ mod tests { let mut s = make_state("Hello"); s.advance_displayed(3); s.fill_display_buffer(); - assert!(s.display_buffer.starts_with("Hel")); + assert!(s.display_buffer.starts_with("He")); assert!(s.display_buffer.contains('\u{25CF}') || s.display_buffer.contains('●')); } diff --git a/src/i18n.rs b/src/i18n.rs index 2f19806e4..7604d7375 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -87,7 +87,7 @@ fn dictionary(language: AppLanguage) -> &'static HashMap { } } -pub fn tr_key<'a>(language: AppLanguage, key: &'a str) -> &'a str { +pub fn tr_key(language: AppLanguage, key: &str) -> &str { dictionary(language) .get(key) .map(String::as_str) diff --git a/src/room/member_search.rs b/src/room/member_search.rs index dacb68c73..27ba01182 100644 --- a/src/room/member_search.rs +++ b/src/room/member_search.rs @@ -916,7 +916,7 @@ mod tests { #[test] fn test_grapheme_starts_with_basic() { // Basic ASCII cases - assert!(grapheme_starts_with("hello", "hel", false)); + assert!(grapheme_starts_with("hello", "hell", false)); assert!(grapheme_starts_with("hello", "hello", false)); assert!(!grapheme_starts_with("hello", "llo", false)); assert!(grapheme_starts_with("hello", "", false)); @@ -926,9 +926,9 @@ mod tests { #[test] fn test_grapheme_starts_with_case_sensitivity() { // Case-insensitive for ASCII - assert!(grapheme_starts_with("Hello", "hel", true)); - assert!(grapheme_starts_with("HELLO", "hel", true)); - assert!(!grapheme_starts_with("Hello", "hel", false)); + assert!(grapheme_starts_with("Hello", "hell", true)); + assert!(grapheme_starts_with("HELLO", "hell", true)); + assert!(!grapheme_starts_with("Hello", "hell", false)); // Case-insensitive only works for ASCII assert!(!grapheme_starts_with("Привет", "прив", true)); // Russian @@ -960,8 +960,8 @@ mod tests { let decomposed = "cafe\u{0301}"; // e + combining acute accent (U+0065 + U+0301) // Both should work - assert!(grapheme_starts_with(precomposed, "caf", false)); - assert!(grapheme_starts_with(decomposed, "caf", false)); + assert!(grapheme_starts_with(precomposed, "ca", false)); + assert!(grapheme_starts_with(decomposed, "ca", false)); // Other combining characters assert!(grapheme_starts_with("naïve", "naï", false)); // ï with diaeresis diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index a5de90b45..1a0bd8354 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -1691,7 +1691,7 @@ impl MentionableTextInput { // Ensure cache is up-to-date (rebuild only if text changed) let needs_rebuild = self.cached_text_analysis.as_ref() - .map_or(true, |(cached_text, _, _)| cached_text != text); + .is_none_or(|(cached_text, _, _)| cached_text != text); if needs_rebuild { let graphemes_owned: Vec = text.graphemes(true).map(|s| s.to_string()).collect(); let positions = utils::build_grapheme_byte_positions(text); From e3e7fcac11d048c16d942d1a20dc2361e44e3a88 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 04:12:43 +0800 Subject: [PATCH 110/283] Add real-time translation feature and fix Settings language dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-time translation: - New translation service module with OpenAI-compatible LLM API - Translation settings UI in Settings → Labs (API URL, key, model) - Translate button in room input bar with language selector popup - Debounce-based real-time translation as user types - Translation preview with Apply button to replace input text - Global config via Mutex for cross-widget access - Config auto-refreshes from global state on every event Settings language dropdown fix: - Remove broken set_type_default() custom DropDown types - Use inline DropDown instance with correct uniform/plain typing - Key insight: draw_text.color must be plain value, all other color variants must use uniform() to match shader declarations Co-Authored-By: Claude Opus 4.6 (1M context) --- resources/i18n/en.json | 21 + resources/i18n/zh-CN.json | 21 + resources/icons/translate.svg | 8 + src/app.rs | 5 + src/home/home_screen.rs | 2 +- src/home/room_screen.rs | 286 ++++++++++++- src/i18n.rs | 29 ++ src/room/mod.rs | 1 + src/room/room_input_bar.rs | 573 ++++++++++++++++++++++++++- src/room/translation.rs | 215 ++++++++++ src/settings/mod.rs | 2 + src/settings/settings_screen.rs | 91 ++++- src/settings/translation_settings.rs | 434 ++++++++++++++++++++ 13 files changed, 1670 insertions(+), 18 deletions(-) create mode 100644 resources/icons/translate.svg create mode 100644 src/room/translation.rs create mode 100644 src/settings/translation_settings.rs diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 272224ae3..ad4d53747 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -223,6 +223,27 @@ "settings.labs.app_service.button.save": "Save", "settings.labs.app_service.popup.saved": "Saved Matrix app service settings.", + "settings.labs.translation.title": "Real-time Translation", + "settings.labs.translation.description": "Configure an OpenAI-compatible API for real-time message translation in the input bar.", + "settings.labs.translation.status.enabled": "Enabled", + "settings.labs.translation.status.disabled": "Disabled", + "settings.labs.translation.field.api_url": "API URL", + "settings.labs.translation.field.api_key": "API Key", + "settings.labs.translation.field.model": "Model", + "settings.labs.translation.button.save": "Save", + "settings.labs.translation.button.test_connection": "Test Connection", + "settings.labs.translation.validation.api_url_empty": "API URL is empty", + "settings.labs.translation.test.testing": "Testing...", + "settings.labs.translation.test.ok": "OK: {result}", + "settings.labs.translation.test.failed": "Failed: {error}", + "settings.labs.translation.test.error": "Error: {error}", + + "room_input_bar.input.placeholder": "Write a message (in Markdown) ...", + "room_input_bar.translation.preview.apply": "Apply", + "room_input_bar.translation.preview.idle": "Start typing to translate...", + "room_input_bar.translation.preview.loading": "Translating...", + "room_input_bar.translation.preview.error": "Error: {error}", + "invite_screen.message.invited_by": "has invited you to join:", "invite_screen.message.invited_generic": "You have been invited to join:", "invite_screen.button.reject": "Reject Invite", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 239ecc01f..dba930b02 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -223,6 +223,27 @@ "settings.labs.app_service.button.save": "保存", "settings.labs.app_service.popup.saved": "已保存 Matrix 应用服务设置。", + "settings.labs.translation.title": "实时翻译", + "settings.labs.translation.description": "在输入栏中配置兼容 OpenAI API 的实时消息翻译服务。", + "settings.labs.translation.status.enabled": "已启用", + "settings.labs.translation.status.disabled": "未启用", + "settings.labs.translation.field.api_url": "API URL", + "settings.labs.translation.field.api_key": "API Key", + "settings.labs.translation.field.model": "模型", + "settings.labs.translation.button.save": "保存", + "settings.labs.translation.button.test_connection": "测试连接", + "settings.labs.translation.validation.api_url_empty": "API URL 不能为空", + "settings.labs.translation.test.testing": "测试中...", + "settings.labs.translation.test.ok": "成功:{result}", + "settings.labs.translation.test.failed": "失败:{error}", + "settings.labs.translation.test.error": "错误:{error}", + + "room_input_bar.input.placeholder": "输入消息(支持 Markdown)...", + "room_input_bar.translation.preview.apply": "应用", + "room_input_bar.translation.preview.idle": "开始输入即可翻译...", + "room_input_bar.translation.preview.loading": "翻译中...", + "room_input_bar.translation.preview.error": "错误:{error}", + "invite_screen.message.invited_by": "邀请你加入:", "invite_screen.message.invited_generic": "你被邀请加入:", "invite_screen.button.reject": "拒绝邀请", diff --git a/resources/icons/translate.svg b/resources/icons/translate.svg new file mode 100644 index 000000000..8d8b8eb9c --- /dev/null +++ b/resources/icons/translate.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/app.rs b/src/app.rs index 53eeaad5f..2fc0caead 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1035,6 +1035,8 @@ impl MatchEvent for App { let logged_in_actual = self.app_state.logged_in; self.app_state = app_state.clone(); self.app_state.logged_in = logged_in_actual; + // Initialize the global translation config so RoomInputBar can access it. + crate::room::translation::set_global_config(&self.app_state.translation); cx.action(MainDesktopUiAction::LoadDockFromAppState); continue; } @@ -2016,6 +2018,9 @@ pub struct AppState { pub adding_account: bool, /// Local configuration and UI state for bot-assisted room binding. pub bot_settings: BotSettingsState, + /// Translation API configuration. + #[serde(default)] + pub translation: crate::room::translation::TranslationConfig, } /// Local bot integration settings persisted per Matrix account. diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 9caa1455f..a0260386b 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -455,7 +455,7 @@ impl Widget for HomeScreen { if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { settings_page .settings_screen(cx, ids!(settings_screen)) - .populate(cx, None, &app_state.bot_settings, app_state.app_language); + .populate(cx, None, &app_state.bot_settings, &app_state.translation, app_state.app_language); self.view.redraw(cx); } else { error!("BUG: failed to set active page to show settings screen."); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4a4d7e473..e5d08be64 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -31,7 +31,7 @@ use crate::{ user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, typing_notice::TypingNoticeWidgetExt}, + room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, translation, typing_notice::TypingNoticeWidgetExt}, shared::{ avatar::{AvatarState, AvatarWidgetExt, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalAction, ConfirmationModalContent, ConfirmationModalWidgetExt}, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, @@ -62,6 +62,11 @@ const VIEWPORT_FILL_PAGINATION_SIZE: u16 = 150; const TOPIC_PREVIEW_CHARS: usize = 140; const ROOM_INFO_PANE_DESKTOP_WIDTH: f32 = 320.0; const ROOM_INFO_PANE_MOBILE_BREAKPOINT: f32 = 700.0; +const TRANSLATION_LANG_POPUP_WIDTH: f64 = 220.0; +const TRANSLATION_LANG_POPUP_SCROLL_HEIGHT: f64 = 288.0; +const TRANSLATION_LANG_POPUP_HEIGHT: f64 = TRANSLATION_LANG_POPUP_SCROLL_HEIGHT + 8.0; +const TRANSLATION_LANG_POPUP_GAP: f64 = 6.0; +const TRANSLATION_LANG_POPUP_MARGIN: f64 = 8.0; /// #FFF4E5 @@ -103,6 +108,29 @@ fn is_msc4357_live(event_tl_item: &EventTimelineItem) -> bool { .unwrap_or(false) } +fn compute_translation_lang_popup_abs_pos(button_rect: Rect, container_rect: Rect) -> DVec2 { + let min_x = container_rect.pos.x + TRANSLATION_LANG_POPUP_MARGIN; + let max_x = (container_rect.pos.x + container_rect.size.x - TRANSLATION_LANG_POPUP_WIDTH - TRANSLATION_LANG_POPUP_MARGIN) + .max(min_x); + let popup_x = button_rect.pos.x + .max(min_x) + .min(max_x); + + let min_y = container_rect.pos.y + TRANSLATION_LANG_POPUP_MARGIN; + let max_y = (container_rect.pos.y + container_rect.size.y - TRANSLATION_LANG_POPUP_HEIGHT - TRANSLATION_LANG_POPUP_MARGIN) + .max(min_y); + let popup_y_above = button_rect.pos.y - TRANSLATION_LANG_POPUP_HEIGHT - TRANSLATION_LANG_POPUP_GAP; + let popup_y = if popup_y_above >= min_y { + popup_y_above + } else { + (button_rect.pos.y + button_rect.size.y + TRANSLATION_LANG_POPUP_GAP) + .max(min_y) + .min(max_y) + }; + + dvec2(popup_x, popup_y) +} + fn streaming_scan_range( clear_cache: bool, changed_indices: &Range, @@ -1834,6 +1862,28 @@ script_mod! { jump_to_bottom_button := JumpToBottomButton { } } + mod.widgets.TranslationLangPopupButton = RobrixIconButton { + width: Fill + height: 36 + spacing: 0 + margin: 0 + padding: Inset{left: 12, right: 12, top: 8, bottom: 8} + icon_walk: Walk{width: 0, height: 0} + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + draw_bg +: { + color: #0000 + color_hover: #xF0F4FA + color_down: #xE8EEF8 + border_size: 0.0 + border_radius: 0.0 + } + } + mod.widgets.RoomScreen = #(RoomScreen::register_widget(vm)) { width: Fill, height: Fill, @@ -1868,6 +1918,60 @@ script_mod! { } } + translation_lang_modal := Modal { + align: Align{x: 0, y: 0} + bg_view.draw_bg.color: #00000000 + content +: { + width: Fill + height: Fill + flow: Overlay + align: Align{x: 0, y: 0} + + translation_lang_popup := RoundedView { + width: 220 + height: Fit + margin: Inset{left: 0, top: 0} + padding: Inset{top: 4, bottom: 4} + show_bg: true + new_batch: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + border_size: 1.0 + border_color: #ddd + shadow_color: #0003 + shadow_radius: 8.0 + shadow_offset: vec2(0.0, 2.0) + } + + translation_lang_scroll := ScrollYView { + width: Fill + height: 288 + flow: Down + spacing: 0 + + lang_en := mod.widgets.TranslationLangPopupButton { text: "en English" } + lang_zh := mod.widgets.TranslationLangPopupButton { text: "zh 简体中文" } + lang_zh_tw := mod.widgets.TranslationLangPopupButton { text: "zh-TW 繁體中文" } + lang_ja := mod.widgets.TranslationLangPopupButton { text: "ja 日本語" } + lang_ko := mod.widgets.TranslationLangPopupButton { text: "ko 한국어" } + lang_es := mod.widgets.TranslationLangPopupButton { text: "es Español" } + lang_fr := mod.widgets.TranslationLangPopupButton { text: "fr Français" } + lang_de := mod.widgets.TranslationLangPopupButton { text: "de Deutsch" } + lang_ru := mod.widgets.TranslationLangPopupButton { text: "ru Русский" } + lang_pt := mod.widgets.TranslationLangPopupButton { text: "pt Português" } + lang_ar := mod.widgets.TranslationLangPopupButton { text: "ar العربية" } + lang_vi := mod.widgets.TranslationLangPopupButton { text: "vi Tiếng Việt" } + lang_th := mod.widgets.TranslationLangPopupButton { text: "th ไทย" } + lang_id := mod.widgets.TranslationLangPopupButton { text: "id Bahasa Indonesia" } + lang_ms := mod.widgets.TranslationLangPopupButton { text: "ms Bahasa Melayu" } + lang_tr := mod.widgets.TranslationLangPopupButton { text: "tr Türkçe" } + lang_hi := mod.widgets.TranslationLangPopupButton { text: "hi हिन्दी" } + } + } + } + } + // Note: here, we're within a View that has an Overlay flow, // so the order that we define the below views determines which one is on top. @@ -3675,6 +3779,7 @@ impl Widget for RoomScreen { // Keep all unhandled actions so we can add them back to the global action list below. true }); + self.handle_translation_lang_popup_actions(cx, &actions_generated_within_this_room_screen); // Add back any unhandled actions to the global action list. cx.extend_actions(actions_generated_within_this_room_screen); } @@ -3915,9 +4020,67 @@ impl RoomScreen { self.view .label(cx, ids!(top_space.label)) .set_text(cx, tr_key(self.app_language, "room_screen.top_space.loading_earlier")); + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .set_app_language(cx, self.app_language); + self.sync_translation_lang_popup(cx); self.view.redraw(cx); } + fn sync_translation_lang_popup(&mut self, cx: &mut Cx) { + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_en)) + .set_text(cx, &translation::language_popup_label("en")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_zh)) + .set_text(cx, &translation::language_popup_label("zh")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_zh_tw)) + .set_text(cx, &translation::language_popup_label("zh-TW")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_ja)) + .set_text(cx, &translation::language_popup_label("ja")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_ko)) + .set_text(cx, &translation::language_popup_label("ko")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_es)) + .set_text(cx, &translation::language_popup_label("es")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_fr)) + .set_text(cx, &translation::language_popup_label("fr")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_de)) + .set_text(cx, &translation::language_popup_label("de")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_ru)) + .set_text(cx, &translation::language_popup_label("ru")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_pt)) + .set_text(cx, &translation::language_popup_label("pt")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_ar)) + .set_text(cx, &translation::language_popup_label("ar")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_vi)) + .set_text(cx, &translation::language_popup_label("vi")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_th)) + .set_text(cx, &translation::language_popup_label("th")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_id)) + .set_text(cx, &translation::language_popup_label("id")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_ms)) + .set_text(cx, &translation::language_popup_label("ms")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_tr)) + .set_text(cx, &translation::language_popup_label("tr")); + self.view + .button(cx, ids!(translation_lang_modal.content.translation_lang_popup.translation_lang_scroll.lang_hi)) + .set_text(cx, &translation::language_popup_label("hi")); + } + fn room_id(&self) -> Option<&OwnedRoomId> { self.room_name_id.as_ref().map(|r| r.room_id()) } @@ -5238,6 +5401,9 @@ impl RoomScreen { MessageAction::ShowRoomInfoPane => { self.show_room_info_pane(cx); } + MessageAction::ToggleTranslationLangPopup { button_rect } => { + self.toggle_translation_lang_popup(cx, *button_rect); + } MessageAction::Redact { details, reason } => { let Some(tl) = self.tl_state.as_ref() else { return }; let timeline_event_id = details.timeline_event_id.clone(); @@ -5277,6 +5443,67 @@ impl RoomScreen { } } + fn toggle_translation_lang_popup(&mut self, cx: &mut Cx, button_rect: Rect) { + let translation_lang_modal = self.view.modal(cx, ids!(translation_lang_modal)); + if translation_lang_modal.is_open() { + translation_lang_modal.close(cx); + return; + } + + let room_screen_rect = self.view.area().clipped_rect(cx); + let popup_abs_pos = compute_translation_lang_popup_abs_pos(button_rect, room_screen_rect); + self.sync_translation_lang_popup(cx); + log!( + "Translation popup: button_rect={button_rect:?}, room_screen_rect={room_screen_rect:?}, popup_abs_pos={popup_abs_pos:?}" + ); + if let Some(mut translation_lang_popup) = self + .view + .view(cx, ids!(translation_lang_modal.content.translation_lang_popup)) + .borrow_mut() + { + translation_lang_popup.walk.abs_pos = Some(popup_abs_pos); + translation_lang_popup.walk.margin.left = 0.0; + translation_lang_popup.walk.margin.top = 0.0; + translation_lang_popup.walk.margin.right = 0.0; + translation_lang_popup.walk.margin.bottom = 0.0; + } + translation_lang_modal.open(cx); + } + + fn handle_translation_lang_popup_actions(&mut self, cx: &mut Cx, actions: &Actions) { + let translation_lang_modal = self.view.modal(cx, ids!(translation_lang_modal)); + if !translation_lang_modal.is_open() { + return; + } + + let lang_ids: &[(&str, &[LiveId])] = &[ + ("en", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_en)]), + ("zh", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_zh)]), + ("zh-TW", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_zh_tw)]), + ("ja", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_ja)]), + ("ko", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_ko)]), + ("es", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_es)]), + ("fr", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_fr)]), + ("de", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_de)]), + ("ru", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_ru)]), + ("pt", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_pt)]), + ("ar", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_ar)]), + ("vi", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_vi)]), + ("th", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_th)]), + ("id", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_id)]), + ("ms", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_ms)]), + ("tr", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_tr)]), + ("hi", &[live_id!(translation_lang_modal), live_id!(content), live_id!(translation_lang_popup), live_id!(translation_lang_scroll), live_id!(lang_hi)]), + ]; + for &(code, id_path) in lang_ids { + if self.button(cx, id_path).clicked(actions) { + self.view.room_input_bar(cx, ids!(room_input_bar)).activate_translation_language(cx, code); + translation_lang_modal.close(cx); + break; + } + } + } + /// Jumps to the target event ID in this timeline by smooth scrolling to it. /// /// This function searches backwards from the given `max_tl_idx` in the timeline @@ -8187,6 +8414,9 @@ pub enum MessageAction { /// in which the (0,0) origin coordinate is the top left corner of the app window. abs_pos: DVec2, }, + ToggleTranslationLangPopup { + button_rect: Rect, + }, /// The user requested opening the message action bar ActionBarOpen { /// At the given timeline item index @@ -8697,4 +8927,58 @@ mod tests { assert!(rebuilt.is_empty()); assert!(!should_schedule_frame); } + + #[test] + fn translation_lang_popup_abs_pos_prefers_above_button() { + let button_rect = Rect { + pos: dvec2(48.0, 680.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 0.0), + size: dvec2(1280.0, 760.0), + }; + + let popup_pos = compute_translation_lang_popup_abs_pos(button_rect, container_rect); + + assert!(popup_pos.y < button_rect.pos.y); + assert!(popup_pos.y >= TRANSLATION_LANG_POPUP_MARGIN); + assert!(popup_pos.x >= TRANSLATION_LANG_POPUP_MARGIN); + } + + #[test] + fn translation_lang_popup_abs_pos_falls_below_when_top_space_is_insufficient() { + let button_rect = Rect { + pos: dvec2(48.0, 20.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 0.0), + size: dvec2(1280.0, 760.0), + }; + + let popup_pos = compute_translation_lang_popup_abs_pos(button_rect, container_rect); + + assert!(popup_pos.y > button_rect.pos.y); + assert!(popup_pos.y >= TRANSLATION_LANG_POPUP_MARGIN); + } + + #[test] + fn translation_lang_popup_abs_pos_clamps_to_room_screen_right_edge() { + let button_rect = Rect { + pos: dvec2(1240.0, 680.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 0.0), + size: dvec2(1280.0, 760.0), + }; + + let popup_pos = compute_translation_lang_popup_abs_pos(button_rect, container_rect); + + assert_eq!( + popup_pos.x + TRANSLATION_LANG_POPUP_WIDTH, + container_rect.size.x - TRANSLATION_LANG_POPUP_MARGIN + ); + } } diff --git a/src/i18n.rs b/src/i18n.rs index 7604d7375..4cee09721 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -113,3 +113,32 @@ pub fn language_dropdown_labels(language: AppLanguage) -> Vec { tr(language, I18nKey::LanguageOptionChineseSimplified).to_string(), ] } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn translation_i18n_keys_exist_for_settings_and_room_input() { + assert_eq!( + tr_key(AppLanguage::English, "settings.labs.translation.title"), + "Real-time Translation", + ); + assert_eq!( + tr_key(AppLanguage::ChineseSimplified, "settings.labs.translation.title"), + "实时翻译", + ); + assert_eq!( + tr_key(AppLanguage::English, "room_input_bar.translation.preview.idle"), + "Start typing to translate...", + ); + assert_eq!( + tr_key(AppLanguage::ChineseSimplified, "room_input_bar.translation.preview.idle"), + "开始输入即可翻译...", + ); + assert_eq!( + tr_key(AppLanguage::ChineseSimplified, "room_input_bar.input.placeholder"), + "输入消息(支持 Markdown)...", + ); + } +} diff --git a/src/room/mod.rs b/src/room/mod.rs index 01e4b84aa..7436186ae 100644 --- a/src/room/mod.rs +++ b/src/room/mod.rs @@ -11,6 +11,7 @@ pub mod member_search; pub mod reply_preview; pub mod room_input_bar; pub mod room_display_filter; +pub mod translation; pub mod typing_notice; pub fn script_mod(vm: &mut ScriptVm) { diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 5847c0f56..0e77af8ce 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -20,9 +20,65 @@ 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, OwnedUserId}; -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}, i18n::AppLanguage, 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}; +use crate::{app::AppState, 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}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, 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}; const ROOM_INFO_CARD_MOBILE_BREAKPOINT: f32 = 700.0; +#[cfg(test)] +const TRANSLATION_LANG_POPUP_WIDTH: f64 = 220.0; +#[cfg(test)] +const TRANSLATION_LANG_POPUP_SCROLL_HEIGHT: f64 = 288.0; +#[cfg(test)] +const TRANSLATION_LANG_POPUP_HEIGHT: f64 = TRANSLATION_LANG_POPUP_SCROLL_HEIGHT + 8.0; +#[cfg(test)] +const TRANSLATION_LANG_POPUP_GAP: f64 = 6.0; +#[cfg(test)] +const TRANSLATION_LANG_POPUP_MARGIN: f64 = 8.0; + +#[cfg(test)] +fn compute_translation_popup_abs_pos( + button_rect_local: Rect, + container_rect_screen: Rect, + pass_size: DVec2, +) -> DVec2 { + let max_x = (container_rect_screen.size.x - TRANSLATION_LANG_POPUP_WIDTH - TRANSLATION_LANG_POPUP_MARGIN) + .max(TRANSLATION_LANG_POPUP_MARGIN); + let popup_x = (button_rect_local.pos.x + button_rect_local.size.x - TRANSLATION_LANG_POPUP_WIDTH) + .max(TRANSLATION_LANG_POPUP_MARGIN) + .min(max_x); + + let max_y = (pass_size.y - TRANSLATION_LANG_POPUP_HEIGHT - TRANSLATION_LANG_POPUP_MARGIN) + .max(TRANSLATION_LANG_POPUP_MARGIN); + let popup_y_above = button_rect_local.pos.y - TRANSLATION_LANG_POPUP_HEIGHT - TRANSLATION_LANG_POPUP_GAP; + let button_screen_y = container_rect_screen.pos.y + button_rect_local.pos.y; + let popup_y = if button_screen_y >= TRANSLATION_LANG_POPUP_HEIGHT + TRANSLATION_LANG_POPUP_GAP + TRANSLATION_LANG_POPUP_MARGIN { + popup_y_above + } else { + let popup_y_screen = (button_screen_y + button_rect_local.size.y + TRANSLATION_LANG_POPUP_GAP) + .max(TRANSLATION_LANG_POPUP_MARGIN) + .min(max_y); + popup_y_screen - container_rect_screen.pos.y + }; + + dvec2(popup_x, popup_y) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TranslationApplyOutcome { + input_text: String, + preserved_preview_text: String, + next_last_source: String, + keep_preview_visible: bool, +} + +fn compute_translation_apply_outcome(translated_text: &str) -> TranslationApplyOutcome { + let applied_text = translated_text.to_string(); + TranslationApplyOutcome { + input_text: applied_text.clone(), + preserved_preview_text: applied_text.clone(), + next_last_source: applied_text, + keep_preview_visible: true, + } +} script_mod! { use mod.prelude.widgets.* @@ -32,6 +88,64 @@ script_mod! { mod.widgets.ICO_LOCATION_PERSON = crate_resource("self://resources/icons/location-person.svg") mod.widgets.ICO_MENU = crate_resource("self://resources/icons/menu.svg") mod.widgets.ICO_THREADS = crate_resource("self://resources/icons/double_chat.svg") + mod.widgets.ICO_TRANSLATE = crate_resource("self://resources/icons/translate.svg") + + mod.widgets.TranslationLangItem = View { + width: Fill, height: 36 + flow: Right + align: Align{y: 0.5} + padding: Inset{left: 12, right: 12} + cursor: MouseCursor.Hand + show_bg: true + draw_bg +: { + color: #0000 + hover: instance(0.0) + pixel: fn() { + return mix(self.color, #xF0F4FA, self.hover) + } + } + animator: Animator { + hover: { + default: @off + off: AnimatorState { + from: {all: Forward {duration: 0.15}} + apply: { draw_bg: { hover: 0.0 } } + } + on: AnimatorState { + from: {all: Forward {duration: 0.15}} + apply: { draw_bg: { hover: 1.0 } } + } + } + } + + RoundedView { + width: Fit, height: Fit + padding: Inset{left: 5, right: 5, top: 2, bottom: 2} + margin: Inset{right: 10} + show_bg: true + draw_bg +: { + color: #xE8EEF8 + border_radius: 3.0 + } + lang_code := Label { + width: Fit, height: Fit + draw_text +: { + color: #x555555 + text_style: REGULAR_TEXT { font_size: 9 } + } + text: "en" + } + } + + lang_name := Label { + width: Fill, height: Fit + draw_text +: { + color: #x333333 + text_style: REGULAR_TEXT { font_size: 11 } + } + text: "English" + } + } mod.widgets.RoomEmojiButton = mod.widgets.RobrixIconButton { spacing: 0 @@ -61,6 +175,8 @@ script_mod! { width: Fill, height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} flow: Down, + clip_x: false, + clip_y: false, // These margins are a hack to make the borders of the RoomInputBar // line up with the boundaries of its parent widgets. @@ -87,6 +203,82 @@ script_mod! { // Upload progress view (shown when a file upload is in progress) upload_progress_view := UploadProgressView { } + // Translation preview: shows the translated text above the input bar. + translation_preview := RoundedView { + visible: false + width: Fill, height: Fit + flow: Right + padding: Inset{left: 12, right: 8, top: 8, bottom: 8} + align: Align{y: 0.5} + spacing: 8 + show_bg: true + draw_bg +: { + color: #xF0F4FA + border_radius: 4.0 + } + + translation_lang_badge := RoundedView { + width: Fit, height: Fit + padding: Inset{left: 6, right: 6, top: 2, bottom: 2} + show_bg: true + draw_bg +: { + color: #xE0E8F0 + border_radius: 3.0 + } + translation_lang_code := Label { + width: Fit, height: Fit + draw_text +: { + color: #x555555 + text_style: REGULAR_TEXT { font_size: 9 } + } + text: "en" + } + } + + translation_preview_text := Label { + width: Fill, height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: #x333333 + text_style: REGULAR_TEXT { font_size: 11 } + } + text: "" + } + + translation_apply_button := Button { + width: Fit, height: Fit + padding: Inset{top: 4, bottom: 4, left: 10, right: 10} + text: "Apply" + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + color_hover: (COLOR_ACTIVE_PRIMARY_DARKER) + color_down: #0C5DAA + border_radius: 4.0 + } + draw_text +: { + color: #fff + text_style: REGULAR_TEXT { font_size: 10 } + } + } + + translation_close_button := RobrixIconButton { + width: Fit, height: Fit + padding: 4 + spacing: 0 + draw_icon +: { + svg: (ICON_CLOSE) + color: #x999999 + } + draw_bg +: { + color: #0000 + color_hover: #xE0E0E0 + color_down: #xD0D0D0 + } + icon_walk: Walk{width: 12, height: 12} + text: "" + } + } + // 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. @@ -262,6 +454,22 @@ script_mod! { text: "", } + translate_button := RobrixIconButton { + margin: Inset{left: 1, right: 1, top: 4, bottom: 4} + spacing: 0, + draw_icon +: { + svg: (mod.widgets.ICO_TRANSLATE) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #xE0E8F0 + color_down: #xD0D8E8 + } + icon_walk: Walk{width: 19, height: 19} + text: "", + } + mentionable_text_input := MentionableTextInput { width: Fill, height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} @@ -332,6 +540,48 @@ script_mod! { editing_pane := EditingPane { } } + + translation_lang_wrapper := RoundedView { + visible: false + width: 220, height: Fit + flow: Down + padding: Inset{top: 4, bottom: 4} + show_bg: true + new_batch: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + border_size: 1.0 + border_color: #ddd + shadow_color: #0003 + shadow_radius: 8.0 + shadow_offset: vec2(0.0, 2.0) + } + + translation_lang_scroll := ScrollYView { + width: Fill, height: 288 + flow: Down + spacing: 0 + + lang_en := mod.widgets.TranslationLangItem {} + lang_zh := mod.widgets.TranslationLangItem {} + lang_zh_tw := mod.widgets.TranslationLangItem {} + lang_ja := mod.widgets.TranslationLangItem {} + lang_ko := mod.widgets.TranslationLangItem {} + lang_es := mod.widgets.TranslationLangItem {} + lang_fr := mod.widgets.TranslationLangItem {} + lang_de := mod.widgets.TranslationLangItem {} + lang_ru := mod.widgets.TranslationLangItem {} + lang_pt := mod.widgets.TranslationLangItem {} + lang_ar := mod.widgets.TranslationLangItem {} + lang_vi := mod.widgets.TranslationLangItem {} + lang_th := mod.widgets.TranslationLangItem {} + lang_id := mod.widgets.TranslationLangItem {} + lang_ms := mod.widgets.TranslationLangItem {} + lang_tr := mod.widgets.TranslationLangItem {} + lang_hi := mod.widgets.TranslationLangItem {} + } + } } } @@ -340,6 +590,8 @@ script_mod! { pub struct RoomInputBar { #[source] source: ScriptObjectRef, #[deref] view: View, + #[rust] app_language: AppLanguage, + #[rust] app_language_initialized: bool, /// Whether the `ReplyingPreview` was visible when the `EditingPane` was shown. /// If true, when the `EditingPane` gets hidden, we need to re-show the `ReplyingPreview`. @@ -358,10 +610,35 @@ pub struct RoomInputBar { /// 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, + + // --- Translation state --- + /// Whether real-time translation is currently active. + #[rust] translation_active: bool, + /// The target language code (e.g., "en", "zh", "ja"). + #[rust] translation_target_code: String, + /// The most recent translation result. + #[rust] translation_preview_text: Option, + /// Whether a translation HTTP request is currently in flight. + #[rust] translation_request_pending: bool, + /// Debounce timer for translation requests. + #[rust] translation_debounce_timer: Timer, + /// The last source text that was sent for translation. + #[rust] translation_last_source: String, + /// Whether the language selector popup is visible. + #[rust] is_lang_popup_visible: bool, + /// Cached translation config, updated from AppState when translation is activated. + #[rust] translation_config: Option, } impl Widget for RoomInputBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + let room_screen_props = scope .props .get::() @@ -388,30 +665,29 @@ impl Widget for RoomInputBar { _ => {} } + // Always read the latest translation config from global state. + // Settings may update it at any time via set_global_config(). + self.translation_config = translation::get_global_config(); + if let Event::Actions(actions) = event { self.handle_actions(cx, actions, room_screen_props); } - // Handle signal events for pending file loads from background threads + // 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::Empty) => {} Err(std::sync::mpsc::TryRecvError::Disconnected) => { - // Channel disconnected remove_receiver = true; } } @@ -422,10 +698,82 @@ impl Widget for RoomInputBar { } } + // Handle translation debounce timer firing. + if let Event::Timer(te) = event { + if self.translation_debounce_timer.is_timer(te).is_some() && self.translation_active { + log!("Translation: debounce timer fired, translation_active={}", self.translation_active); + let mentionable_text_input = self.mentionable_text_input(cx, ids!(mentionable_text_input)); + let source_text = mentionable_text_input.text().trim().to_string(); + log!("Translation: source_text='{}', last_source='{}'", source_text, self.translation_last_source); + if !source_text.is_empty() && source_text != self.translation_last_source { + self.translation_last_source = source_text.clone(); + self.translation_request_pending = true; + + self.view + .label(cx, ids!(translation_preview_text)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.translation.preview.loading")); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, true); + self.redraw(cx); + + log!("Translation: config cached={}, target='{}'", self.translation_config.is_some(), self.translation_target_code); + if let Some(config) = &self.translation_config { + log!("Translation: config enabled={}, api_url='{}', model='{}'", config.enabled, config.api_base_url, config.model); + if config.is_configured() { + let target_code = self.translation_target_code.clone(); + log!("Translation: sending request for '{}' -> '{}'", source_text, target_code); + translation::send_translation_request( + cx, + config, + &source_text, + &target_code, + ); + } else { + log!("Translation: config not properly configured"); + } + } else { + log!("Translation: no cached config!"); + } + } + } + } + + // Handle translation HTTP response. + if let Event::NetworkResponses(responses) = event { + for response in responses { + if let NetworkResponse::HttpResponse { request_id, response } = response { + if *request_id == TRANSLATION_REQUEST_ID { + self.translation_request_pending = false; + match translation::parse_translation_response(response) { + Ok(translated_text) => { + self.translation_preview_text = Some(translated_text.clone()); + self.view.label(cx, ids!(translation_preview_text)).set_text(cx, &translated_text); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, true); + } + Err(e) => { + log!("Translation error: {e}"); + self.view.label(cx, ids!(translation_preview_text)).set_text( + cx, + &tr_fmt(self.app_language, "room_input_bar.translation.preview.error", &[("error", &e)]), + ); + } + } + self.redraw(cx); + } + } + } + } + self.view.handle_event(cx, event, scope); } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + // Shrink the input_bar's height as the editing pane slides in, // and grow it back as the editing pane slides out. // slide=1.0 → editing pane hidden → input_bar at full Fit height. @@ -457,11 +805,56 @@ impl Widget for RoomInputBar { let width = self.view.area().rect(cx).size.x as f32; let show_room_info_card = !(width > 1.0 && width < ROOM_INFO_CARD_MOBILE_BREAKPOINT); self.button(cx, ids!(room_info_card_button)).set_visible(cx, show_room_info_card); + self.view.draw_walk(cx, scope, walk) } } impl RoomInputBar { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)) + .set_empty_text(cx, tr_key(self.app_language, "room_input_bar.input.placeholder").to_string()); + self.button(cx, ids!(translation_apply_button)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.translation.preview.apply")); + if self.translation_active { + if self.translation_request_pending { + self.view + .label(cx, ids!(translation_preview_text)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.translation.preview.loading")); + } else if self.translation_preview_text.is_none() { + self.view + .label(cx, ids!(translation_preview_text)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.translation.preview.idle")); + } + } + self.view.redraw(cx); + } + + /// Handles a language being selected from the popup. + fn on_language_selected(&mut self, cx: &mut Cx, code: &str) { + self.translation_target_code = code.to_string(); + self.translation_active = true; + self.is_lang_popup_visible = false; + self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); + + // Show the language code in the preview badge + self.view.label(cx, ids!(translation_lang_code)).set_text(cx, code); + self.view + .label(cx, ids!(translation_preview_text)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.translation.preview.idle")); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, true); + + // Focus the text input + self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); + self.redraw(cx); + } + fn resolve_target_user_id( &mut self, explicit_target_user_id: Option, @@ -552,6 +945,58 @@ impl RoomInputBar { self.redraw(cx); } + // Handle the translate button being clicked — toggle language selector popup. + if self.button(cx, ids!(translate_button)).clicked(actions) { + if self.translation_active { + // Turn off translation + self.translation_active = false; + self.translation_preview_text = None; + self.translation_request_pending = false; + self.translation_last_source.clear(); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); + self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); + self.is_lang_popup_visible = false; + self.redraw(cx); + } else { + self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); + self.is_lang_popup_visible = false; + let button_rect = self.button(cx, ids!(translate_button)).area().clipped_rect(cx); + if button_rect.size.x > 0.0 { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::ToggleTranslationLangPopup { button_rect }, + ); + } + } + } + + // Handle "Apply" button on translation preview — replace input text with translation. + if self.button(cx, ids!(translation_apply_button)).clicked(actions) { + if let Some(translated) = self.translation_preview_text.clone() { + let outcome = compute_translation_apply_outcome(&translated); + mentionable_text_input.set_text(cx, &outcome.input_text); + self.enable_send_message_button(cx, !outcome.input_text.trim().is_empty()); + self.translation_preview_text = Some(outcome.preserved_preview_text.clone()); + self.translation_last_source = outcome.next_last_source; + self.view.label(cx, ids!(translation_preview_text)).set_text(cx, &outcome.preserved_preview_text); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, outcome.keep_preview_visible); + self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); + self.redraw(cx); + } + } + + // Handle close button on translation preview. + if self.button(cx, ids!(translation_close_button)).clicked(actions) { + self.translation_active = false; + self.translation_preview_text = None; + self.translation_request_pending = false; + self.translation_last_source.clear(); + self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); + self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); + self.is_lang_popup_visible = false; + self.redraw(cx); + } + // Handle the location card being clicked. if self.button(cx, ids!(location_card_button)).clicked(actions) { log!("Location card clicked; requesting current location..."); @@ -707,6 +1152,18 @@ impl RoomInputBar { room_id: room_screen_props.timeline_kind.room_id().clone(), typing: !is_empty, }); + + // Trigger translation debounce if translation mode is active. + if self.translation_active { + let trimmed = new_text.trim().to_string(); + log!("Translation: text changed, trimmed='{}', last_source='{}'", trimmed, self.translation_last_source); + if !trimmed.is_empty() && trimmed != self.translation_last_source { + cx.stop_timer(self.translation_debounce_timer); + self.translation_debounce_timer = cx.start_timeout(0.5); + log!("Translation: debounce timer started"); + } + } + is_empty } else { text_input.text().is_empty() @@ -767,7 +1224,7 @@ impl RoomInputBar { populate_preview_of_timeline_item( cx, &replying_preview.html_or_plaintext(cx, ids!(reply_preview_content.reply_preview_body)), - AppLanguage::default(), + self.app_language, replying_to.0.content(), replying_to.0.sender(), &replying_preview_username, @@ -1039,6 +1496,16 @@ impl RoomInputBar { } impl RoomInputBarRef { + pub fn activate_translation_language(&self, cx: &mut Cx, code: &str) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.on_language_selected(cx, code); + } + + pub fn set_app_language(&self, cx: &mut Cx, app_language: AppLanguage) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.set_app_language(cx, app_language); + } + /// Shows a preview of the given event that the user is currently replying to /// above the message input bar. pub fn show_replying_to( @@ -1153,6 +1620,8 @@ impl RoomInputBarRef { inner.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); inner.is_emoji_picker_expanded = false; inner.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); + inner.is_lang_popup_visible = false; + inner.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); // 2. Restore the state of the replying-to preview. if let Some(replying_to) = replying_to { @@ -1307,3 +1776,89 @@ enum ShowEditingPaneBehavior { editing_pane_state: EditingPaneState, }, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn translation_popup_position_prefers_above_button() { + let button_rect = Rect { + pos: dvec2(40.0, 4.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 640.0), + size: dvec2(1280.0, 64.0), + }; + let pass_size = dvec2(1280.0, 800.0); + + let pos = compute_translation_popup_abs_pos(button_rect, container_rect, pass_size); + + assert!(pos.y < button_rect.pos.y); + assert!(pos.x >= 8.0); + assert!(pos.x + 220.0 <= container_rect.size.x - 8.0); + } + + #[test] + fn translation_popup_position_falls_below_when_not_enough_space_above() { + let button_rect = Rect { + pos: dvec2(40.0, 4.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 10.0), + size: dvec2(1280.0, 64.0), + }; + let pass_size = dvec2(1280.0, 800.0); + + let pos = compute_translation_popup_abs_pos(button_rect, container_rect, pass_size); + + assert!(pos.y > button_rect.pos.y); + } + + #[test] + fn translation_popup_position_clamps_to_right_edge() { + let button_rect = Rect { + pos: dvec2(1260.0, 4.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 640.0), + size: dvec2(1280.0, 64.0), + }; + let pass_size = dvec2(1280.0, 800.0); + + let pos = compute_translation_popup_abs_pos(button_rect, container_rect, pass_size); + + assert_eq!(pos.x + 220.0, container_rect.size.x - 8.0); + } + + #[test] + fn translation_popup_position_can_resolve_to_negative_local_y() { + let button_rect = Rect { + pos: dvec2(40.0, 4.0), + size: dvec2(32.0, 32.0), + }; + let container_rect = Rect { + pos: dvec2(0.0, 640.0), + size: dvec2(1280.0, 64.0), + }; + let pass_size = dvec2(1280.0, 800.0); + + let popup_pos = compute_translation_popup_abs_pos(button_rect, container_rect, pass_size); + + assert!(popup_pos.y < button_rect.pos.y); + assert!(popup_pos.y < 0.0); + } + + #[test] + fn translation_apply_keeps_session_open() { + let outcome = compute_translation_apply_outcome("Hola mundo"); + + assert_eq!(outcome.input_text, "Hola mundo"); + assert_eq!(outcome.preserved_preview_text, "Hola mundo"); + assert_eq!(outcome.next_last_source, "Hola mundo"); + assert!(outcome.keep_preview_visible); + } +} diff --git a/src/room/translation.rs b/src/room/translation.rs new file mode 100644 index 000000000..646c5c428 --- /dev/null +++ b/src/room/translation.rs @@ -0,0 +1,215 @@ +use std::sync::Mutex; +use makepad_widgets::*; +use serde::{Deserialize, Serialize}; + +pub const TRANSLATION_REQUEST_ID: LiveId = live_id!(translation_request); + +/// Supported target languages for translation. +pub const SUPPORTED_LANGUAGES: &[(&str, &str)] = &[ + ("en", "English"), + ("zh", "简体中文"), + ("zh-TW", "繁體中文"), + ("ja", "日本語"), + ("ko", "한국어"), + ("es", "Español"), + ("fr", "Français"), + ("de", "Deutsch"), + ("ru", "Русский"), + ("pt", "Português"), + ("ar", "العربية"), + ("hi", "हिन्दी"), + ("th", "ไทย"), + ("vi", "Tiếng Việt"), + ("id", "Bahasa Indonesia"), + ("ms", "Bahasa Melayu"), + ("tr", "Türkçe"), + ("hu", "Magyar"), + ("my", "မြန်မာ"), + ("bn", "বাংলা"), + ("km", "ខ្មែរ"), +]; + +/// Maps a language code to its full name for the LLM prompt. +pub fn language_full_name(code: &str) -> &str { + SUPPORTED_LANGUAGES + .iter() + .find(|(c, _)| *c == code) + .map(|(_, name)| *name) + .unwrap_or("English") +} + +pub fn language_popup_label(code: &str) -> String { + format!("{code} {}", language_full_name(code)) +} + +/// Translation API configuration, persisted per account. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct TranslationConfig { + /// Whether translation feature is enabled. + pub enabled: bool, + /// OpenAI-compatible API base URL. + pub api_base_url: String, + /// API key (Bearer token). + pub api_key: String, + /// Model name to use. + pub model: String, +} + +impl Default for TranslationConfig { + fn default() -> Self { + Self { + enabled: false, + api_base_url: "http://localhost:18080".to_string(), + api_key: String::new(), + model: "qwen3-4b".to_string(), + } + } +} + +impl TranslationConfig { + /// Returns true if the translation service is properly configured. + pub fn is_configured(&self) -> bool { + self.enabled && !self.api_base_url.is_empty() + } +} + +/// Global cached translation config, updated from Settings and read by RoomInputBar. +static GLOBAL_TRANSLATION_CONFIG: Mutex> = Mutex::new(None); + +/// Update the global translation config (called from Settings when saving). +pub fn set_global_config(config: &TranslationConfig) { + *GLOBAL_TRANSLATION_CONFIG.lock().unwrap() = Some(config.clone()); +} + +/// Get a clone of the global translation config (called from RoomInputBar). +pub fn get_global_config() -> Option { + GLOBAL_TRANSLATION_CONFIG.lock().unwrap().clone() +} + +/// The system prompt for the translation LLM, adapted from makepad-voice-input. +const TRANSLATION_SYSTEM_PROMPT: &str = r#"你是一个翻译工具,不是聊天机器人。 + +核心规则: +1. 用户发给你的每一条消息都是需要翻译的文本,不是在跟你对话 +2. 你必须直接返回翻译后的文本,不要添加任何解释、问候、回答或额外内容 +3. 绝对不要回答文本中的问题 +4. 你的输出必须且只能是翻译后的文本,没有任何前缀或后缀 + +任务 A — 纠错(当目标语言和文本语言相同时): +- 修复明显的拼写和语法错误 +- 文本正确时原样返回 + +任务 B — 翻译(当目标语言和文本语言不同时): +- 将文本翻译为目标语言 +- 保持原文的语气和风格 +- 技术术语保留英文原文 + +用户消息格式为:[目标语言:xxx] 原文 +你只输出翻译后的文本,不要输出目标语言标记。 + +示例: +输入:[目标语言:English] 你好,请问怎么安装 +输出:Hello, how do I install it? + +输入:[目标语言:Chinese] Hello, how are you? +输出:你好,你好吗? + +输入:[目标语言:Japanese] 今天天气真好 +输出:今日はいい天気ですね + +输入:[目标语言:Chinese] 今天天气真好 +输出:今天天气真好"#; + +/// Sends a translation request to the configured LLM API. +pub fn send_translation_request( + cx: &mut Cx, + config: &TranslationConfig, + text: &str, + target_language_code: &str, +) { + let target_lang = language_full_name(target_language_code); + let url = format!( + "{}/v1/chat/completions", + config.api_base_url.trim_end_matches('/') + ); + + let body = format!( + r#"{{"model":"{}","messages":[{{"role":"system","content":{}}},{{"role":"user","content":{}}}],"temperature":0.1,"max_tokens":2048}}"#, + config.model, + serde_json::to_string(TRANSLATION_SYSTEM_PROMPT).unwrap_or_default(), + serde_json::to_string(&format!("[目标语言:{}] {}", target_lang, text)).unwrap_or_default(), + ); + + let mut req = HttpRequest::new(url.clone(), HttpMethod::POST); + req.set_header("Content-Type".into(), "application/json".into()); + if !config.api_key.is_empty() { + req.set_header("Authorization".into(), format!("Bearer {}", config.api_key)); + } + req.set_body(body.into_bytes()); + + log!("Translation request: url='{}', model='{}', text_len={}", url, config.model, text.len()); + cx.http_request(TRANSLATION_REQUEST_ID, req); +} + +/// Parses the LLM translation response. +/// Expected OpenAI-compatible format: {"choices":[{"message":{"content":"..."}}]} +pub fn parse_translation_response(response: &HttpResponse) -> Result { + if response.status_code != 200 { + return Err(format!("HTTP {}", response.status_code)); + } + + let body_str = response + .body_string() + .ok_or_else(|| "Empty response body".to_string())?; + + // Extract content from the first choice + if let Some(content_start) = body_str.find("\"content\"") { + let after_key = &body_str[content_start + 9..]; + let after_colon = after_key + .trim_start() + .strip_prefix(':') + .unwrap_or(after_key) + .trim_start(); + + if let Some(stripped) = after_colon.strip_prefix('"') { + let mut result = String::new(); + let mut chars = stripped.chars(); + while let Some(ch) = chars.next() { + if ch == '\\' { + if let Some(escaped) = chars.next() { + match escaped { + 'n' => result.push('\n'), + 't' => result.push('\t'), + '"' => result.push('"'), + '\\' => result.push('\\'), + _ => { + result.push('\\'); + result.push(escaped); + } + } + } + } else if ch == '"' { + break; + } else { + result.push(ch); + } + } + return Ok(result); + } + } + + Err(format!("Unexpected LLM response format: {body_str}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn language_popup_label_uses_native_language_names() { + assert_eq!(language_popup_label("zh"), "zh 简体中文"); + assert_eq!(language_popup_label("en"), "en English"); + assert_eq!(language_popup_label("unknown"), "unknown English"); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 3155e1186..d474feaf4 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -3,9 +3,11 @@ use makepad_widgets::ScriptVm; pub mod settings_screen; pub mod account_settings; pub mod bot_settings; +pub mod translation_settings; pub fn script_mod(vm: &mut ScriptVm) { account_settings::script_mod(vm); bot_settings::script_mod(vm); + translation_settings::script_mod(vm); settings_screen::script_mod(vm); } diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 13c59b04e..3229897fc 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,12 +1,14 @@ use makepad_widgets::*; -use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; +use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; script_mod! { use mod.prelude.widgets.* use mod.widgets.* + // No custom DropDown types — inline override on the instance instead. + // Custom set_type_default() on DropDownFlat fails to render in this Makepad version. // The main, top-level settings screen widget. mod.widgets.SettingsScreen = #(SettingsScreen::register_widget(vm)) { @@ -109,10 +111,77 @@ script_mod! { text: "Application language" } - language_dropdown := DropDownFlat { - width: 165 - height: 40 + language_dropdown := DropDown { + width: 200 margin: Inset{left: 5, top: 2, bottom: 2} + // draw_text.color is plain (NOT uniform) in DropDownFlat + draw_text +: { + color: #x333333 + color_hover: uniform(#x333333) + color_focus: uniform(#x333333) + color_down: uniform(#x333333) + text_style: REGULAR_TEXT { font_size: 11 } + } + // draw_bg colors are all uniform() in DropDownFlat + draw_bg +: { + color: uniform(#xF6FAFF) + color_hover: uniform(#xF0F6FF) + color_focus: uniform(#xFFFFFF) + color_down: uniform(#xEAF2FF) + color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + color_2_focus: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + color_2_down: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color: uniform(#xC8D9F2) + border_color_hover: uniform(#x74A7EE) + border_color_focus: uniform(#x1B6EF3) + border_color_down: uniform(#x1B6EF3) + border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color_2_focus: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color_2_down: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + arrow_color: uniform(#x555555) + arrow_color_hover: uniform(#x1B6EF3) + arrow_color_focus: uniform(#x1B6EF3) + arrow_color_down: uniform(#x1B6EF3) + border_size: uniform(1.5) + border_radius: uniform(6.0) + } + popup_menu +: { + draw_bg +: { + color: uniform(#xFDFEFF) + color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color: uniform(#xD3E1F6) + border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_size: uniform(1.0) + border_radius: uniform(8.0) + color_dither: uniform(0.0) + } + // menu_item.draw_text.color is plain; others are uniform + menu_item +: { + draw_text +: { + color: #x333333 + color_hover: uniform(#x333333) + color_active: uniform(#x1B6EF3) + } + draw_bg +: { + color: uniform(#x00000000) + color_hover: uniform(#xF0F4FA) + color_active: uniform(#xDCEBFF) + color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + color_2_active: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color: uniform(#x00000000) + border_color_hover: uniform(#x00000000) + border_color_active: uniform(#x00000000) + border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + border_color_2_active: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) + mark_color: uniform(#x00000000) + mark_color_active: uniform(#x1B6EF3) + } + } + } labels: ["English", "Simplified Chinese"] } @@ -137,6 +206,10 @@ script_mod! { LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + translation_settings := TranslationSettings {} + + LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + // The TSP wallet settings section. tsp_settings_screen := TspSettingsScreen {} } @@ -330,6 +403,9 @@ impl SettingsScreen { self.view .bot_settings(cx, ids!(bot_settings)) .set_app_language(cx, self.app_language); + self.view + .translation_settings(cx, ids!(translation_settings)) + .set_app_language(cx, self.app_language); self.view.redraw(cx); } @@ -374,13 +450,14 @@ impl SettingsScreen { } /// Fetches the current user's profile and uses it to populate the settings screen. - pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, app_language: AppLanguage) { + pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, translation_config: &crate::room::translation::TranslationConfig, app_language: AppLanguage) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { error!("Failed to get own profile for settings screen."); return; }; self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); self.view.bot_settings(cx, ids!(bot_settings)).populate(cx, bot_settings); + self.view.translation_settings(cx, ids!(translation_settings)).populate(cx, translation_config); self.set_app_language(cx, app_language); self.set_selected_category(cx, SettingsCategory::Account); self.view.button(cx, ids!(close_button)).reset_hover(cx); @@ -391,9 +468,9 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. - pub fn populate(&self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, app_language: AppLanguage) { + pub fn populate(&self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, translation_config: &crate::room::translation::TranslationConfig, app_language: AppLanguage) { let Some(mut inner) = self.borrow_mut() else { return; }; - inner.populate(cx, own_profile, bot_settings, app_language); + inner.populate(cx, own_profile, bot_settings, translation_config, app_language); } } diff --git a/src/settings/translation_settings.rs b/src/settings/translation_settings.rs new file mode 100644 index 000000000..fa74dfb80 --- /dev/null +++ b/src/settings/translation_settings.rs @@ -0,0 +1,434 @@ +use makepad_widgets::*; + +use crate::{ + app::AppState, + i18n::{AppLanguage, tr_fmt, tr_key}, + persistence, + room::translation::{self, TranslationConfig}, + sliding_sync::current_user_id, +}; + +const TEST_TRANSLATION_REQUEST_ID: LiveId = live_id!(test_translation); + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.TranslationSettings = #(TranslationSettings::register_widget(vm)) { + width: Fill + height: Fit + flow: Down + spacing: 10 + + translation_header := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 1.0} + spacing: 8 + margin: Inset{left: 5, right: 8, bottom: 2} + + translation_title := TitleLabel { + width: Fit + text: "Real-time Translation" + } + + description := Label { + width: Fill + height: Fit + margin: 0 + draw_text +: { + color: #x7A7A7A + text_style: REGULAR_TEXT { font_size: 9.5 } + } + text: "Configure an OpenAI-compatible API for real-time message translation in the input bar." + } + } + + toggle_row := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 0.0, y: 0.5} + spacing: 8 + margin: Inset{left: 5, bottom: 2} + + translation_switch := Toggle { + width: Fit + height: Fit + padding: Inset{top: 8, right: 8, bottom: 8, left: 8} + text: "" + active: false + draw_bg +: { + size: 20.0 + } + } + + switch_state_label := Label { + width: Fit + height: Fit + draw_text +: { + color: #999 + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Disabled" + } + } + + config_section := View { + visible: false + width: Fill + height: Fit + flow: Down + spacing: 8 + margin: Inset{left: 5, right: 8} + + View { + width: Fill, height: Fit + flow: Down, spacing: 4 + api_url_label := Label { + width: Fit, height: Fit + draw_text +: { + color: #555 + text_style: REGULAR_TEXT { font_size: 10 } + } + text: "API URL" + } + api_url_input := RobrixTextInput { + width: Fill, height: Fit + padding: 8 + empty_text: "http://localhost:18080" + } + } + + View { + width: Fill, height: Fit + flow: Down, spacing: 4 + api_key_label := Label { + width: Fit, height: Fit + draw_text +: { + color: #555 + text_style: REGULAR_TEXT { font_size: 10 } + } + text: "API Key" + } + api_key_input := RobrixTextInput { + width: Fill, height: Fit + padding: 8 + empty_text: "sk-..." + is_password: true + } + } + + View { + width: Fill, height: Fit + flow: Down, spacing: 4 + model_label := Label { + width: Fit, height: Fit + draw_text +: { + color: #555 + text_style: REGULAR_TEXT { font_size: 10 } + } + text: "Model" + } + model_input := RobrixTextInput { + width: Fill, height: Fit + padding: 8 + empty_text: "qwen3-4b" + } + } + + View { + width: Fill, height: Fit + flow: Right + spacing: 8 + margin: Inset{top: 4} + + save_button := RobrixIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + icon_walk: Walk{width: 0, height: 0} + spacing: 0 + text: "Save" + } + + test_button := RobrixNeutralIconButton { + padding: Inset{top: 8, bottom: 8, left: 16, right: 16} + icon_walk: Walk{width: 0, height: 0} + spacing: 0 + text: "Test Connection" + } + + test_result_label := Label { + width: Fit, height: Fit + margin: Inset{left: 8} + align: Align{y: 0.5} + draw_text +: { + color: #x999999 + text_style: REGULAR_TEXT { font_size: 10 } + } + text: "" + } + } + } + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct TranslationSettings { + #[deref] + view: View, + #[rust] + app_language: AppLanguage, + #[rust] + app_language_initialized: bool, +} + +impl Widget for TranslationSettings { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + + // Handle test connection HTTP response + if let Event::NetworkResponses(responses) = event { + for response in responses { + if let NetworkResponse::HttpResponse { request_id, response } = response { + if *request_id == TEST_TRANSLATION_REQUEST_ID { + log!("Test translation response: status={}, body={:?}", + response.status_code, + response.body_string().unwrap_or_default().chars().take(200).collect::()); + let label = self.view.label(cx, ids!(test_result_label)); + match translation::parse_translation_response(response) { + Ok(result) => { + let mut lbl = label; + script_apply_eval!(cx, lbl, { + draw_text +: { color: #x00AA00 } + }); + lbl.set_text(cx, &tr_fmt(self.app_language, "settings.labs.translation.test.ok", &[("result", &result)])); + } + Err(e) => { + let mut lbl = label; + script_apply_eval!(cx, lbl, { + draw_text +: { color: #xCC0000 } + }); + lbl.set_text(cx, &tr_fmt(self.app_language, "settings.labs.translation.test.failed", &[("error", &e)])); + } + } + self.view.redraw(cx); + } + } + if let NetworkResponse::HttpError { request_id, error } = response { + if *request_id == TEST_TRANSLATION_REQUEST_ID { + let mut label = self.view.label(cx, ids!(test_result_label)); + script_apply_eval!(cx, label, { + draw_text +: { color: #xCC0000 } + }); + label.set_text(cx, &tr_fmt(self.app_language, "settings.labs.translation.test.error", &[("error", &error.message)])); + self.view.redraw(cx); + } + } + } + } + + self.view.handle_event(cx, event, scope); + self.widget_match_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let app_language = scope.data.get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default(); + if !self.app_language_initialized || self.app_language != app_language { + self.set_app_language(cx, app_language); + } + self.view.draw_walk(cx, scope, walk) + } +} + +impl WidgetMatchEvent for TranslationSettings { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + let translation_switch = self.view.check_box(cx, ids!(translation_switch)); + + let Some(app_state) = scope.data.get_mut::() else { + return; + }; + + if let Some(enabled) = translation_switch.changed(actions) { + app_state.translation.enabled = enabled; + self.sync_ui(cx, &app_state.translation); + translation::set_global_config(&app_state.translation); + persist_translation_config(app_state); + self.view.redraw(cx); + } + + if self.view.button(cx, ids!(save_button)).clicked(actions) { + let api_url = self.view.text_input(cx, ids!(api_url_input)).text().trim().to_string(); + let api_key = self.view.text_input(cx, ids!(api_key_input)).text().trim().to_string(); + let model = self.view.text_input(cx, ids!(model_input)).text().trim().to_string(); + + if !api_url.is_empty() { + app_state.translation.api_base_url = api_url; + } + app_state.translation.api_key = api_key; + if !model.is_empty() { + app_state.translation.model = model; + } + translation::set_global_config(&app_state.translation); + persist_translation_config(app_state); + self.view.redraw(cx); + } + + // Test connection button: send a simple translation request to validate API + if self.view.button(cx, ids!(test_button)).clicked(actions) { + let api_url = self.view.text_input(cx, ids!(api_url_input)).text().trim().to_string(); + let api_key = self.view.text_input(cx, ids!(api_key_input)).text().trim().to_string(); + let model = self.view.text_input(cx, ids!(model_input)).text().trim().to_string(); + + if api_url.is_empty() { + self.view + .label(cx, ids!(test_result_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.validation.api_url_empty")); + self.view.redraw(cx); + return; + } + + // Show testing status + let mut label = self.view.label(cx, ids!(test_result_label)); + script_apply_eval!(cx, label, { + draw_text +: { color: #x999999 } + }); + label.set_text(cx, tr_key(self.app_language, "settings.labs.translation.test.testing")); + self.view.redraw(cx); + + // Send a test translation request + let test_config = TranslationConfig { + enabled: true, + api_base_url: api_url, + api_key, + model: if model.is_empty() { "qwen3-4b".to_string() } else { model }, + }; + + let url = format!( + "{}/v1/chat/completions", + test_config.api_base_url.trim().trim_end_matches('/') + ); + log!("Test connection URL: '{}', model: '{}'", url, test_config.model); + let body = format!( + r#"{{"model":"{}","messages":[{{"role":"user","content":"Say OK"}}],"temperature":0.1,"max_tokens":10}}"#, + test_config.model, + ); + let mut req = HttpRequest::new(url, HttpMethod::POST); + req.set_header("Content-Type".into(), "application/json".into()); + if !test_config.api_key.is_empty() { + req.set_header("Authorization".into(), format!("Bearer {}", test_config.api_key)); + } + req.set_body(body.into_bytes()); + cx.http_request(TEST_TRANSLATION_REQUEST_ID, req); + } + } +} + +impl TranslationSettings { + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { + self.app_language = app_language; + self.app_language_initialized = true; + self.sync_app_language(cx); + } + + fn sync_app_language(&mut self, cx: &mut Cx) { + self.view + .label(cx, ids!(translation_title)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.title")); + self.view + .label(cx, ids!(description)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.description")); + self.view + .label(cx, ids!(api_url_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.field.api_url")); + self.view + .label(cx, ids!(api_key_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.field.api_key")); + self.view + .label(cx, ids!(model_label)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.field.model")); + self.view + .button(cx, ids!(save_button)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.button.save")); + self.view + .button(cx, ids!(test_button)) + .set_text(cx, tr_key(self.app_language, "settings.labs.translation.button.test_connection")); + self.set_switch_state_label( + cx, + self.view.check_box(cx, ids!(translation_switch)).active(cx), + ); + self.view.redraw(cx); + } + + fn set_switch_state_label(&mut self, cx: &mut Cx, enabled: bool) { + let mut switch_state_label = self.view.label(cx, ids!(switch_state_label)); + if enabled { + script_apply_eval!(cx, switch_state_label, { + text: #(tr_key(self.app_language, "settings.labs.translation.status.enabled")), + draw_text +: { + color: mod.widgets.COLOR_FG_ACCEPT_GREEN + } + }); + } else { + script_apply_eval!(cx, switch_state_label, { + text: #(tr_key(self.app_language, "settings.labs.translation.status.disabled")), + draw_text +: { + color: #999 + } + }); + } + } + + fn sync_ui(&mut self, cx: &mut Cx, config: &TranslationConfig) { + self.view.view(cx, ids!(config_section)) + .set_visible(cx, config.enabled); + + self.view.check_box(cx, ids!(translation_switch)) + .set_active(cx, config.enabled); + self.set_switch_state_label(cx, config.enabled); + } + + /// Populates the translation settings UI from the current app state. + pub fn populate(&mut self, cx: &mut Cx, config: &TranslationConfig) { + translation::set_global_config(config); + self.sync_ui(cx, config); + + self.view.text_input(cx, ids!(api_url_input)) + .set_text(cx, &config.api_base_url); + self.view.text_input(cx, ids!(api_key_input)) + .set_text(cx, &config.api_key); + self.view.text_input(cx, ids!(model_input)) + .set_text(cx, &config.model); + } +} + +impl TranslationSettingsRef { + pub fn populate(&self, cx: &mut Cx, config: &TranslationConfig) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.populate(cx, config); + } + + pub fn set_app_language(&self, cx: &mut Cx, app_language: AppLanguage) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.set_app_language(cx, app_language); + } +} + +fn persist_translation_config(app_state: &AppState) { + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(app_state.clone(), user_id) { + error!("Failed to persist translation settings. Error: {e}"); + } + } +} From 2f66a1aee274dfe1cbe1c6562ef049220bdb6f65 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 04:21:56 +0800 Subject: [PATCH 111/283] Add translation spec and dropdown arrow issue documentation - specs/task-realtime-translation.spec.md: complete feature spec - issues/002: Settings DropDown arrow visual artifact with root cause analysis of Makepad uniform/plain shader type system Co-Authored-By: Claude Opus 4.6 (1M context) --- ...settings-dropdown-arrow-visual-artifact.md | 54 ++++++++ specs/task-realtime-translation.spec.md | 123 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 issues/002-settings-dropdown-arrow-visual-artifact.md create mode 100644 specs/task-realtime-translation.spec.md diff --git a/issues/002-settings-dropdown-arrow-visual-artifact.md b/issues/002-settings-dropdown-arrow-visual-artifact.md new file mode 100644 index 000000000..aea2dae0f --- /dev/null +++ b/issues/002-settings-dropdown-arrow-visual-artifact.md @@ -0,0 +1,54 @@ +# Issue 002: Settings DropDown Arrow Visual Artifact + +## Status: Open + +## Symptom + +The Settings → Preferences → Application Language dropdown displays a blue elliptical shape on the right side of the dropdown button. This is the dropdown arrow indicator area rendered by the DropDown shader, appearing as an oversized capsule shape. + +## Root Cause + +Makepad's `DropDown` widget has a hardcoded arrow drawing region in its `pixel: fn()` shader (in `widgets/src/drop_down.rs`, lines 113-200). The shader draws: +1. A background quad with `border_radius` +2. A separate arrow region on the right side with its own color (`arrow_color`) + +When `border_radius` is set to `6.0` and the button width is `Fit`, the arrow region's capsule shape becomes visually prominent because the shader's arrow area calculation doesn't account for the smaller border radius. + +## Affected Files + +- `src/settings/settings_screen.rs` — the `language_dropdown` instance + +## Investigation Notes + +### Why styling the DropDown is difficult in Makepad + +1. **uniform() vs plain value types**: The DropDown shader uses two types of color declarations: + - `draw_text.color` = plain value (base color for `get_color` shader mix chain) + - `draw_text.color_hover/focus/down` = `uniform()` (GPU uniforms) + - All `draw_bg.*` colors = `uniform()` + +2. Overriding `color` with `uniform()` breaks the shader's `get_color` function (the widget renders blank) +3. Overriding `uniform()` colors with plain values has no effect +4. Custom `set_type_default() do mod.widgets.DropDownFlat { ... }` types fail to render — the `popup_menu` field (stored as `ScriptValue`) loses its registration chain through `on_after_apply` → `PopupMenuGlobal` `ComponentMap` + +### What was tried and failed + +- Custom `SettingsLanguageDropdown` type via `set_type_default()` — rendered blank +- Plain value overrides on `draw_bg.color` — no effect (uniform not overridden) +- `uniform()` override on `draw_text.color` — broke get_color shader, blank text +- `DropDownFlat` with inline overrides — partially worked but arrow artifact remains + +### Current workaround + +Using `DropDown` (not `DropDownFlat`) with correct uniform/plain type matching for color overrides. The arrow visual artifact is accepted as a cosmetic issue. + +## Potential Fix + +Override the `pixel: fn()` shader on the `draw_bg` to customize the arrow drawing area. This requires rewriting ~90 lines of shader code from `drop_down.rs` and is high-effort for a cosmetic issue. + +Alternative: Wait for Makepad upstream to expose arrow styling as configurable properties. + +## Related + +- Makepad source: `widgets/src/drop_down.rs` lines 113-200 (pixel shader) +- Makepad source: `widgets/src/popup_menu.rs` (PopupMenu rendering) diff --git a/specs/task-realtime-translation.spec.md b/specs/task-realtime-translation.spec.md new file mode 100644 index 000000000..aa4b67f9c --- /dev/null +++ b/specs/task-realtime-translation.spec.md @@ -0,0 +1,123 @@ +spec: task +name: "Real-time Translation for Room Input Bar" +inherits: project +tags: [feature, translation, llm, ui] +estimate: 2d +--- + +## Intent + +Add a real-time translation feature to the chat message input bar. Users can toggle "write-and-translate" mode, select a target language, type in their native language, and see the translation appear in a preview area above the input. Clicking "Apply" replaces the input text with the translation, ready to send. + +## Context + +The translation feature uses any OpenAI-compatible LLM API (local or cloud) to translate text in real-time as the user types. The system prompt is adapted from the makepad-voice-input (Vox) project's bilingual correction+translation prompt. + +## Acceptance Criteria + +### Scenario: Configure translation API +- **Given** the user opens Settings → Labs → Real-time Translation +- **When** they enable the toggle and enter API URL, API key, and model name +- **Then** clicking "Save" persists the configuration +- **And** clicking "Test Connection" validates the API returns a response + +### Scenario: Activate translation mode +- **Given** translation is configured and enabled +- **When** the user clicks the translate button (文A icon) in the input bar +- **Then** a language selector popup appears with 17 supported languages +- **When** the user selects a target language (e.g., English) +- **Then** translation mode activates with a preview area showing the language badge + +### Scenario: Real-time translation +- **Given** translation mode is active with target language "English" +- **When** the user types "你好" in the input bar and pauses for 500ms +- **Then** an LLM translation request is sent +- **And** the preview area shows "Hello" (or equivalent translation) + +### Scenario: Apply translation +- **Given** the translation preview shows a result +- **When** the user clicks "Apply" +- **Then** the translated text replaces the input bar content +- **And** the user can send the translated message normally + +### Scenario: Deactivate translation +- **Given** translation mode is active +- **When** the user clicks the translate button again or the close (X) button on the preview +- **Then** translation mode deactivates +- **And** the preview area disappears + +### Scenario: Settings changes take effect immediately +- **Given** translation mode is active +- **When** the user changes the model in Settings → Labs → Translation and clicks Save +- **Then** subsequent translation requests use the new model + +## Decisions + +- LLM API: OpenAI-compatible `/v1/chat/completions` endpoint via Makepad's `cx.http_request()` +- System prompt: Adapted from makepad-voice-input's bilingual correction+translation prompt, supporting both same-language correction and cross-language translation +- Debounce: 500ms timeout via Makepad `Timer` API — restart on each text change +- Config storage: `TranslationConfig` in `AppState` (persisted per account) + global `Mutex>` for cross-widget access (because `scope.data` is not available during Timer/Network events) +- Config refresh: RoomInputBar reads global config on every `handle_event` call (Mutex lock is nanosecond-level) +- Language selector: Static DSL items in `overlay_wrapper` with `abs_pos` positioning via `draw_walk` +- Labels populated via Rust code in `populate_language_list()` (DSL dot-path overrides on deeply nested named children don't work reliably) +- Translation preview: RoundedView above input bar with language badge, preview text, Apply button, close button +- HTTP response handling: via `Event::NetworkResponses` / `NetworkResponse::HttpResponse` pattern + +## Boundaries + +### Allowed Changes +- `src/room/translation.rs` — NEW: translation service, config, LLM API, response parsing +- `src/settings/translation_settings.rs` — NEW: Settings UI for translation API config +- `src/room/room_input_bar.rs` — translate button, language popup, preview, debounce, HTTP handling +- `src/room/mod.rs` — register translation module +- `src/settings/mod.rs` — register translation_settings module +- `src/settings/settings_screen.rs` — add TranslationSettings to Labs tab +- `src/app.rs` — add TranslationConfig to AppState, init global config on restore +- `src/home/home_screen.rs` — pass translation config to settings populate +- `resources/icons/translate.svg` — translation icon +- `resources/i18n/en.json`, `zh-CN.json` — translation-related i18n keys + +### Forbidden +- Do NOT add new cargo dependencies +- Do NOT modify the message sending pipeline (translation is applied before send, not during) +- Do NOT store translation state in Matrix room events +- Do NOT run `cargo fmt` + +## Supported Languages + +| Code | Name | +|------|------| +| en | English | +| zh | 简体中文 | +| zh-TW | 繁體中文 | +| ja | 日本語 | +| ko | 한국어 | +| es | Español | +| fr | Français | +| de | Deutsch | +| ru | Русский | +| pt | Português | +| ar | العربية | +| hi | हिन्दी | +| th | ไทย | +| vi | Tiếng Việt | +| id | Bahasa Indonesia | +| ms | Bahasa Melayu | +| tr | Türkçe | + +## Known Issues + +- Language selector popup positioning uses `abs_pos` in `draw_walk` — position is calculated from button rect, may shift if input bar layout changes +- Settings → Preferences language dropdown has arrow visual artifact (see issues/002) + +## Completion Criteria + +- [x] Translation service with OpenAI-compatible API +- [x] Settings UI with toggle, API config, Save, Test Connection +- [x] Translate button in input bar +- [x] Language selector popup with 17 languages +- [x] Real-time debounced translation +- [x] Translation preview with Apply/Close +- [x] Global config for cross-widget access +- [x] Config changes take effect immediately +- [x] `cargo build` passes From 5e7155c65202ba12b584a650cdb86a20510b7494 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 05:38:22 +0800 Subject: [PATCH 112/283] Replace DropDown with custom language selector using ExpandArrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace Makepad DropDown widget (unsolvable shader styling issues) with custom RoundedView + Label + ExpandArrow + popup list - ExpandArrow provides animated triangle: right (>) collapsed, down (v) expanded — SDF-drawn, no SVG dependency - Add chevron_right.svg and chevron_down.svg (unused now, kept for potential future use) - Fix issue #002: DropDown arrow visual artifact no longer applies Co-Authored-By: Claude Opus 4.6 (1M context) --- resources/icons/chevron_down.svg | 3 + resources/icons/chevron_right.svg | 3 + src/settings/settings_screen.rs | 218 ++++++++++++++++++------------ 3 files changed, 141 insertions(+), 83 deletions(-) create mode 100644 resources/icons/chevron_down.svg create mode 100644 resources/icons/chevron_right.svg diff --git a/resources/icons/chevron_down.svg b/resources/icons/chevron_down.svg new file mode 100644 index 000000000..ab08d1415 --- /dev/null +++ b/resources/icons/chevron_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/icons/chevron_right.svg b/resources/icons/chevron_right.svg new file mode 100644 index 000000000..3cf8725e9 --- /dev/null +++ b/resources/icons/chevron_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 3229897fc..3ba531fef 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,14 +1,14 @@ use makepad_widgets::*; -use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; +use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{expand_arrow::ExpandArrow, popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; script_mod! { use mod.prelude.widgets.* use mod.widgets.* - // No custom DropDown types — inline override on the instance instead. - // Custom set_type_default() on DropDownFlat fails to render in this Makepad version. + mod.widgets.ICO_CHEVRON_RIGHT = crate_resource("self://resources/icons/chevron_right.svg") + mod.widgets.ICO_CHEVRON_DOWN = crate_resource("self://resources/icons/chevron_down.svg") // The main, top-level settings screen widget. mod.widgets.SettingsScreen = #(SettingsScreen::register_widget(vm)) { @@ -111,78 +111,88 @@ script_mod! { text: "Application language" } - language_dropdown := DropDown { - width: 200 + // Custom language selector: button + popup list + // (replaces DropDown which has unsolvable arrow shader artifact) + language_selector_button := RoundedView { + width: 200, height: Fit + flow: Right + align: Align{y: 0.5} + padding: Inset{left: 12, right: 10, top: 10, bottom: 10} margin: Inset{left: 5, top: 2, bottom: 2} - // draw_text.color is plain (NOT uniform) in DropDownFlat - draw_text +: { - color: #x333333 - color_hover: uniform(#x333333) - color_focus: uniform(#x333333) - color_down: uniform(#x333333) - text_style: REGULAR_TEXT { font_size: 11 } - } - // draw_bg colors are all uniform() in DropDownFlat + cursor: MouseCursor.Hand + show_bg: true draw_bg +: { - color: uniform(#xF6FAFF) - color_hover: uniform(#xF0F6FF) - color_focus: uniform(#xFFFFFF) - color_down: uniform(#xEAF2FF) - color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - color_2_focus: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - color_2_down: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color: uniform(#xC8D9F2) - border_color_hover: uniform(#x74A7EE) - border_color_focus: uniform(#x1B6EF3) - border_color_down: uniform(#x1B6EF3) - border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color_2_focus: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color_2_down: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - arrow_color: uniform(#x555555) - arrow_color_hover: uniform(#x1B6EF3) - arrow_color_focus: uniform(#x1B6EF3) - arrow_color_down: uniform(#x1B6EF3) - border_size: uniform(1.5) - border_radius: uniform(6.0) + color: (COLOR_PRIMARY) + border_radius: 4.0 + border_size: 1.0 + border_color: #xC8D9F2 + } + + language_selector_label := Label { + width: Fill, height: Fit + draw_text +: { + color: #x333333 + text_style: REGULAR_TEXT { font_size: 11 } + } + text: "English" } - popup_menu +: { + + language_arrow := ExpandArrow { + width: 14, height: 14 draw_bg +: { - color: uniform(#xFDFEFF) - color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color: uniform(#xD3E1F6) - border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_size: uniform(1.0) - border_radius: uniform(8.0) - color_dither: uniform(0.0) + color: instance(#x888888) } - // menu_item.draw_text.color is plain; others are uniform - menu_item +: { + } + } + + language_popup := RoundedView { + visible: false + width: 200, height: Fit + flow: Down + padding: Inset{top: 4, bottom: 4} + show_bg: true + new_batch: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + border_size: 1.0 + border_color: #xD3E1F6 + } + + lang_option_en := View { + width: Fill, height: 36 + flow: Right + align: Align{y: 0.5} + padding: Inset{left: 12, right: 12} + cursor: MouseCursor.Hand + show_bg: true + draw_bg +: { color: #0000 } + Label { + width: Fit, height: Fit draw_text +: { color: #x333333 - color_hover: uniform(#x333333) - color_active: uniform(#x1B6EF3) + text_style: REGULAR_TEXT { font_size: 11 } } - draw_bg +: { - color: uniform(#x00000000) - color_hover: uniform(#xF0F4FA) - color_active: uniform(#xDCEBFF) - color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - color_2_active: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color: uniform(#x00000000) - border_color_hover: uniform(#x00000000) - border_color_active: uniform(#x00000000) - border_color_2: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color_2_hover: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - border_color_2_active: uniform(vec4(-1.0, -1.0, -1.0, -1.0)) - mark_color: uniform(#x00000000) - mark_color_active: uniform(#x1B6EF3) + text: "English" + } + } + lang_option_zh := View { + width: Fill, height: 36 + flow: Right + align: Align{y: 0.5} + padding: Inset{left: 12, right: 12} + cursor: MouseCursor.Hand + show_bg: true + draw_bg +: { color: #0000 } + Label { + width: Fit, height: Fit + draw_text +: { + color: #x333333 + text_style: REGULAR_TEXT { font_size: 11 } } + text: "简体中文" } } - labels: ["English", "Simplified Chinese"] } preferences_language_hint_label := Label { @@ -249,6 +259,7 @@ pub struct SettingsScreen { #[rust] selected_category: SettingsCategory, #[rust] app_language: AppLanguage, + #[rust] language_popup_visible: bool, } impl Widget for SettingsScreen { @@ -286,26 +297,57 @@ impl Widget for SettingsScreen { cx.action(NavigationBarAction::CloseSettings); } - if let Event::Actions(actions) = event { - if self.view.drop_down(cx, ids!(language_dropdown)).changed(actions).is_some() { - let selected_language = AppLanguage::from_dropdown_index( - self.view.drop_down(cx, ids!(language_dropdown)).selected_item(), - ); - if self.app_language != selected_language { - self.set_app_language(cx, selected_language); - if let Some(app_state) = scope.data.get_mut::() { - if app_state.app_language != selected_language { - app_state.app_language = selected_language; - persist_app_state(app_state); - enqueue_popup_notification( - tr(selected_language, I18nKey::LanguageReloadHint), - PopupKind::Info, - Some(4.0), - ); + // Handle language selector button click + { + let selector = self.view.view(cx, ids!(language_selector_button)); + if let Hit::FingerUp(fe) = event.hits(cx, selector.area()) { + if fe.is_over && fe.was_tap() { + self.language_popup_visible = !self.language_popup_visible; + self.view.view(cx, ids!(language_popup)).set_visible(cx, self.language_popup_visible); + self.update_language_button_text(cx); + self.redraw(cx); + } + } + } + + // Handle language popup item selection via finger_up + if self.language_popup_visible { + let lang_options: &[(&[LiveId], usize)] = &[ + (&[live_id!(lang_option_en)], 0), + (&[live_id!(lang_option_zh)], 1), + ]; + for &(id_path, index) in lang_options { + let item_view = self.view.view(cx, id_path); + if let Hit::FingerUp(fe) = event.hits(cx, item_view.area()) { + if fe.is_over && fe.was_tap() { + self.language_popup_visible = false; + self.view.view(cx, &[live_id!(language_popup)]).set_visible(cx, false); + self.update_language_button_text(cx); + + let selected_language = AppLanguage::from_dropdown_index(index); + if self.app_language != selected_language { + self.set_app_language(cx, selected_language); + if let Some(app_state) = scope.data.get_mut::() { + if app_state.app_language != selected_language { + app_state.app_language = selected_language; + persist_app_state(app_state); + enqueue_popup_notification( + tr(selected_language, I18nKey::LanguageReloadHint), + PopupKind::Info, + Some(4.0), + ); + } + } } + self.redraw(cx); + break; } } } + } + + if let Event::Actions(actions) = event { + // Handle language selector click — moved to finger_up below if self.view.button(cx, ids!(category_account_button)).clicked(actions) { self.set_selected_category(cx, SettingsCategory::Account); @@ -394,9 +436,7 @@ impl SettingsScreen { self.view .label(cx, ids!(preferences_language_hint_label)) .set_text(cx, tr(self.app_language, I18nKey::LanguageReloadHint)); - let language_dropdown = self.view.drop_down(cx, ids!(language_dropdown)); - language_dropdown.set_labels(cx, language_dropdown_labels(self.app_language)); - language_dropdown.set_selected_item(cx, self.app_language.dropdown_index()); + self.update_language_button_text(cx); self.view .account_settings(cx, ids!(account_settings)) .set_app_language(cx, self.app_language); @@ -409,6 +449,18 @@ impl SettingsScreen { self.view.redraw(cx); } + fn update_language_button_text(&mut self, cx: &mut Cx) { + let labels = language_dropdown_labels(self.app_language); + let selected_idx = self.app_language.dropdown_index(); + let selected_label = labels.get(selected_idx).cloned().unwrap_or_else(|| "English".to_string()); + self.view.label(cx, ids!(language_selector_label)).set_text(cx, &selected_label); + + // Toggle expand arrow direction + if let Some(mut arrow) = self.view.child_by_path(ids!(language_arrow)).borrow_mut::() { + arrow.set_is_open(cx, self.language_popup_visible, Animate::Yes); + } + } + fn set_selected_category(&mut self, cx: &mut Cx, category: SettingsCategory) { self.selected_category = category; self.sync_selected_category(cx); From f070c3aa763978d96a8e9e08082922bbe0a23702 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 05:44:25 +0800 Subject: [PATCH 113/283] Fix clippy large_enum_variant: Box AppState in RestoreAppStateFromPersistentState CI runs with RUSTFLAGS="-D warnings", so clippy warnings are errors. Box the AppState payload to reduce enum size from 512+ bytes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app.rs | 4 ++-- src/sliding_sync.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2fc0caead..59a25eb46 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1033,7 +1033,7 @@ impl MatchEvent for App { Some(AppStateAction::RestoreAppStateFromPersistentState(app_state)) => { // Ignore the `logged_in` state that was stored persistently. let logged_in_actual = self.app_state.logged_in; - self.app_state = app_state.clone(); + self.app_state = *app_state.clone(); self.app_state.logged_in = logged_in_actual; // Initialize the global translation config so RoomInputBar can access it. crate::room::translation::set_global_config(&self.app_state.translation); @@ -2429,7 +2429,7 @@ pub enum AppStateAction { UpgradedInviteToJoinedRoom(OwnedRoomId), /// The given app state was loaded from persistent storage /// and is ready to be restored. - RestoreAppStateFromPersistentState(AppState), + RestoreAppStateFromPersistentState(Box), /// A room-level BotFather bind or unbind action completed. BotRoomBindingUpdated { room_id: OwnedRoomId, diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index a915ba36a..ce71a702c 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -4683,7 +4683,7 @@ fn handle_load_app_state(user_id: OwnedUserId) { && !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)); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(Box::new(app_state))); } } Err(_e) => { From 88665d95635d944678307d05f04967c2887e21c1 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 05:57:22 +0800 Subject: [PATCH 114/283] Fix Android/cross-platform build: remove direct reqwest dependency - Replace reqwest::Error in UrlPreviewError with String to avoid direct reqwest crate dependency (not available on Android target) - Replace reqwest::StatusCode imports with matrix_sdk::reqwest::StatusCode (matrix-sdk re-exports reqwest) - Fix clippy unnecessary closure warnings in ok_or_else Co-Authored-By: Claude Opus 4.6 (1M context) --- src/home/room_image_viewer.rs | 2 +- src/media_cache.rs | 2 +- src/sliding_sync.rs | 22 +++++++--------------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/home/room_image_viewer.rs b/src/home/room_image_viewer.rs index 9bc11b6c4..ba8eaa3a3 100644 --- a/src/home/room_image_viewer.rs +++ b/src/home/room_image_viewer.rs @@ -4,7 +4,7 @@ use matrix_sdk::{ media::MediaFormat, ruma::events::room::{message::MessageType, MediaSource}, }; -use reqwest::StatusCode; +use matrix_sdk::reqwest::StatusCode; use crate::{media_cache::{MediaCache, MediaCacheEntry}, shared::image_viewer::{ImageViewerAction, ImageViewerError, LoadState}}; diff --git a/src/media_cache.rs b/src/media_cache.rs index 8b20e40b3..672a16cf1 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -2,7 +2,7 @@ use std::{ops::{Deref, DerefMut}, sync::{Arc, Mutex}, time::SystemTime}; use hashbrown::{hash_map::RawEntryMut, HashMap}; use makepad_widgets::{error, log, SignalToUI}; use matrix_sdk::{media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, ruma::{events::room::MediaSource, OwnedMxcUri}, Error, HttpError}; -use reqwest::StatusCode; +use matrix_sdk::reqwest::StatusCode; use crate::{home::room_screen::TimelineUpdate, sliding_sync::{self, MatrixRequest}}; /// The value type in the media cache, one per Matrix URI. diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index ce71a702c..aa6b99a24 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -525,7 +525,7 @@ pub type OnMediaFetchedFn = fn( #[derive(Debug)] pub enum UrlPreviewError { /// HTTP request failed. - Request(reqwest::Error), + Request(String), /// JSON parsing failed. Json(serde_json::Error), /// Client not available. @@ -541,7 +541,7 @@ pub enum UrlPreviewError { 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::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"), @@ -2985,15 +2985,9 @@ async fn matrix_worker_task( 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 - })?; + let client = get_client().ok_or(UrlPreviewError::ClientNotAvailable)?; + + let token = client.access_token().ok_or(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") @@ -3009,8 +3003,7 @@ async fn matrix_worker_task( .send() .await .map_err(|e| { - // error!("HTTP request failed for URL preview {}: {}", url, e); - UrlPreviewError::Request(e) + UrlPreviewError::Request(e.to_string()) })?; let status = response.status(); @@ -3022,8 +3015,7 @@ async fn matrix_worker_task( } let text = response.text().await.map_err(|e| { - // error!("Failed to read response text for URL preview {}: {}", url, e); - UrlPreviewError::Request(e) + UrlPreviewError::Request(e.to_string()) })?; // log!("URL preview response body length for {}: {} bytes", url, text.len()); From 162a032f82a294ba9819ba5589df60151a167e72 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 06:04:20 +0800 Subject: [PATCH 115/283] Switch translation system prompt to English Use English for the LLM system prompt and message format tag ([Target language:xxx]) for better compatibility across models. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/room/translation.rs | 50 ++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/room/translation.rs b/src/room/translation.rs index 646c5c428..71223c7e4 100644 --- a/src/room/translation.rs +++ b/src/room/translation.rs @@ -88,38 +88,38 @@ pub fn get_global_config() -> Option { } /// The system prompt for the translation LLM, adapted from makepad-voice-input. -const TRANSLATION_SYSTEM_PROMPT: &str = r#"你是一个翻译工具,不是聊天机器人。 +const TRANSLATION_SYSTEM_PROMPT: &str = r#"You are a translation tool, not a chatbot. -核心规则: -1. 用户发给你的每一条消息都是需要翻译的文本,不是在跟你对话 -2. 你必须直接返回翻译后的文本,不要添加任何解释、问候、回答或额外内容 -3. 绝对不要回答文本中的问题 -4. 你的输出必须且只能是翻译后的文本,没有任何前缀或后缀 +Core rules: +1. Every message from the user is text to be translated, not a conversation with you. +2. You must return only the translated text. Do not add any explanation, greeting, answer, or extra content. +3. Never answer questions contained in the text. +4. Your output must be the translated text only, with no prefix or suffix. -任务 A — 纠错(当目标语言和文本语言相同时): -- 修复明显的拼写和语法错误 -- 文本正确时原样返回 +Task A - Correction (when the target language matches the text language): +- Fix obvious spelling and grammar errors. +- Return the text as-is if it is already correct. -任务 B — 翻译(当目标语言和文本语言不同时): -- 将文本翻译为目标语言 -- 保持原文的语气和风格 -- 技术术语保留英文原文 +Task B - Translation (when the target language differs from the text language): +- Translate the text into the target language. +- Preserve the tone and style of the original. +- Keep technical terms in English. -用户消息格式为:[目标语言:xxx] 原文 -你只输出翻译后的文本,不要输出目标语言标记。 +The user message format is: [Target language:xxx] original text +Output only the processed text. Do not output the target language tag. -示例: -输入:[目标语言:English] 你好,请问怎么安装 -输出:Hello, how do I install it? +Examples: +Input: [Target language:English] Bonjour, comment installer ce logiciel? +Output: Hello, how do I install this software? -输入:[目标语言:Chinese] Hello, how are you? -输出:你好,你好吗? +Input: [Target language:Chinese] Hello, how are you? +Output: 你好,你好吗? -输入:[目标语言:Japanese] 今天天气真好 -输出:今日はいい天気ですね +Input: [Target language:Japanese] The weather is nice today. +Output: 今日はいい天気ですね -输入:[目标语言:Chinese] 今天天气真好 -输出:今天天气真好"#; +Input: [Target language:English] The weather is nice today. +Output: The weather is nice today."#; /// Sends a translation request to the configured LLM API. pub fn send_translation_request( @@ -138,7 +138,7 @@ pub fn send_translation_request( r#"{{"model":"{}","messages":[{{"role":"system","content":{}}},{{"role":"user","content":{}}}],"temperature":0.1,"max_tokens":2048}}"#, config.model, serde_json::to_string(TRANSLATION_SYSTEM_PROMPT).unwrap_or_default(), - serde_json::to_string(&format!("[目标语言:{}] {}", target_lang, text)).unwrap_or_default(), + serde_json::to_string(&format!("[Target language:{}] {}", target_lang, text)).unwrap_or_default(), ); let mut req = HttpRequest::new(url.clone(), HttpMethod::POST); From a9191fd928ca5a60c3b08c59157639b9dc11f414 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 06:11:32 +0800 Subject: [PATCH 116/283] Fix Windows build: move latest_log_path into cfg(unix) block The variable was only used inside #[cfg(unix)] but declared outside, causing unused variable warning on Windows (treated as error by CI). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 59a25eb46..937b15563 100644 --- a/src/app.rs +++ b/src/app.rs @@ -452,11 +452,10 @@ fn init_file_logging() -> Option<()> { let log_path = logs_dir.join(&log_filename); // Also create/update a symlink to the latest log file for convenience - let latest_log_path = logs_dir.join("robrix_latest.log"); - - // Remove old symlink if it exists (ignore errors) + // Remove old symlink if it exists and create a new one (unix only) #[cfg(unix)] { + let latest_log_path = logs_dir.join("robrix_latest.log"); let _ = std::fs::remove_file(&latest_log_path); let _ = std::os::unix::fs::symlink(&log_filename, &latest_log_path); } From b04eb588ada7cad9330aabf4edc5dc19ce93557b Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 06:40:40 +0800 Subject: [PATCH 117/283] Merge upstream project-robius/robrix:main (19 commits) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings in upstream changes including matrix-sdk 0.16 upgrade, SpaceLobby filter/suggested rooms, overlay container positioning, LoginScreen RoundedView, and RoomFilterAction→MainFilterAction rename. Resolves conflicts by preserving our i18n translations, logout state cleanup, and mime dependencies while adopting upstream structural improvements. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/builds.yml | 22 +- .github/workflows/release.yml | 3 + Cargo.lock | 887 +++++++++++++++++----------- Cargo.toml | 26 +- README.md | 17 +- resources/i18n/en.json | 1 + resources/i18n/zh-CN.json | 1 + src/app.rs | 24 +- src/event_preview.rs | 43 +- src/home/edited_indicator.rs | 2 +- src/home/home_screen.rs | 15 +- src/home/room_screen.rs | 33 +- src/home/rooms_list.rs | 6 +- src/home/rooms_sidebar.rs | 8 + src/home/space_lobby.rs | 315 +++++++++- src/home/spaces_bar.rs | 9 +- src/login/login_screen.rs | 24 +- src/room/room_input_bar.rs | 6 + src/shared/room_filter_input_bar.rs | 51 +- src/sliding_sync.rs | 33 +- src/utils.rs | 4 +- 21 files changed, 1044 insertions(+), 486 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index e219bc1c9..7aee76ee6 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -181,6 +181,10 @@ jobs: run: | cargo makepad apple ios install-toolchain + - name: Install bindgen-cli for aws-lc-sys cross-compilation + run: | + cargo install --force --locked bindgen-cli + - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 with: @@ -189,6 +193,7 @@ jobs: - name: Build for iOS targets env: RUSTFLAGS: "-D warnings" + AWS_LC_SYS_CMAKE_BUILDER: 1 run: | # Install iOS targets rustup target add aarch64-apple-ios @@ -197,13 +202,12 @@ jobs: cargo makepad apple ios \ --org=rs.robius \ --app=robrix \ - run-sim -p robrix \ + build -p robrix \ --config profile.dev.opt-level=0 \ --config profile.dev.debug=false \ --config profile.dev.lto=\"off\" \ --config profile.dev.strip=true \ --config profile.dev.debug-assertions=false - continue-on-error: true # iOS builds may fail due to signing requirements in CI build_android_on_macos: name: Build Android (macOS Host) @@ -220,7 +224,7 @@ jobs: - name: Install Android toolchain run: | - cargo makepad android install-toolchain + cargo makepad android install-toolchain --full-ndk - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -230,6 +234,7 @@ jobs: - name: Build Android APK env: RUSTFLAGS: "-D warnings" + AWS_LC_SYS_CMAKE_BUILDER: 1 run: | cargo makepad android build -p robrix \ --config profile.dev.opt-level=0 \ @@ -253,7 +258,7 @@ jobs: - name: Install Android toolchain run: | - cargo makepad android install-toolchain + cargo makepad android install-toolchain --full-ndk - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -263,6 +268,7 @@ jobs: - name: Build Android APK env: RUSTFLAGS: "-D warnings" + AWS_LC_SYS_CMAKE_BUILDER: 1 run: | cargo makepad android build -p robrix \ --config profile.dev.opt-level=0 \ @@ -294,11 +300,17 @@ jobs: - name: Install Android toolchain run: | - cargo makepad android install-toolchain + cargo makepad android install-toolchain --full-ndk + + - name: Install Ninja + run: choco install ninja -y + shell: powershell - name: Build Android APK env: RUSTFLAGS: "-D warnings" + AWS_LC_SYS_CMAKE_BUILDER: 1 + CMAKE_GENERATOR: Ninja run: | ## Note: we can't use the profile here because cargo-makepad doesn't support custom profiles. # cargo makepad android build -p robrix --profile fast diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b21c72a19..6e1394630 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -345,6 +345,8 @@ jobs: - name: Package (android) uses: project-robius/makepad-packaging-action@v1 env: + AWS_LC_SYS_CMAKE_BUILDER: 1 + MAKEPAD_ANDROID_FULL_NDK: true MAKEPAD_MOBILE_CARGO_EXTRA_ARGS: >- --config profile.dev.opt-level=0 --config profile.dev.debug=false @@ -370,6 +372,7 @@ jobs: - name: Package (iOS) uses: project-robius/makepad-packaging-action@v1 env: + AWS_LC_SYS_CMAKE_BUILDER: 1 MAKEPAD_MOBILE_CARGO_EXTRA_ARGS: >- --config profile.dev.opt-level=0 --config profile.dev.debug=false diff --git a/Cargo.lock b/Cargo.lock index 2182661e2..f7188bde6 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=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "accessory" @@ -43,7 +43,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -224,7 +224,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2 0.10.6", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -320,7 +320,7 @@ dependencies = [ "block-modes", "bls12_381", "cbc", - "chacha20", + "chacha20 0.9.1", "chacha20poly1305", "cipher", "crypto_box", @@ -353,7 +353,7 @@ dependencies = [ "argon2", "base64", "blake2 0.10.6", - "chacha20", + "chacha20 0.9.1", "chacha20poly1305", "digest 0.10.7", "group", @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ "async-io", "async-lock", @@ -844,7 +844,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "bitmaps" @@ -996,7 +996,7 @@ checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "byteorder" @@ -1007,7 +1007,7 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "byteorder-lite" @@ -1083,7 +1083,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", ] [[package]] @@ -1093,7 +1104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "poly1305", "zeroize", @@ -1348,6 +1359,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1460,7 +1480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "crypto_secretbox", "curve25519-dalek", "salsa20", @@ -1475,7 +1495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" dependencies = [ "aead", - "chacha20", + "chacha20 0.9.1", "cipher", "generic-array", "poly1305", @@ -1510,7 +1530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", @@ -1580,30 +1600,29 @@ checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" [[package]] name = "deadpool" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" dependencies = [ "deadpool-runtime", - "lazy_static", "num_cpus", "tokio", ] [[package]] name = "deadpool-runtime" -version = "0.1.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" dependencies = [ "tokio", ] [[package]] name = "deadpool-sync" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +checksum = "e385cc95d3d582c328b36d1ff90feac061102b001894b555e6b465a2e0eaabbf" dependencies = [ "deadpool-runtime", ] @@ -1613,10 +1632,6 @@ name = "decancer" version = "3.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d" -dependencies = [ - "lazy_static", - "regex", -] [[package]] name = "delegate-display" @@ -1715,7 +1730,7 @@ dependencies = [ "ed25519-dalek", "multihash", "percent-encoding", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_json_canonicalizer", @@ -1890,7 +1905,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50c1c1870b766fc398e5f0526498d09c94b6de15be5fd769a28bbc804fb1b05d" dependencies = [ - "phf 0.13.1", + "phf", ] [[package]] @@ -2202,16 +2217,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures" version = "0.3.31" @@ -2337,9 +2342,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", ] [[package]] @@ -2389,11 +2394,27 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", + "wasm-bindgen", +] + [[package]] name = "ghash" version = "0.5.1" @@ -2536,30 +2557,6 @@ dependencies = [ "hashbrown 0.15.5", ] -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.5.0" @@ -2586,9 +2583,9 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hilog-sys" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51434915c43a27465a1806e65273b52293feb27cfd28ee1996c5c2b37eecfffe" +checksum = "96b1d492766a538e49020f97af3e91e0acb718b3b008ed4ab6d39374f42b3e83" [[package]] name = "hkdf" @@ -2640,13 +2637,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.35.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" dependencies = [ "log", "markup5ever", - "match_token", ] [[package]] @@ -2670,15 +2666,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-auth" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" -dependencies = [ - "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "http-body" version = "1.0.1" @@ -2761,7 +2748,6 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", ] [[package]] @@ -2916,6 +2902,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -3176,10 +3168,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.84" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "992dc2f5318945507d390b324ab307c7e7ef69da0002cd14f178a5f37e289dc5" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -3216,21 +3210,11 @@ dependencies = [ [[package]] name = "konst" -version = "0.3.16" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" dependencies = [ "const_panic", - "konst_kernel", - "typewit", -] - -[[package]] -name = "konst_kernel" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" -dependencies = [ "typewit", ] @@ -3249,6 +3233,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lebe" version = "0.5.3" @@ -3374,12 +3364,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "macroific" version = "2.0.0" @@ -3440,7 +3424,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-objc-sys", ] @@ -3448,12 +3432,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-widgets", ] @@ -3461,7 +3445,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-proc-macro", ] @@ -3469,7 +3453,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -3478,7 +3462,7 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "ab_glyph_rasterizer", "fxhash", @@ -3492,15 +3476,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-serde", ] @@ -3508,22 +3492,22 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-live-id", ] @@ -3537,7 +3521,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "ttf-parser", ] @@ -3545,7 +3529,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-live-id-macros", "serde", @@ -3554,7 +3538,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-proc-macro", ] @@ -3562,7 +3546,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-filesystem-watcher", ] @@ -3570,7 +3554,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-serde", ] @@ -3578,12 +3562,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3592,7 +3576,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-proc-macro", ] @@ -3600,7 +3584,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3614,15 +3598,15 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3643,7 +3627,7 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "wayland-client 0.31.12", "wayland-egl", "wayland-protocols 0.32.10", @@ -3655,12 +3639,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-error-log", "makepad-html", @@ -3668,13 +3652,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-micro-proc-macro", ] @@ -3682,7 +3666,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-network", "makepad-script", @@ -3691,14 +3675,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3708,7 +3692,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-html", "makepad-live-id", @@ -3717,7 +3701,7 @@ dependencies = [ [[package]] name = "makepad-tsdf" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-math", "makepad-micro-serde", @@ -3726,7 +3710,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3735,7 +3719,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-byteorder-lite", ] @@ -3743,7 +3727,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3752,18 +3736,18 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "simd-adler32 0.3.8", ] @@ -3771,7 +3755,7 @@ dependencies = [ [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-zune-core", ] @@ -3779,7 +3763,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3793,26 +3777,15 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.35.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" dependencies = [ "log", "tendril", "web_atoms", ] -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "matchers" version = "0.2.0" @@ -3854,7 +3827,7 @@ dependencies = [ [[package]] name = "matrix-sdk" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "anymap2", "aquamarine", @@ -3887,12 +3860,15 @@ dependencies = [ "mime", "mime2ext", "oauth2", - "once_cell", + "oauth2-reqwest", "percent-encoding", "pin-project-lite", - "rand 0.8.5", - "reqwest", + "rand 0.10.0", + "reqwest 0.13.2", "ruma", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "serde", "serde_html_form", "serde_json", @@ -3907,13 +3883,14 @@ dependencies = [ "url", "urlencoding", "vodozemac", + "webpki-roots", "zeroize", ] [[package]] name = "matrix-sdk-base" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "as_variant", "async-trait", @@ -3926,7 +3903,6 @@ dependencies = [ "matrix-sdk-common", "matrix-sdk-crypto", "matrix-sdk-store-encryption", - "once_cell", "regex", "ruma", "serde", @@ -3940,7 +3916,7 @@ dependencies = [ [[package]] name = "matrix-sdk-common" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "eyeball-im", "futures-core", @@ -3963,7 +3939,7 @@ dependencies = [ [[package]] name = "matrix-sdk-crypto" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "aes", "aquamarine", @@ -3982,7 +3958,7 @@ dependencies = [ "js_option", "matrix-sdk-common", "pbkdf2", - "rand 0.8.5", + "rand 0.10.0", "rmp-serde", "ruma", "serde", @@ -4003,12 +3979,12 @@ dependencies = [ [[package]] name = "matrix-sdk-indexeddb" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "async-trait", "base64", "futures-util", - "getrandom 0.2.16", + "getrandom 0.4.2", "gloo-utils", "hkdf", "js-sys", @@ -4034,7 +4010,7 @@ dependencies = [ [[package]] name = "matrix-sdk-sqlite" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "as_variant", "async-trait", @@ -4061,15 +4037,15 @@ dependencies = [ [[package]] name = "matrix-sdk-store-encryption" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "base64", "blake3", "chacha20poly1305", - "getrandom 0.2.16", + "getrandom 0.4.2", "hmac", "pbkdf2", - "rand 0.8.5", + "rand 0.10.0", "rmp-serde", "serde", "serde_json", @@ -4081,7 +4057,7 @@ dependencies = [ [[package]] name = "matrix-sdk-ui" version = "0.16.0" -source = "git+https://github.com/matrix-org/matrix-rust-sdk?branch=main#d64c9906587a686f477042a5129d4ca9e6f0d1a2" +source = "git+https://github.com/project-robius/matrix-rust-sdk?branch=space_room_suggested#627563bb87e4746c0758560efe2c3fdc034faac6" dependencies = [ "as_variant", "async-rx", @@ -4104,7 +4080,6 @@ dependencies = [ "matrix-sdk-base", "matrix-sdk-common", "mime", - "once_cell", "pin-project-lite", "ruma", "serde", @@ -4185,7 +4160,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "memoffset" @@ -4349,7 +4324,7 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", "security-framework 2.11.1", @@ -4508,7 +4483,6 @@ dependencies = [ "getrandom 0.2.16", "http", "rand 0.8.5", - "reqwest", "serde", "serde_json", "serde_path_to_error", @@ -4517,6 +4491,16 @@ dependencies = [ "url", ] +[[package]] +name = "oauth2-reqwest" +version = "0.1.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234fb5c965bbce983ee5de636a7a51d6a3223da8067ea02f9ab2d2d78ac08be2" +dependencies = [ + "oauth2", + "reqwest 0.13.2", +] + [[package]] name = "objc2" version = "0.6.2" @@ -4644,6 +4628,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -4755,7 +4745,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", ] [[package]] @@ -4773,51 +4762,34 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_shared 0.13.1", + "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ "phf_generator", - "phf_shared 0.11.3", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ - "siphasher", + "fastrand", + "phf_shared", ] [[package]] @@ -4918,7 +4890,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -4930,7 +4902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -5068,10 +5040,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "unicase 2.9.0", ] @@ -5194,6 +5166,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -5215,6 +5193,17 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5253,6 +5242,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "rand_xoshiro" version = "0.7.0" @@ -5471,9 +5466,47 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", "web-sys", - "webpki-roots", ] [[package]] @@ -5648,7 +5681,7 @@ dependencies = [ "quinn", "rand 0.8.5", "rangemap", - "reqwest", + "reqwest 0.12.28", "rfd", "robius-directories", "robius-location", @@ -5689,7 +5722,7 @@ dependencies = [ [[package]] name = "ruma" version = "0.14.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "assign", "js_int", @@ -5697,7 +5730,6 @@ dependencies = [ "ruma-client-api", "ruma-common", "ruma-events", - "ruma-federation-api", "ruma-html", "web-time", ] @@ -5705,7 +5737,7 @@ dependencies = [ [[package]] name = "ruma-client-api" version = "0.22.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "as_variant", "assign", @@ -5728,7 +5760,7 @@ dependencies = [ [[package]] name = "ruma-common" version = "0.17.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "as_variant", "base64", @@ -5737,7 +5769,6 @@ dependencies = [ "getrandom 0.2.16", "http", "indexmap 2.13.0", - "js-sys", "js_int", "konst", "percent-encoding", @@ -5761,52 +5792,29 @@ dependencies = [ [[package]] name = "ruma-events" version = "0.32.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "as_variant", "indexmap 2.13.0", "js_int", "js_option", - "percent-encoding", "pulldown-cmark 0.13.0", - "regex", "ruma-common", "ruma-html", - "ruma-identifiers-validation", "ruma-macros", "serde", "serde_json", "thiserror 2.0.17", "tracing", - "url", "web-time", "wildmatch", "zeroize", ] -[[package]] -name = "ruma-federation-api" -version = "0.13.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" -dependencies = [ - "headers", - "http", - "http-auth", - "js_int", - "mime", - "ruma-common", - "ruma-events", - "ruma-signatures", - "serde", - "serde_json", - "thiserror 2.0.17", - "tracing", -] - [[package]] name = "ruma-html" version = "0.6.0" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "as_variant", "html5ever", @@ -5817,7 +5825,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" version = "0.12.0" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "js_int", "thiserror 2.0.17", @@ -5826,7 +5834,7 @@ dependencies = [ [[package]] name = "ruma-macros" version = "0.17.1" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" +source = "git+https://github.com/project-robius/ruma.git?branch=tsp#98196b1db32d3393216f31d2c085a7082bd1d28d" dependencies = [ "as_variant", "cfg-if", @@ -5839,21 +5847,6 @@ dependencies = [ "toml", ] -[[package]] -name = "ruma-signatures" -version = "0.19.0" -source = "git+https://github.com/project-robius/ruma.git?branch=tsp#7028b7fbf1351a092328ec250da86beed885a681" -dependencies = [ - "base64", - "ed25519-dalek", - "pkcs8", - "rand 0.8.5", - "ruma-common", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.17", -] - [[package]] name = "rusqlite" version = "0.37.0" @@ -5904,9 +5897,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -5920,11 +5913,11 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.5.0", @@ -5941,14 +5934,41 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.1", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.6" @@ -5970,12 +5990,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", - "bytemuck 1.25.0 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "bytemuck 1.25.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis)", + "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -6070,7 +6090,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "sealed" @@ -6259,9 +6279,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -6317,7 +6337,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -6328,7 +6348,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -6339,7 +6359,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.11.0-rc.4", ] @@ -6380,7 +6400,7 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "simd-adler32" @@ -6421,7 +6441,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "socket2" @@ -6654,25 +6674,24 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "string_cache" -version = "0.8.9" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ "phf_generator", - "phf_shared 0.11.3", + "phf_shared", "proc-macro2", "quote", ] @@ -6778,12 +6797,11 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] @@ -6996,15 +7014,15 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -7016,6 +7034,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.23.4" @@ -7023,18 +7050,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ "indexmap 2.13.0", - "toml_datetime", + "toml_datetime 0.7.3", "toml_parser", - "winnow", + "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.1", ] [[package]] @@ -7178,7 +7205,7 @@ dependencies = [ "quinn", "rand 0.8.5", "rand_core 0.6.4", - "reqwest", + "reqwest 0.12.28", "rustls", "rustls-native-certs", "rustls-pemfile", @@ -7201,7 +7228,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "tungstenite" @@ -7233,15 +7260,6 @@ name = "typewit" version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" [[package]] name = "uds_windows" @@ -7273,7 +7291,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-bidi" @@ -7284,17 +7302,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-ident" @@ -7305,7 +7323,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-normalization" @@ -7325,12 +7343,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-segmentation" @@ -7341,7 +7359,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "unicode-width" @@ -7530,7 +7548,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -7541,9 +7568,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.107" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1310980282a2842658e512a8bd683c962bbf9395e0544fa7bc0509343b8f7d10" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -7554,23 +7581,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.57" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de050049980fd9bee908eebfcdc8fa78dddb59acdbe7cbcc5b523a93c9fe0a4e" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.107" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83321b348310f762bebefa30cd9504f673f3b554a53755eaa93af8272d28f7b" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7578,9 +7601,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.107" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6971fd7d06a3063afaaf6b843a2b2b16c3d84b42f4e2ec4e0c8deafbcb179708" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -7591,13 +7614,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.107" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54d2e1dc11b30bef0c334a34e7c7a1ed57cff1b602ad7eb6e5595e2e1e60bd62" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -7611,6 +7656,19 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm_evt_listener" version = "0.1.0" @@ -7629,10 +7687,22 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "downcast-rs", "libc", @@ -7658,7 +7728,7 @@ dependencies = [ [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", @@ -7680,7 +7750,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "wayland-backend 0.3.12", "wayland-sys 0.31.8", @@ -7689,7 +7759,7 @@ dependencies = [ [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "wayland-backend 0.3.12", @@ -7722,7 +7792,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "log", "pkg-config", @@ -7741,9 +7811,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.84" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1803a5757552f43190297bc8351e32442341c064b940983d29ac94a0b957577" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -7762,21 +7832,30 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.3" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" dependencies = [ - "phf 0.11.3", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -7799,9 +7878,9 @@ dependencies = [ [[package]] name = "wildmatch" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" +checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" [[package]] name = "winapi-util" @@ -7838,7 +7917,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -7857,7 +7936,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-core 0.62.2", ] @@ -7890,7 +7969,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -7911,7 +7990,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-core 0.62.2", ] @@ -7975,7 +8054,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" [[package]] name = "windows-numerics" @@ -8019,7 +8098,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-link 0.2.1", ] @@ -8036,7 +8115,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=text_flow_ellipsis#c733120c2afebdb2d34944ca8353ad9f0d9915a8" +source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" dependencies = [ "windows-link 0.2.1", ] @@ -8356,12 +8435,106 @@ dependencies = [ "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" + [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.106", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.106", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.1" @@ -8445,7 +8618,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.1", - "winnow", + "winnow 0.7.13", "zbus_macros", "zbus_names", "zvariant", @@ -8473,7 +8646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "winnow", + "winnow 0.7.13", "zvariant", ] @@ -8611,7 +8784,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow", + "winnow 0.7.13", "zvariant_derive", "zvariant_utils", ] @@ -8639,5 +8812,5 @@ dependencies = [ "quote", "serde", "syn 2.0.106", - "winnow", + "winnow 0.7.13", ] diff --git a/Cargo.toml b/Cargo.toml index a8597c24b..662adcd15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" # 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 = "text_flow_ellipsis", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "text_flow_ellipsis" } +makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "cargo_makepad_ndk_fix", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "cargo_makepad_ndk_fix" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. @@ -48,8 +48,8 @@ imghdr = "0.7.0" linkify = "0.10.0" mime = "0.3" mime_guess = "2.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 = [ +matrix-sdk-base = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested" } +matrix-sdk = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested", default-features = false, features = [ "e2e-encryption", "automatic-room-key-forwarding", "markdown", @@ -58,7 +58,7 @@ matrix-sdk = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = " "bundled-sqlite", "sso-login", ] } -matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch = "main", default-features = false, features = [ +matrix-sdk-ui = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested", default-features = false, features = [ "rustls-tls", ] } ## Use the same ruma version as what's specified in matrix-sdk's Cargo.toml. @@ -66,7 +66,7 @@ matrix-sdk-ui = { git = "https://github.com/matrix-org/matrix-rust-sdk", branch ## * "compat-optional" feature to allow missing body field in m.room.tombstone event. ## * "compat-unset-avatar" feature to allow deleting the user's avatar to work properly. ## * Note: we need a feature like "compat-unset-display-name" to unset display names, but that doesn't exist yet. -ruma = { version = "0.14.1", features = [ +ruma = { git = "https://github.com/ruma/ruma", rev = "a0acf4187a7c7557d145db54bcb23b01f6295ce7", features = [ "compat-optional", "compat-unset-avatar", ] } @@ -93,13 +93,14 @@ tsp_sdk = { git = "https://github.com/openwallet-foundation-labs/tsp.git", rev = "resolve", ] } quinn = { version = "0.11", default-features = false, optional = true } -## We only include this such that we can specify the prebuilt-nasm features, +## We only include this such that we can specify the prebuilt-nasm feature, ## which is required to build this on Windows x86_64 without having to install NASM separately. aws-lc-rs = { version = "1.13", optional = true, features = ["prebuilt-nasm"] } percent-encoding = { version = "2.3", optional = true } -## The following reqwest features were taken from the tsp_sdk's `Cargo.toml` file. -## I'm not sure if all of them are actually needed. -reqwest = { version = "0.12", default-features = false, features = [ +## The following reqwest 0.12 features were taken from the tsp_sdk's `Cargo.toml` file. +## This is only needed for the optional TSP feature; the matrix-sdk uses reqwest 0.13 internally, +## which is re-exported as `matrix_sdk::reqwest` and used by regular (non-TSP) Robrix code. +reqwest = { version = "0.12", default-features = false, optional = true, features = [ "rustls-tls-native-roots", "json", "stream", @@ -112,7 +113,7 @@ reqwest = { version = "0.12", default-features = false, features = [ [features] default = [] ## Enables experimental support for using TSP wallets. -tsp = ["dep:tsp_sdk", "dep:quinn", "dep:aws-lc-rs", "dep:percent-encoding"] +tsp = ["dep:tsp_sdk", "dep:quinn", "dep:aws-lc-rs", "dep:percent-encoding", "dep:reqwest"] ## Hides the command prompt console on Windows. hide_windows_console = [] @@ -158,6 +159,9 @@ askar-storage = { git = "https://github.com/openwallet-foundation/askar.git" } ## and then we won't need to patch ruma-events anymore. ## But that is a significant amount of work, so for now we just patch ruma-events. ## +## Note: matrix-sdk currently uses a git dependency for ruma, +## so we patch the git source, not crates-io. +[patch."https://github.com/ruma/ruma"] ruma = { git = "https://github.com/project-robius/ruma.git", branch = "tsp" } diff --git a/README.md b/README.md index 31e219b12..cb1c8d6ca 100644 --- a/README.md +++ b/README.md @@ -52,16 +52,21 @@ The following table shows which host systems can currently be used to build Robr ## Building & Running Robrix on Desktop 1. First, [install Rust](https://www.rust-lang.org/tools/install). -2. If you're building on **Linux** or **WSL** on Windows, install the required dependencies. Otherwise, proceed to step 3. - * `openssl`, `clang`/`libclang`, `binfmt`, `Xcursor`/`X11`, `asound`/`pulse`. +2. Install `cmake`, which is required for some Matrix SDK dependencies. + * macOS: `brew install cmake` + * Windows: `choco install cmake` (or install `cmake` using Visual Studio) + * Linux: see step 3 below. + +3. If you're building on **Linux** or **WSL** on Windows, install the required dependencies. Otherwise, proceed to step 4. + * `openssl`, `clang`/`libclang`, `cmake`, `binfmt`, `Xcursor`/`X11`, `asound`/`pulse`. On a Debian-like Linux distro (e.g., Ubuntu), run the following: ```sh sudo apt-get update - sudo apt-get install libssl-dev libsqlite3-dev pkg-config binfmt-support libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + sudo apt-get install libssl-dev cmake llvm clang libclang-dev libsqlite3-dev pkg-config binfmt-support libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev ``` -3. Then, build and run Robrix. +4. Then, build and run Robrix. ```sh cargo run --release ``` @@ -74,9 +79,9 @@ The following table shows which host systems can currently be used to build Robr ``` ### Android -2. Use `cargo-makepad` to install the Android toolchain: +2. Use `cargo-makepad` to install the Android toolchain with the full NDK included: ```sh - cargo makepad android install-toolchain + cargo makepad android install-toolchain --full-ndk ``` 3. Build and run Robrix using `cargo-makepad`: diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ad4d53747..d91de0838 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -538,6 +538,7 @@ "space_lobby.header.button.invite": "Invite", "space_lobby.status.loading_rooms_spaces": "Loading rooms and spaces...", "space_lobby.status.no_rooms_spaces": "No rooms or spaces found.", + "space_lobby.status.no_matching_rooms_spaces": "No matching rooms or spaces.", "space_lobby.status.loading": "Loading...", "space_lobby.item.button.join": "Join", "space_lobby.item.button.view": "View", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index dba930b02..8c77b7696 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -536,6 +536,7 @@ "space_lobby.header.button.invite": "邀请", "space_lobby.status.loading_rooms_spaces": "正在加载房间和空间...", "space_lobby.status.no_rooms_spaces": "未找到房间或空间。", + "space_lobby.status.no_matching_rooms_spaces": "未找到匹配的房间或空间。", "space_lobby.status.loading": "加载中...", "space_lobby.item.button.join": "加入", "space_lobby.item.button.view": "查看", diff --git a/src/app.rs b/src/app.rs index 937b15563..92fedfa48 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,7 +15,7 @@ use crate::{ event_source_modal::{EventSourceModalAction, EventSourceModalWidgetRefExt}, invite_modal::{InviteModalAction, InviteModalWidgetRefExt, mark_invite_modal_closed}, 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, TimelineUpdate, clear_timeline_states}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ + }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, file_upload_modal::{FilePreviewerAction, FileUploadModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::FilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, AccountSwitchAction, current_user_id, submit_async_request, get_timeline_update_sender}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -105,7 +105,7 @@ script_mod! { right: (mod.widgets.SAFE_INSET_PAD_RIGHT), } - View { + overlay_container := View { width: Fill, height: Fill, flow: Overlay, @@ -858,7 +858,7 @@ impl MatchEvent for App { // which will open the login_status_modal to show the failure message. } - if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { + if let FilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { cx.stop_timer(self.room_filter_debounce_timer); self.pending_room_filter_keywords = keywords.clone(); self.room_filter_debounce_timer = cx.start_timeout(0.12); @@ -940,10 +940,11 @@ impl MatchEvent for App { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); let expected_dimensions = new_message_context_menu.show(cx, details, self.app_state.app_language); - // Ensure the context menu does not spill over the window's bounds. - let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); - let pos_x = min(abs_pos.x, rect.size.x - expected_dimensions.x); - let pos_y = min(abs_pos.y, rect.size.y - expected_dimensions.y); + // Use the overlay container's rect (not the window's) to correctly position + // the context menu relative to the body area, which excludes the caption bar. + let rect = self.ui.view(cx, ids!(overlay_container)).area().rect(cx); + let pos_x = min(abs_pos.x - rect.pos.x, rect.size.x - expected_dimensions.x); + let pos_y = min(abs_pos.y - rect.pos.y, rect.size.y - expected_dimensions.y); let margin = Inset { left: pos_x as f64, top: pos_y as f64, @@ -963,10 +964,11 @@ impl MatchEvent for App { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); let expected_dimensions = room_context_menu.show(cx, details, self.app_state.app_language); - // Ensure the context menu does not spill over the window's bounds. - let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); - let pos_x = min(pos.x, rect.size.x - expected_dimensions.x); - let pos_y = min(pos.y, rect.size.y - expected_dimensions.y); + // Use the overlay container's rect (not the window's) to correctly position + // the context menu relative to the body area, which excludes the caption bar. + let rect = self.ui.view(cx, ids!(overlay_container)).area().rect(cx); + let pos_x = min(pos.x - rect.pos.x, rect.size.x - expected_dimensions.x); + let pos_y = min(pos.y - rect.pos.y, rect.size.y - expected_dimensions.y); let margin = Inset { left: pos_x as f64, top: pos_y as f64, diff --git a/src/event_preview.rs b/src/event_preview.rs index d4e0cde25..949f98ebc 100644 --- a/src/event_preview.rs +++ b/src/event_preview.rs @@ -7,9 +7,9 @@ use std::borrow::Cow; -use matrix_sdk::{ruma::{OwnedUserId, events::{room::{guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, message::{MessageFormat, MessageType}}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent}, serde::Raw, UserId}}; +use matrix_sdk::{ruma::{OwnedUserId, events::{room::{guest_access::GuestAccess, history_visibility::HistoryVisibility, join_rules::JoinRule, message::{MessageFormat, MessageType}}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, StateEventContentChange, SyncMessageLikeEvent}, serde::Raw, UserId}}; use matrix_sdk_base::crypto::types::events::UtdCause; -use matrix_sdk_ui::timeline::{self, AnyOtherFullStateEventContent, EncryptedMessage, EventTimelineItem, MemberProfileChange, MembershipChange, MsgLikeKind, OtherMessageLike, RoomMembershipChange, TimelineItemContent}; +use matrix_sdk_ui::timeline::{self, AnyOtherStateEventContentChange, EncryptedMessage, EventTimelineItem, MemberProfileChange, MembershipChange, MsgLikeKind, OtherMessageLike, RoomMembershipChange, TimelineItemContent}; use crate::utils; @@ -94,6 +94,10 @@ pub fn text_preview_of_timeline_item( preview } MsgLikeKind::UnableToDecrypt(em) => text_preview_of_encrypted_message(em), + MsgLikeKind::LiveLocation(_) => TextPreview::from(( + String::from("[Live Location]"), + BeforeText::UsernameWithColon, + )), MsgLikeKind::Other(oml) => text_preview_of_other_message_like(oml), } } @@ -165,6 +169,9 @@ pub fn plaintext_body_of_timeline_item( text_preview_of_encrypted_message(em) .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false) } + MsgLikeKind::LiveLocation(_) => { + String::from("[Live Location]") + } MsgLikeKind::Other(other_msg_like) => { text_preview_of_other_message_like(other_msg_like) .format_with(&utils::get_or_fetch_event_sender(event_tl_item, None), false)} @@ -442,7 +449,7 @@ pub fn text_preview_of_other_state( format_as_html: bool, ) -> Option { let text = match other_state.content() { - AnyOtherFullStateEventContent::RoomAliases(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomAliases(StateEventContentChange::Original { content, .. }) => { let mut s = String::from("set this room's aliases to "); let last_alias = content.aliases.len() - 1; for (i, alias) in content.aliases.iter().enumerate() { @@ -454,28 +461,28 @@ pub fn text_preview_of_other_state( s.push('.'); Some(s) } - AnyOtherFullStateEventContent::RoomAvatar(_) => { + AnyOtherStateEventContentChange::RoomAvatar(_) => { Some(String::from("set this room's avatar picture.")) } - AnyOtherFullStateEventContent::RoomCanonicalAlias(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomCanonicalAlias(StateEventContentChange::Original { content, .. }) => { Some(format!("set the main address of this room to {}.", content.alias.as_ref().map(|a| a.as_str()).unwrap_or("none") )) } - AnyOtherFullStateEventContent::RoomCreate(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomCreate(StateEventContentChange::Original { content, .. }) => { Some(format!("created this room (v{}).", content.room_version.as_str())) } - AnyOtherFullStateEventContent::RoomEncryption(_) => { + AnyOtherStateEventContentChange::RoomEncryption(_) => { Some(String::from("enabled encryption in this room.")) } - AnyOtherFullStateEventContent::RoomGuestAccess(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomGuestAccess(StateEventContentChange::Original { content, .. }) => { Some(match &content.guest_access { GuestAccess::CanJoin => String::from("has allowed guests to join this room."), GuestAccess::Forbidden => String::from("has forbidden guests from joining this room."), custom => format!("has set custom guest access rules for this room: {}", custom.as_str()), }) } - AnyOtherFullStateEventContent::RoomHistoryVisibility(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomHistoryVisibility(StateEventContentChange::Original { content, .. }) => { Some(format!("set this room's history to be visible by {}", match &content.history_visibility { HistoryVisibility::Invited => "invited users, since they were invited.", @@ -486,7 +493,7 @@ pub fn text_preview_of_other_state( }, )) } - AnyOtherFullStateEventContent::RoomJoinRules(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomJoinRules(StateEventContentChange::Original { content, .. }) => { Some(match &content.join_rule { JoinRule::Public => String::from("set this room to be joinable by anyone."), JoinRule::Knock => String::from("set this room to be joinable by invite only or by request."), @@ -497,10 +504,10 @@ pub fn text_preview_of_other_state( custom => format!("set custom join rules for this room: {}", custom.as_str()), }) } - AnyOtherFullStateEventContent::RoomPinnedEvents(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomPinnedEvents(StateEventContentChange::Original { content, .. }) => { Some(format!("pinned {} events in this room.", content.pinned.len())) } - AnyOtherFullStateEventContent::RoomName(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomName(StateEventContentChange::Original { content, .. }) => { let name = if format_as_html { htmlize::escape_text(&content.name) } else { @@ -508,16 +515,16 @@ pub fn text_preview_of_other_state( }; Some(format!("changed this room's name to \"{name}\".")) } - AnyOtherFullStateEventContent::RoomPowerLevels(_) => { + AnyOtherStateEventContentChange::RoomPowerLevels(_) => { Some(String::from("set the power levels for this room.")) } - AnyOtherFullStateEventContent::RoomServerAcl(_) => { + AnyOtherStateEventContentChange::RoomServerAcl(_) => { Some(String::from("set the server access control list for this room.")) } - AnyOtherFullStateEventContent::RoomTombstone(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomTombstone(StateEventContentChange::Original { content, .. }) => { Some(format!("closed this room and upgraded it to {}", content.replacement_room.matrix_to_uri())) } - AnyOtherFullStateEventContent::RoomTopic(FullStateEventContent::Original { content, .. }) => { + AnyOtherStateEventContentChange::RoomTopic(StateEventContentChange::Original { content, .. }) => { let topic = if format_as_html { htmlize::escape_text(&content.topic) } else { @@ -525,7 +532,7 @@ pub fn text_preview_of_other_state( }; Some(format!("changed this room's topic to \"{topic}\".")) } - AnyOtherFullStateEventContent::SpaceParent(_) => { + AnyOtherStateEventContentChange::SpaceParent(_) => { let state_key = if format_as_html { htmlize::escape_text(other_state.state_key()) } else { @@ -533,7 +540,7 @@ pub fn text_preview_of_other_state( }; Some(format!("set this room's parent space to \"{state_key}\".")) } - AnyOtherFullStateEventContent::SpaceChild(_) => { + AnyOtherStateEventContentChange::SpaceChild(_) => { let state_key = if format_as_html { htmlize::escape_text(other_state.state_key()) } else { diff --git a/src/home/edited_indicator.rs b/src/home/edited_indicator.rs index 07fb24f0d..bc1933263 100644 --- a/src/home/edited_indicator.rs +++ b/src/home/edited_indicator.rs @@ -4,7 +4,7 @@ //! with an underline to indicate that it is clickable. //! Upon hover, it shows a tooltip with the date and time when the message was edited. //! -//! On click, this widget opens a scrollabel modal dialog that shows the full edit history +//! On click, this widget opens a scrollable modal dialog that shows the full edit history //! of the message, including all previous content versions and their timestamps. use chrono::{DateTime, Local}; diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index a0260386b..bb7cb03bb 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,6 +1,11 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; +use crate::{ + app::AppState, + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, + settings::settings_screen::SettingsScreenWidgetRefExt, + shared::room_filter_input_bar::{MainFilterAction, RoomFilterInputBarWidgetExt}, +}; script_mod! { use mod.prelude.widgets.* @@ -415,6 +420,14 @@ pub struct HomeScreen { impl Widget for HomeScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { + // On desktop, the RoomFilterInputBar is inside this HomeScreen. + // Check if it changed and re-emit as a MainFilterAction so that + // RoomsList and SpacesBar can respond without cross-talk from + // other RoomFilterInputBar instances (e.g., SpaceLobbyScreen's). + if let Some(keywords) = self.view.room_filter_input_bar(cx, ids!(room_filter_input_bar)).changed(actions) { + cx.action(MainFilterAction::Changed(keywords)); + } + let app_state = scope.data.get_mut::().unwrap(); for action in actions { match action.downcast_ref() { diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index e5d08be64..f15d72782 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -21,7 +21,7 @@ use matrix_sdk::{ } }; use matrix_sdk_ui::timeline::{ - self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem + self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, LiveLocationState, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem }; use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; @@ -3901,6 +3901,16 @@ impl Widget for RoomScreen { utd, item_drawn_status, ), + MsgLikeKind::LiveLocation(live_loc) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + app_language, + event_tl_item, + live_loc, + item_drawn_status, + ), MsgLikeKind::Other(other) => populate_small_state_event( cx, list, @@ -8105,6 +8115,27 @@ impl SmallStateEventContent for EncryptedMessage { } // For other message-like content (custom message-like events). +impl SmallStateEventContent for LiveLocationState { + fn populate_item_content( + &self, + cx: &mut Cx, + _list: &mut PortalList, + _item_id: usize, + item: WidgetRef, + _event_tl_item: &EventTimelineItem, + username: &str, + _item_drawn_status: ItemDrawnStatus, + mut new_drawn_status: ItemDrawnStatus, + ) -> (WidgetRef, ItemDrawnStatus) { + item.label(cx, ids!(content)).set_text( + cx, + &format!("{username} shared a live location."), + ); + new_drawn_status.content_drawn = true; + (item, new_drawn_status) + } +} + impl SmallStateEventContent for OtherMessageLike { fn populate_item_content( &self, diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index ba1e167b1..e482b6ab3 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -40,7 +40,7 @@ use crate::{ collapsible_header::{CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory}, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification}, - room_filter_input_bar::RoomFilterAction, + room_filter_input_bar::MainFilterAction, }, logout::logout_confirm_modal::LogoutAction, sliding_sync::{MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request}, @@ -1425,7 +1425,9 @@ impl Widget for RoomsList { continue; } - if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { + // Only handle filter changes from the home screen's filter bar, + // not from any other RoomFilterInputBar instance (e.g., SpaceLobbyScreen's). + if let Some(MainFilterAction::Changed(keywords)) = action.downcast_ref() { self.regenerate_display_filter_and_sort_fn(keywords); self.update_displayed_rooms(cx, true); continue; diff --git a/src/home/rooms_sidebar.rs b/src/home/rooms_sidebar.rs index 9f93bc071..5d2637ae2 100644 --- a/src/home/rooms_sidebar.rs +++ b/src/home/rooms_sidebar.rs @@ -10,6 +10,7 @@ use makepad_widgets::*; use crate::home::rooms_list::RoomsListWidgetExt; +use crate::shared::room_filter_input_bar::{MainFilterAction, RoomFilterInputBarWidgetExt}; script_mod! { use mod.prelude.widgets.* @@ -152,6 +153,13 @@ impl ScriptHook for RoomsSideBar { } impl Widget for RoomsSideBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + // If the main room filter input bar changed keywords, re-emit that action + // as a MainFilterAction so that other widgets can handle it. + if let Event::Actions(actions) = event { + if let Some(keywords) = self.view.room_filter_input_bar(cx, ids!(room_filter_input_bar)).changed(actions) { + cx.action(MainFilterAction::Changed(keywords)); + } + } self.view.handle_event(cx, event, scope); } diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index 385a9bb15..7d90862bd 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -13,7 +13,7 @@ use makepad_widgets::*; use makepad_widgets::animator::Animate; use matrix_sdk::{RoomDisplayName, RoomState, ruma::OwnedRoomId}; use matrix_sdk_ui::spaces::SpaceRoom; -use ruma::room::JoinRuleSummary; +use ruma::{OwnedRoomAliasId, room::JoinRuleSummary}; use tokio::sync::mpsc::UnboundedSender; use crate::shared::avatar::AvatarState; use crate::shared::expand_arrow::ExpandArrow; @@ -32,7 +32,10 @@ use crate::{ i18n::{AppLanguage, tr_fmt, tr_key}, join_leave_room_modal::{JoinLeaveModalKind, JoinLeaveRoomModalAction}, room::BasicRoomDetails, - shared::avatar::{AvatarWidgetExt, AvatarWidgetRefExt}, + shared::{ + avatar::{AvatarWidgetExt, AvatarWidgetRefExt}, + room_filter_input_bar::RoomFilterInputBarWidgetExt, + }, space_service_sync::{SpaceRequest, SpaceRoomExt, SpaceRoomListAction}, utils::{self, RoomNameId}, }; @@ -313,6 +316,27 @@ script_mod! { text_overflow: Ellipsis draw_text +: { text_style: REGULAR_TEXT {font_size: 10.5}, color: #1a1a1a } } + + suggested_tag := RoundedView { + visible: false + width: Fit, height: Fit, + padding: Inset { left: 6, right: 6, top: 3, bottom: 3 } + show_bg: true + draw_bg +: { + color: #E8F4FD + border_radius: 3.0 + border_size: 0.75 + border_color: (COLOR_INFO_BLUE) + } + suggested_label := Label { + padding: 0 + margin: 0 + width: Fit, height: Fit, + text: "Suggested" + draw_text +: { text_style: REGULAR_TEXT {font_size: 8.5}, color: (COLOR_INFO_BLUE) } + } + } + info_label := Label { width: Fill, height: Fit, margin: 0 @@ -443,7 +467,7 @@ script_mod! { loading_spinner := LoadingSpinner { width: 14, height: 14, - margin: Inset{left: 10, right: 6} + margin: Inset{left: 10, right: 4} draw_bg +: { color: (COLOR_ACTIVE_PRIMARY) border_size: 2.0 @@ -507,14 +531,30 @@ script_mod! { show_bg: true, draw_bg.color: (COLOR_BG_PREVIEW) - space_info_label := Label { + space_info_row := View { width: Fill, height: Fit, - flow: Right, // do not wrap - margin: Inset{left: 2} - draw_text +: { - text_style: REGULAR_TEXT {font_size: 10}, - color: #737373, + flow: Right, + spacing: 10, + align: Align { y: 0.5 } + + space_info_label := Label { + width: Fit, + height: Fit, + flow: Right, // do not wrap + margin: Inset{left: 2} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10}, + color: #737373, + } + text: "Welcome to the space:" + } + + // Filter input bar for searching rooms/spaces in this space + filter_bar := mod.widgets.RoomFilterInputBar { + input +: { + empty_text: "Filter this space..." + } } text: "" } @@ -855,6 +895,7 @@ impl Widget for SubspaceEntry { struct SpaceRoomInfo { id: OwnedRoomId, name: String, + canonical_alias: Option, topic: Option, avatar: AvatarState, num_joined_members: u64, @@ -863,6 +904,8 @@ struct SpaceRoomInfo { join_rule: Option, /// If `Some`, this is a space. If `None`, it's a room. children_count: Option, + /// Whether the room is suggested by the space administrators. + suggested: bool, } impl SpaceRoomInfo { fn is_space(&self) -> bool { @@ -874,6 +917,7 @@ impl From<&SpaceRoom> for SpaceRoomInfo { SpaceRoomInfo { id: space_room.room_id.clone(), name: space_room.display_name.clone(), + canonical_alias: space_room.canonical_alias.clone(), topic: space_room.topic.as_ref().map(|t| { replace_linebreaks_separators(t.trim(), false).into_owned() }), @@ -882,6 +926,7 @@ impl From<&SpaceRoom> for SpaceRoomInfo { state: space_room.state, join_rule: space_room.join_rule.clone(), children_count: space_room.is_space().then_some(space_room.children_count), + suggested: space_room.suggested, } } } @@ -889,6 +934,7 @@ impl From for SpaceRoomInfo { fn from(space_room: SpaceRoom) -> Self { SpaceRoomInfo { children_count: space_room.is_space().then_some(space_room.children_count), + canonical_alias: space_room.canonical_alias, id: space_room.room_id, name: space_room.display_name, topic: space_room.topic.map(|t| { @@ -898,6 +944,7 @@ impl From for SpaceRoomInfo { num_joined_members: space_room.num_joined_members, state: space_room.state, join_rule: space_room.join_rule, + suggested: space_room.suggested, } } } @@ -956,6 +1003,9 @@ pub struct SpaceLobbyScreen { #[rust] top_level_join_rule: Option, #[rust] top_level_member_count: Option, #[rust] app_language: AppLanguage, + + /// The current filter keywords entered by the user, if any. + #[rust] filter_keywords: String, } impl Widget for SpaceLobbyScreen { @@ -1083,6 +1133,16 @@ impl Widget for SpaceLobbyScreen { cx.action(InviteModalAction::Open(space_name_id.clone())); } } + + // Handle changes to this screen's own filter input bar. + if let Some(keywords) = self.view.room_filter_input_bar(cx, ids!(filter_bar)).changed(actions) { + self.filter_keywords = keywords; + self.rebuild_tree_entries(); + // Reset scroll to the top when filter changes. + let portal_list = self.view.portal_list(cx, ids!(tree_list)); + portal_list.set_first_id_and_scroll(0, 0.0); + self.redraw(cx); + } } } @@ -1143,10 +1203,12 @@ impl Widget for SpaceLobbyScreen { // No entries found else if entry_count == 0 && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); - item.child_by_path(ids!(label)).as_label().set_text( - cx, - tr_key(app_language, "space_lobby.status.no_rooms_spaces"), - ); + let msg = if self.filter_keywords.is_empty() { + tr_key(app_language, "space_lobby.status.no_rooms_spaces") + } else { + tr_key(app_language, "space_lobby.status.no_matching_rooms_spaces") + }; + item.child_by_path(ids!(label)).as_label().set_text(cx, msg); item.child_by_path(ids!(loading_spinner)).set_visible(cx, false); item } @@ -1266,6 +1328,12 @@ impl Widget for SpaceLobbyScreen { spacer.walk.width = Size::Fixed(indent_pixel); } + // Show "Suggested" tag if recommended and not already joined + let show_suggested = info.suggested + && !matches!(info.state, Some(RoomState::Joined)); + item.child_by_path(ids!(main_entry.content.suggested_tag)) + .set_visible(cx, show_suggested); + // Build the info label with join status, member count, and topic // Note: Public/Private is intentionally not shown per-item to reduce clutter let info_label = item.child_by_path(ids!(main_entry.content.info_label)).as_label(); @@ -1380,7 +1448,7 @@ impl SpaceLobbyScreen { } else { String::new() }; - self.view.label(cx, ids!(header.space_info_label)).set_text(cx, &text); + self.view.label(cx, ids!(header.space_info_row.space_info_label)).set_text(cx, &text); } fn insert_created_room_placeholder(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { @@ -1414,6 +1482,7 @@ impl SpaceLobbyScreen { is_direct: Some(false), children_count: 0, state: Some(RoomState::Joined), + suggested: false, heroes: None, via: Vec::new(), }); @@ -1469,24 +1538,213 @@ impl SpaceLobbyScreen { self.redraw(cx); } - /// Rebuild the flattened tree entries based on the current expansion state. + /// Rebuild the flattened tree entries based on the current expansion state, + /// and then apply the current filter keywords (if any). fn rebuild_tree_entries(&mut self) { let Some(space_name_id) = &self.space_name_id else { return }; let root_space_id = space_name_id.room_id().clone(); - // Build tree starting from root let mut new_tree_entries = Vec::new(); - Self::build_tree_for_space( - &self.children_cache, - &self.expanded_spaces, - &self.loading_subspaces, - &mut new_tree_entries, - &root_space_id, - 0, - 0, - ); + + if self.filter_keywords.is_empty() { + // No filter: build tree respecting expansion state. + Self::build_tree_for_space( + &self.children_cache, + &self.expanded_spaces, + &self.loading_subspaces, + &mut new_tree_entries, + &root_space_id, + 0, + 0, + ); + } else { + // Filter active: build tree showing all matching entries + // plus their ancestor spaces to preserve hierarchy context. + let kw = self.filter_keywords.to_lowercase(); + Self::build_filtered_tree( + &self.children_cache, + &mut new_tree_entries, + &root_space_id, + &kw, + 0, + 0, + ); + } + self.tree_entries = new_tree_entries; } + /// Returns whether the given [`SpaceRoomInfo`] matches the filter keywords. + fn matches_filter(info: &SpaceRoomInfo, keywords: &str) -> bool { + info.name.to_lowercase().contains(keywords) + || info.id.as_str().to_lowercase().contains(keywords) + || info.canonical_alias.as_ref() + .is_some_and(|a| a.as_str().to_lowercase().contains(keywords)) + || info.topic.as_ref() + .is_some_and(|t| t.to_lowercase().contains(keywords)) + } + + /// Recursively build a filtered tree that includes only entries matching + /// the keywords, plus any ancestor spaces needed to preserve the hierarchy. + /// + /// Returns `true` if any matching entry was added within this subtree. + fn build_filtered_tree( + children_cache: &HashMap>, + tree_entries: &mut Vec, + space_id: &OwnedRoomId, + keywords: &str, + level: usize, + parent_mask: u32, + ) -> bool { + let Some(children) = children_cache.get(space_id) else { return false }; + + // Sort identically to the unfiltered tree: spaces first, then rooms, both alphabetically. + let mut sorted_children: Vec<_> = children.iter().collect(); + sorted_children.sort_by(|a, b| { + match (a.is_space(), b.is_space()) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()), + } + }); + + // First pass: determine which children have matches (self or descendants) + // so we can correctly compute `is_last` for tree line drawing. + let matched_indices: Vec = sorted_children.iter().enumerate().filter_map(|(i, child)| { + let info = SpaceRoomInfo::from(*child); + let self_matches = Self::matches_filter(&info, keywords); + let has_matching_descendants = child.is_space() + && children_cache.contains_key(&child.room_id) + && Self::subtree_has_match(children_cache, &child.room_id, keywords); + if self_matches || has_matching_descendants { + Some(i) + } else { + None + } + }).collect(); + + if matched_indices.is_empty() { + return false; + } + + // Second pass: emit entries for matched children, preserving hierarchy. + for (pos, &child_idx) in matched_indices.iter().enumerate() { + let child = sorted_children[child_idx]; + let is_last = pos == matched_indices.len() - 1; + let info = SpaceRoomInfo::from(child); + let self_matches = Self::matches_filter(&info, keywords); + + let child_mask = if is_last { + parent_mask + } else { + parent_mask | (1 << level) + }; + + if child.is_space() && children_cache.contains_key(&child.room_id) { + // For spaces: always include if self matches or descendants match. + tree_entries.push(TreeEntry::Item { + info, + level, + is_last, + parent_mask, + }); + // Recurse into child space: if the space itself matches, + // show ALL of its children (unfiltered); otherwise show only + // the matching descendants. + if self_matches { + // Show all children of a matching space (no further filtering). + Self::build_tree_for_space_ignoring_expansion( + children_cache, + tree_entries, + &child.room_id, + level + 1, + child_mask, + ); + } else { + // Space doesn't match, but some descendant does — recurse with filter. + Self::build_filtered_tree( + children_cache, + tree_entries, + &child.room_id, + keywords, + level + 1, + child_mask, + ); + } + } else if self_matches { + // Non-space room or space without cached children: include only if it matches. + tree_entries.push(TreeEntry::Item { + info, + level, + is_last, + parent_mask, + }); + } + } + + true + } + + /// Returns `true` if any entry in the subtree rooted at `space_id` matches the keywords. + fn subtree_has_match( + children_cache: &HashMap>, + space_id: &OwnedRoomId, + keywords: &str, + ) -> bool { + let Some(children) = children_cache.get(space_id) else { return false }; + children.iter().any(|child| { + let info = SpaceRoomInfo::from(child); + Self::matches_filter(&info, keywords) + || (child.is_space() && Self::subtree_has_match(children_cache, &child.room_id, keywords)) + }) + } + + /// Like [`build_tree_for_space`] but ignores expansion state — shows all children. + /// Used to display the full contents of a space that itself matched the filter. + fn build_tree_for_space_ignoring_expansion( + children_cache: &HashMap>, + tree_entries: &mut Vec, + space_id: &OwnedRoomId, + level: usize, + parent_mask: u32, + ) { + let Some(children) = children_cache.get(space_id) else { return }; + + let mut sorted_children: Vec<_> = children.iter().collect(); + sorted_children.sort_by(|a, b| { + match (a.is_space(), b.is_space()) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()), + } + }); + + let count = sorted_children.len(); + for (i, child) in sorted_children.into_iter().enumerate() { + let is_last = i == count - 1; + tree_entries.push(TreeEntry::Item { + info: SpaceRoomInfo::from(child), + level, + is_last, + parent_mask, + }); + + if child.is_space() && children_cache.contains_key(&child.room_id) { + let child_mask = if is_last { + parent_mask + } else { + parent_mask | (1 << level) + }; + Self::build_tree_for_space_ignoring_expansion( + children_cache, + tree_entries, + &child.room_id, + level + 1, + child_mask, + ); + } + } + } + /// Recursively build the tree of spaces and their expanded children such that they /// can be displayed in the SpaceLobbyScreen's PortalList. // @@ -1606,9 +1864,14 @@ impl SpaceLobbyScreen { self.tree_entries.clear(); self.top_level_join_rule = None; self.top_level_member_count = None; - self.view.label(cx, ids!(header.space_info_label)).set_text(cx, ""); + self.view.label(cx, ids!(header.space_info_row.space_info_label)).set_text(cx, ""); self.is_loading = true; + // Clear the filter bar when switching to a new space. + self.filter_keywords.clear(); + self.view.text_input(cx, ids!(filter_bar.input)).set_text(cx, ""); + self.view.button(cx, ids!(filter_bar.clear_button)).set_visible(cx, false); + // Restore UI state if we've viewed this space before, otherwise start fresh self.expanded_spaces = SPACE_LOBBY_STATES.with_borrow(|states| { states diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 2b3d56844..1ab310190 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -13,7 +13,7 @@ use matrix_sdk::{RoomDisplayName, RoomState}; use ruma::{OwnedRoomAliasId, OwnedRoomId, room::JoinRuleSummary}; use crate::{ - app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, i18n::{AppLanguage, tr_fmt, tr_key}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, sliding_sync::AccountSwitchAction, utils::{self, RoomNameId} + app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, i18n::{AppLanguage, tr_fmt, tr_key}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::MainFilterAction}, sliding_sync::AccountSwitchAction, utils::{self, RoomNameId} }; script_mod! { @@ -535,9 +535,10 @@ impl Widget for SpacesBar { continue; } - // The room filter input bar is also used to filter which spaces are visible. - if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast() { - self.update_displayed_spaces(cx, &keywords); + // Only handle filter changes from the home screen's filter bar, + // not from any other RoomFilterInputBar instance (e.g., SpaceLobbyScreen's). + if let Some(MainFilterAction::Changed(keywords)) = action.downcast_ref() { + self.update_displayed_spaces(cx, keywords); continue; } diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 42a7be961..4bcbb3380 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -11,9 +11,7 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* - mod.widgets.IMG_APP_LOGO = crate_resource("self://resources/robrix_logo_alpha.png") - mod.widgets.ICON_EYE_OPEN = crate_resource("self://resources/icons/eye_open.svg") mod.widgets.ICON_EYE_CLOSED = crate_resource("self://resources/icons/eye_closed.svg") @@ -53,39 +51,37 @@ script_mod! { show_bg: true, draw_bg +: { color: COLOR_SECONDARY - // color: COLOR_PRIMARY // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY } ScrollYView { width: Fill, height: Fill, - // Note: *do NOT* vertically center this, it will break scrolling. - align: Align{x: 0.5} + flow: Down, // Required for vertical scrolling to work. + align: Align{x: 0.5, y: 0.5} show_bg: true, draw_bg.color: (COLOR_SECONDARY) - // draw_bg.color: (COLOR_PRIMARY) // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY - + // allow the view to be scrollable but hide the actual scroll bar scroll_bars: { + show_scroll_x: false, show_scroll_y: true, scroll_bar_y: { bar_size: 0.0 min_handle_size: 0.0 + drag_scrolling: true } } - View { - margin: Inset{top: 40, bottom: 40} - width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` + RoundedView { + margin: Inset{top: 50, bottom: 50} + width: Fill height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, View { - width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` + width: Fill height: Fit flow: Down align: Align{x: 0.5, y: 0.5} - padding: Inset{top: 30, bottom: 30} - margin: Inset{top: 40, bottom: 40} spacing: 15.0 logo_image := Image { @@ -330,8 +326,6 @@ script_mod! { // The modal that pops up to display login status messages, // such as when the user is logging in or when there is an error. login_status_modal := Modal { - // width: Fit, height: Fit, - // align: Align{x: 0.5, y: 0.5}, can_dismiss: false, content +: { login_status_modal_inner := mod.widgets.LoginStatusModal {} diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 0e77af8ce..bc38f6a1e 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -18,6 +18,7 @@ use makepad_widgets::*; use matrix_sdk::room::reply::{EnforceThread, Reply}; +use ruma::events::room::message::AddMentions; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId}; use crate::{app::AppState, 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}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, 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}; @@ -1054,6 +1055,7 @@ impl RoomInputBar { Reply { event_id: event_id.to_owned(), enforce_thread, + add_mentions: AddMentions::Yes, } }) ).or_else(|| @@ -1061,6 +1063,7 @@ impl RoomInputBar { Reply { event_id: thread_root_event_id.clone(), enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + add_mentions: AddMentions::No, } ) ); @@ -1115,6 +1118,7 @@ impl RoomInputBar { Reply { event_id: event_id.to_owned(), enforce_thread, + add_mentions: AddMentions::Yes, } }) ).or_else(|| @@ -1122,6 +1126,7 @@ impl RoomInputBar { Reply { event_id: thread_root_event_id.clone(), enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + add_mentions: AddMentions::No, } ) ); @@ -1708,6 +1713,7 @@ impl RoomInputBarRef { event_tl_item.event_id().map(|event_id| Reply { event_id: event_id.to_owned(), enforce_thread: EnforceThread::MaybeThreaded, + add_mentions: ruma::events::room::message::AddMentions::Yes, }) }); diff --git a/src/shared/room_filter_input_bar.rs b/src/shared/room_filter_input_bar.rs index d89a7fed8..d948d52cd 100644 --- a/src/shared/room_filter_input_bar.rs +++ b/src/shared/room_filter_input_bar.rs @@ -1,8 +1,5 @@ -//! A text input used to filter the rooms list +//! A text input used to filter a list of rooms/spaces //! with a search icon and a button to clear the input. -//! -//! This is a dedicated widget instead of a general "SearchBar" so it can be -//! reused consistently across both Desktop and Mobile layouts. use makepad_widgets::*; use crate::{app::AppState, i18n::{AppLanguage, tr_key}}; @@ -55,6 +52,7 @@ script_mod! { clear_button := RobrixNeutralIconButton { visible: false, + margin: 0 padding: Inset{top: 5, bottom: 5, left: 9, right: 9}, spacing: 0, align: Align{x: 0.5, y: 0.5} @@ -64,7 +62,7 @@ script_mod! { } } -/// A text input (with a search icon and cancel button) used to filter the rooms list. +/// A text input (with a search icon and cancel button) used to filter a list of rooms/spaces. /// /// See the module-level docs for more detail. #[derive(Script, ScriptHook, Widget)] @@ -75,20 +73,32 @@ pub struct RoomFilterInputBar { /// Actions emitted by the `RoomFilterInputBar` based on user interaction with it. #[derive(Clone, Debug, Default)] -pub enum RoomFilterAction { +pub enum FilterAction { /// The user has changed the text entered into the filter bar. Changed(String), #[default] None, } -impl ActionDefaultRef for RoomFilterAction { +impl ActionDefaultRef for FilterAction { fn default_ref() -> &'static Self { - static DEFAULT: RoomFilterAction = RoomFilterAction::None; + static DEFAULT: FilterAction = FilterAction::None; &DEFAULT } } +/// An action emitted by the HomeScreen or RoomsSideBar when the keywords in the +/// main filter input bar (for rooms and spaces) changes. +/// +/// This is a separate action type from [`FilterAction`] so that consumers +/// like `RoomsList` and `SpacesBar` only react to filter changes from +/// the home screen's filter bar, ignoring other filter bar instances. +#[derive(Debug)] +pub enum MainFilterAction { + /// The user changed the home screen's filter text to the given keywords. + Changed(String), +} + impl Widget for RoomFilterInputBar { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { let app_language = scope.data.get::() @@ -112,6 +122,27 @@ impl Widget for RoomFilterInputBar { } } +impl RoomFilterInputBar { + /// Returns `Some(keywords)` if the filter text in this filter input bar + /// was changed in the given `actions`. + /// The returned keywords are already trimmed of whitespace. + pub fn changed(&self, actions: &Actions) -> Option { + if let Some(item) = actions.find_widget_action(self.widget_uid()) { + if let FilterAction::Changed(keywords) = item.cast() { + return Some(keywords); + } + } + None + } +} + +impl RoomFilterInputBarRef { + /// See [`RoomFilterInputBar::changed()`]. + pub fn changed(&self, actions: &Actions) -> Option { + self.borrow().and_then(|inner| inner.changed(actions)) + } +} + impl WidgetMatchEvent for RoomFilterInputBar { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { let input = self.text_input(cx, ids!(input)); @@ -130,7 +161,7 @@ impl WidgetMatchEvent for RoomFilterInputBar { clear_button.reset_hover(cx); cx.widget_action( self.widget_uid(), - RoomFilterAction::Changed(keywords) + FilterAction::Changed(keywords) ); } @@ -140,7 +171,7 @@ impl WidgetMatchEvent for RoomFilterInputBar { input.set_key_focus(cx); cx.widget_action( self.widget_uid(), - RoomFilterAction::Changed(String::new()) + FilterAction::Changed(String::new()) ); } } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index aa6b99a24..1395da7e3 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -525,7 +525,7 @@ pub type OnMediaFetchedFn = fn( #[derive(Debug)] pub enum UrlPreviewError { /// HTTP request failed. - Request(String), + Request(matrix_sdk::reqwest::Error), /// JSON parsing failed. Json(serde_json::Error), /// Client not available. @@ -2940,10 +2940,11 @@ async fn matrix_worker_task( }; let _pin_task = Handle::current().spawn(async move { + let room = timeline.room(); let result = if pin { - timeline.pin_event(&event_id).await + room.pin_event(&event_id).await } else { - timeline.unpin_event(&event_id).await + room.unpin_event(&event_id).await }; match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { Ok(_) => SignalToUI::set_ui_signal(), @@ -2990,33 +2991,29 @@ async fn matrix_worker_task( let token = client.access_token().ok_or(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") + let mut endpoint_url = client.homeserver().join("/_matrix/client/v1/media/preview_url") .map_err(UrlPreviewError::UrlParse)?; + endpoint_url.query_pairs_mut().append_pair("url", url.as_str()); // 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| { - UrlPreviewError::Request(e.to_string()) - })?; - + .map_err(UrlPreviewError::Request)?; + 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| { - UrlPreviewError::Request(e.to_string()) - })?; + + let text = response.text().await.map_err(UrlPreviewError::Request)?; // log!("URL preview response body length for {}: {} bytes", url, text.len()); // if text.len() > MAX_LOG_RESPONSE_BODY_LENGTH { @@ -4721,7 +4718,8 @@ fn handle_session_changes( Handle::current().spawn(async move { loop { match receiver.recv().await { - Ok(SessionChange::UnknownToken { soft_logout }) => { + Ok(SessionChange::UnknownToken(data)) => { + let soft_logout = data.soft_logout; let msg = if soft_logout { "Your login session has expired.\n\nPlease log in again." } else { @@ -5161,6 +5159,9 @@ async fn get_latest_event_details( ).format_with(&sender_username, true); Some((*timestamp, latest_message_text)) } + LatestEventValue::RemoteInvite { timestamp, .. } => { + Some((*timestamp, String::from("You were invited to this room."))) + } } } diff --git a/src/utils.rs b/src/utils.rs index 636053171..cba8b5758 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -423,8 +423,8 @@ pub fn stringify_pagination_error( return format!("Failed to load earlier messages in \"{room_name}\": \ pagination is not supported in this timeline focus mode."); } - TimelineError::PaginationError(PaginationError::Paginator(PaginatorError::SdkError(sdk_error))) - | TimelineError::EventCacheError(EventCacheError::BackpaginationError(sdk_error)) => + TimelineError::PaginationError(PaginationError::Pagination(PaginatorError::SdkError(sdk_error))) + | TimelineError::EventCacheError(EventCacheError::PaginationError(sdk_error)) => { if let Some(message) = match_sdk_error(sdk_error) { return message; From 0b6ed01f057ba1fe3e79f8dfafc9b6592550448d Mon Sep 17 00:00:00 2001 From: AlexZ Date: Wed, 8 Apr 2026 06:52:31 +0800 Subject: [PATCH 118/283] Fix CI: clippy unneeded_struct_pattern and iOS unused imports - Remove struct pattern from ErrorKind::Forbidden (now unit variant) - Gate FilePreviewerMetaData/ThumbnailData imports behind desktop cfg - Gate RefCell/ConfirmDeleteAction/ConfirmationModalContent behind desktop cfg Co-Authored-By: Claude Opus 4.6 (1M context) --- src/room/room_input_bar.rs | 4 +++- src/settings/account_settings.rs | 5 ++++- src/sliding_sync.rs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index bc38f6a1e..7f348dbb6 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -21,7 +21,9 @@ use matrix_sdk::room::reply::{EnforceThread, Reply}; use ruma::events::room::message::AddMentions; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId}; -use crate::{app::AppState, 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}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, 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}; +use crate::{app::AppState, 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}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, shared::{avatar::AvatarWidgetRefExt, file_upload_modal::{FileData, FileLoadedData, FilePreviewerAction}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; +#[cfg(not(any(target_os = "ios", target_os = "android")))] +use crate::shared::file_upload_modal::{FilePreviewerMetaData, ThumbnailData}; const ROOM_INFO_CARD_MOBILE_BREAKPOINT: f32 = 700.0; #[cfg(test)] diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index cd003dac5..78d3db37f 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; @@ -5,7 +6,9 @@ use makepad_widgets::{text::selection::Cursor, *}; use rfd::FileDialog; use matrix_sdk::ruma::OwnedUserId; -use crate::{account_manager, app::{AppState, ConfirmDeleteAction}, avatar_cache::{self}, home::navigation_tab_bar::get_own_profile, i18n::{AppLanguage, tr_fmt, tr_key}, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::{user_profile::UserProfile, user_profile_cache}, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, AccountSwitchAction, MatrixRequest, submit_async_request}, utils}; +use crate::{account_manager, app::AppState, avatar_cache::{self}, home::navigation_tab_bar::get_own_profile, i18n::{AppLanguage, tr_fmt, tr_key}, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::{user_profile::UserProfile, user_profile_cache}, shared::{avatar::{AvatarState, AvatarWidgetExt}, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, AccountSwitchAction, MatrixRequest, submit_async_request}, utils}; +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +use crate::{app::ConfirmDeleteAction, shared::confirmation_modal::ConfirmationModalContent}; script_mod! { use mod.prelude.widgets.* diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 1395da7e3..ae0070887 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -205,7 +205,7 @@ fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { Some(ErrorKind::WeakPassword) => { return "That password is too weak. Please choose a stronger password.".to_owned(); } - Some(ErrorKind::Forbidden { .. }) => { + Some(ErrorKind::Forbidden) => { return "This homeserver does not allow open registration.".to_owned(); } Some(ErrorKind::LimitExceeded { .. }) => { From 1c7e5dab65714ec0701f0870e729b8e64392f87e Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 09:38:34 +0800 Subject: [PATCH 119/283] fix: remove redundant closure to satisfy clippy --- src/home/room_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 02f7c5246..15970fe6f 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -175,7 +175,7 @@ where I: IntoIterator>, F: FnMut(usize) -> bool, { - indices.into_iter().flatten().any(|idx| is_visible(idx)) + indices.into_iter().flatten().any(is_visible) } fn streaming_candidates_from_items<'a>( From 25f471a0a5d4e012086d747e67bb53a116af947d Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 09:48:25 +0800 Subject: [PATCH 120/283] fix: remove unnecessary mut from is_visible parameter --- src/home/room_screen.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 15970fe6f..bd762a325 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -169,7 +169,7 @@ where fn any_timeline_indices_visible( indices: I, - mut is_visible: F, + is_visible: F, ) -> bool where I: IntoIterator>, From 0c72701b5861de4cdfedc79261d2b95784e0cf6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 7 Apr 2026 16:24:13 +0800 Subject: [PATCH 121/283] feat: add updater check flow and Contribute & About settings tab - add desktop updater integration with cargo-packager-updater and latest.json fallback check - extend release workflow to upload updater metadata/signatures and wire updater env vars - add Contribute & About settings section with version display and manual check update button - add i18n keys/messages for update checks and categories - migrate repository and docs links to Project-Robius-China/robrix2 --- .github/workflows/release.yml | 18 +++ Cargo.lock | 113 +++++++++++++++++- Cargo.toml | 4 +- README.md | 44 +++---- resources/i18n/en.json | 14 +++ resources/i18n/zh-CN.json | 14 +++ src/i18n.rs | 2 + src/lib.rs | 1 + src/settings/settings_screen.rs | 195 +++++++++++++++++++++++++++++++- src/updater.rs | 136 ++++++++++++++++++++++ 10 files changed, 515 insertions(+), 26 deletions(-) create mode 100644 src/updater.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e1394630..ffa294210 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -272,9 +272,15 @@ jobs: env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json with: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: deb + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_macos: @@ -296,6 +302,10 @@ jobs: env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }} @@ -305,6 +315,8 @@ jobs: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: dmg enable_macos_notarization: true + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_windows: @@ -326,9 +338,15 @@ jobs: env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json with: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: nsis + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_android: diff --git a/Cargo.lock b/Cargo.lock index f7188bde6..b70a8a8c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1027,6 +1027,42 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +[[package]] +name = "cargo-packager-updater" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec09acab5c2227aba2e592d431708305bdeb6d507703f6cd8983fb57b6c5ef7" +dependencies = [ + "base64", + "cargo-packager-utils", + "ctor", + "dirs", + "dunce", + "flate2", + "http", + "log", + "minisign-verify", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tempfile", + "thiserror 1.0.69", + "time", + "url", +] + +[[package]] +name = "cargo-packager-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43458dd2ee3cdab3f5b105acd80791383b730380c929018701313d7d299d4e8" +dependencies = [ + "ctor", +] + [[package]] name = "cbc" version = "0.1.2" @@ -1764,6 +1800,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1772,7 +1829,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.1", ] @@ -2142,6 +2199,17 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -4199,6 +4267,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -5363,6 +5437,17 @@ dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -5432,6 +5517,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -5601,7 +5687,7 @@ name = "robius-directories" version = "6.0.0" source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", "jni", "robius-android-env", ] @@ -5656,6 +5742,7 @@ dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "blurhash", "bytesize", + "cargo-packager-updater", "chrono", "clap", "crossbeam-channel", @@ -5689,6 +5776,7 @@ dependencies = [ "robius-use-makepad", "ruma", "sanitize-filename", + "semver", "serde", "serde_json", "thiserror 2.0.17", @@ -6782,6 +6870,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -8553,6 +8652,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/Cargo.toml b/Cargo.toml index 662adcd15..e9706c32e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ keywords = ["matrix", "chat", "client", "robius", "makepad"] license = "MIT" readme = "README.md" categories = ["gui"] -repository = "https://github.com/project-robius/robrix" +repository = "https://github.com/Project-Robius-China/robrix2" version = "0.0.1-pre-alpha-4" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" @@ -83,6 +83,8 @@ url = "2.5.0" [target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] rfd = "0.15" +cargo-packager-updater = "0.2" +semver = "1" ## Dependencies for TSP support. ## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219. diff --git a/README.md b/README.md index cb1c8d6ca..e1963f3f9 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ These are generally sorted in order of priority. If you're interested in helping - [x] Cache fetched media on a per-room basis - [x] Fetch and display user profile avatars - [x] Backwards pagination to view a room's older history -- [x] Dynamic backwards pagination based on scroll position/movement: https://github.com/project-robius/robrix/issues/109 -- [x] Loading animation while waiting for pagination request: https://github.com/project-robius/robrix/issues/109 +- [x] Dynamic backwards pagination based on scroll position/movement: https://github.com/Project-Robius-China/robrix2/issues/109 +- [x] Loading animation while waiting for pagination request: https://github.com/Project-Robius-China/robrix2/issues/109 - [x] Stable vertical position of events during timeline update - [x] Display simple plaintext messages - [x] Display image messages (PNG, JPEG) @@ -161,33 +161,33 @@ These are generally sorted in order of priority. If you're interested in helping - [x] Display reactions (annotations) - [x] Handle opening links on click - [x] Linkify plaintext hyperlinks -- [x] Show reply previews above messages: https://github.com/project-robius/robrix/issues/82 +- [x] Show reply previews above messages: https://github.com/Project-Robius-China/robrix2/issues/82 - [x] Send standalone messages -- [x] Interactive reaction button, send reactions: https://github.com/project-robius/robrix/issues/115 -- [x] Show reply button, send reply: https://github.com/project-robius/robrix/issues/83 +- [x] Interactive reaction button, send reactions: https://github.com/Project-Robius-China/robrix2/issues/115 +- [x] Show reply button, send reply: https://github.com/Project-Robius-China/robrix2/issues/83 - [x] Edit existing messages -- [x] E2EE device verification, decrypt message content: https://github.com/project-robius/robrix/issues/116 -- [ ] Re-spawn timeline as focused on an old event after a full timeline clear: https://github.com/project-robius/robrix/issues/103 +- [x] E2EE device verification, decrypt message content: https://github.com/Project-Robius-China/robrix2/issues/116 +- [ ] Re-spawn timeline as focused on an old event after a full timeline clear: https://github.com/Project-Robius-China/robrix2/issues/103 ### Auxiliary features, login, registration, settings -- [x] Persistence of app session to disk: https://github.com/project-robius/robrix/issues/112 -- [x] Username/password login screen: https://github.com/project-robius/robrix/issues/113 -- [x] SSO, other 3rd-party auth providers login screen: https://github.com/project-robius/robrix/issues/114 -- [x] Client logout, with server-side logout and app state reset: https://github.com/project-robius/robrix/pull/432 +- [x] Persistence of app session to disk: https://github.com/Project-Robius-China/robrix2/issues/112 +- [x] Username/password login screen: https://github.com/Project-Robius-China/robrix2/issues/113 +- [x] SSO, other 3rd-party auth providers login screen: https://github.com/Project-Robius-China/robrix2/issues/114 +- [x] Client logout, with server-side logout and app state reset: https://github.com/Project-Robius-China/robrix2/pull/432 - [x] Side panel showing detailed user profile info (click on their Avatar) - [x] Ignore and unignore users (see known issues) -- [x] Display read receipts besides messages: https://github.com/project-robius/robrix/pull/162 -- [x] Mention users within a room (or the whole `@room`): https://github.com/project-robius/robrix/issues/452 -- [x] Dedicated view of direct messages (DMs): https://github.com/project-robius/robrix/issues/139 -- [x] Keyword filters for the list of all rooms: https://github.com/project-robius/robrix/issues/123 -- [ ] Collapsible/expandable view of contiguous "small" events: https://github.com/project-robius/robrix/issues/118 -- [ ] Display multimedia (audio/video/gif) message events: https://github.com/project-robius/robrix/issues/120 +- [x] Display read receipts besides messages: https://github.com/Project-Robius-China/robrix2/pull/162 +- [x] Mention users within a room (or the whole `@room`): https://github.com/Project-Robius-China/robrix2/issues/452 +- [x] Dedicated view of direct messages (DMs): https://github.com/Project-Robius-China/robrix2/issues/139 +- [x] Keyword filters for the list of all rooms: https://github.com/Project-Robius-China/robrix2/issues/123 +- [ ] Collapsible/expandable view of contiguous "small" events: https://github.com/Project-Robius-China/robrix2/issues/118 +- [ ] Display multimedia (audio/video/gif) message events: https://github.com/Project-Robius-China/robrix2/issues/120 - [x] User settings screen -- [x] Dedicated view of spaces: https://github.com/project-robius/robrix/pull/636 -- [x] Link previews beneath messages: https://github.com/project-robius/robrix/issues/81, https://github.com/project-robius/robrix/pull/585 -- [ ] Search messages within a room: https://github.com/project-robius/robrix/issues/122 +- [x] Dedicated view of spaces: https://github.com/Project-Robius-China/robrix2/pull/636 +- [x] Link previews beneath messages: https://github.com/Project-Robius-China/robrix2/issues/81, https://github.com/Project-Robius-China/robrix2/pull/585 +- [ ] Search messages within a room: https://github.com/Project-Robius-China/robrix2/issues/122 - [ ] Room browser, search for public rooms - [x] Accept/reject room invites - [x] Join room by accepting invite @@ -197,13 +197,13 @@ These are generally sorted in order of priority. If you're interested in helping - [ ] Room settings/info screen - [ ] Room members pane - [ ] Administrative abilities: ban, kick, etc -- [x] Offline mode with persistent event cache: https://github.com/project-robius/robrix/pull/445 +- [x] Offline mode with persistent event cache: https://github.com/Project-Robius-China/robrix2/pull/445 ## Packaging Robrix for Distribution on Desktop Platforms > [!TIP] -> We already have [pre-built releases of Robrix](https://github.com/project-robius/robrix/releases) available for download. +> We already have [pre-built releases of Robrix](https://github.com/Project-Robius-China/robrix2/releases) available for download. 1. Install `cargo-packager`: diff --git a/resources/i18n/en.json b/resources/i18n/en.json index d91de0838..d46db6c20 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -3,6 +3,20 @@ "settings.category.account": "Account", "settings.category.preferences": "Preferences", "settings.category.labs": "Labs", + "settings.category.contribute": "Contribute & About", + "settings.category.about": "About", + "settings.contribute.title": "Contribute", + "settings.contribute.description": "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2", + "settings.about.title": "About Robrix", + "settings.about.description": "Robrix is a multi-platform Matrix chat client built with Makepad and Robius.", + "settings.update.current_version": "Current version: {version}", + "settings.update.button.check": "Check for Updates", + "settings.update.button.checking": "Checking for Updates...", + "settings.update.popup.latest": "You are already on the latest version ({version}).", + "settings.update.popup.available": "A newer version is available: {latest} (current: {current}).", + "settings.update.popup.not_configured": "Updater is not configured yet in this build.", + "settings.update.popup.unsupported": "Update checks are only supported on desktop builds.", + "settings.update.popup.failed": "Failed to check for updates: {error}", "settings.preferences.language.title": "Language", "settings.preferences.language.application_label": "Application language", "settings.preferences.language.reload_hint": "The app will reload after selecting another language", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 8c77b7696..53c8f956c 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -3,6 +3,20 @@ "settings.category.account": "账号", "settings.category.preferences": "偏好", "settings.category.labs": "实验室", + "settings.category.contribute": "贡献&关于", + "settings.category.about": "关于", + "settings.contribute.title": "参与贡献", + "settings.contribute.description": "欢迎在 GitHub 参与 Robrix 贡献:https://github.com/Project-Robius-China/robrix2", + "settings.about.title": "关于 Robrix", + "settings.about.description": "Robrix 是基于 Makepad 和 Robius 构建的多平台 Matrix 聊天客户端。", + "settings.update.current_version": "当前版本:{version}", + "settings.update.button.check": "检查更新", + "settings.update.button.checking": "正在检查更新...", + "settings.update.popup.latest": "当前已是最新版本({version})。", + "settings.update.popup.available": "发现新版本:{latest}(当前:{current})。", + "settings.update.popup.not_configured": "当前构建尚未配置 updater。", + "settings.update.popup.unsupported": "仅桌面端构建支持检查更新。", + "settings.update.popup.failed": "检查更新失败:{error}", "settings.preferences.language.title": "语言", "settings.preferences.language.application_label": "应用语言", "settings.preferences.language.reload_hint": "选择其他语言后,应用将重新加载", diff --git a/src/i18n.rs b/src/i18n.rs index 4cee09721..ff4170bc6 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -46,6 +46,7 @@ pub enum I18nKey { SettingsCategoryAccount, SettingsCategoryPreferences, SettingsCategoryLabs, + SettingsCategoryContribute, LanguageTitle, ApplicationLanguageLabel, LanguageReloadHint, @@ -60,6 +61,7 @@ impl I18nKey { I18nKey::SettingsCategoryAccount => "settings.category.account", I18nKey::SettingsCategoryPreferences => "settings.category.preferences", I18nKey::SettingsCategoryLabs => "settings.category.labs", + I18nKey::SettingsCategoryContribute => "settings.category.contribute", I18nKey::LanguageTitle => "settings.preferences.language.title", I18nKey::ApplicationLanguageLabel => "settings.preferences.language.application_label", I18nKey::LanguageReloadHint => "settings.preferences.language.reload_hint", diff --git a/src/lib.rs b/src/lib.rs index 67d2c9fcf..81e858724 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,6 +79,7 @@ pub mod space_service_sync; pub mod avatar_cache; pub mod media_cache; pub mod verification; +pub mod updater; pub mod utils; /// Multi-account management for supporting multiple Matrix accounts simultaneously. diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 3ba531fef..04ce5bd9b 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,7 +1,7 @@ use makepad_widgets::*; -use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{expand_arrow::ExpandArrow, popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id}; +use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr, tr_fmt, tr_key}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{expand_arrow::ExpandArrow, popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id, updater::{UpdateCheckOutcome, check_for_updates}}; script_mod! { use mod.prelude.widgets.* @@ -80,6 +80,14 @@ script_mod! { icon_walk: Walk{width: 0, height: 0, margin: 0} text: "Labs" } + + category_contribute_button := RobrixNeutralIconButton { + width: Fit, height: Fit, + padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + text: "Contribute" + } } ScrollXYView { @@ -223,6 +231,65 @@ script_mod! { // The TSP wallet settings section. tsp_settings_screen := TspSettingsScreen {} } + + contribute_settings_section := View { + visible: false + width: Fill, height: Fit + flow: Down + spacing: 8 + + contribute_title := TitleLabel { + text: "Contribute" + } + + contribute_description := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + margin: Inset{left: 5, right: 8, top: 1, bottom: 2} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2" + } + + about_title := TitleLabel { + text: "About Robrix" + } + + about_description := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + margin: Inset{left: 5, right: 8, top: 1, bottom: 2} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Robrix is a multi-platform Matrix chat client built with Makepad and Robius." + } + + contribute_current_version_label := Label { + width: Fill + height: Fit + margin: Inset{left: 5, right: 8, top: 2, bottom: 3} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Current version: 0.0.0" + } + + contribute_check_update_button := RobrixNeutralIconButton { + width: Fit, height: Fit, + margin: Inset{left: 5} + padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + text: "Check for Updates" + } + } } } } @@ -250,6 +317,12 @@ enum SettingsCategory { Account, Preferences, Labs, + Contribute, +} + +#[derive(Debug)] +enum SettingsUpdateAction { + CheckFinished(UpdateCheckOutcome), } /// The top-level widget showing all app and user settings/preferences. @@ -260,6 +333,7 @@ pub struct SettingsScreen { #[rust] selected_category: SettingsCategory, #[rust] app_language: AppLanguage, #[rust] language_popup_visible: bool, + #[rust] is_update_checking: bool, } impl Widget for SettingsScreen { @@ -358,6 +432,29 @@ impl Widget for SettingsScreen { else if self.view.button(cx, ids!(category_labs_button)).clicked(actions) { self.set_selected_category(cx, SettingsCategory::Labs); } + else if self.view.button(cx, ids!(category_contribute_button)).clicked(actions) { + self.set_selected_category(cx, SettingsCategory::Contribute); + } + + if !self.is_update_checking && ( + self.view.button(cx, ids!(contribute_check_update_button)).clicked(actions) + ) { + self.set_update_checking(cx, true); + cx.spawn_thread(move || { + let result = check_for_updates(); + Cx::post_action(SettingsUpdateAction::CheckFinished(result)); + }); + } + + for action in actions { + match action.downcast_ref() { + Some(SettingsUpdateAction::CheckFinished(result)) => { + self.set_update_checking(cx, false); + self.show_update_check_result(result); + } + None => { } + } + } #[cfg(feature = "tsp")] { @@ -427,6 +524,9 @@ impl SettingsScreen { self.view .button(cx, ids!(category_labs_button)) .set_text(cx, tr(self.app_language, I18nKey::SettingsCategoryLabs)); + self.view + .button(cx, ids!(category_contribute_button)) + .set_text(cx, tr(self.app_language, I18nKey::SettingsCategoryContribute)); self.view .label(cx, ids!(preferences_language_title)) .set_text(cx, tr(self.app_language, I18nKey::LanguageTitle)); @@ -446,6 +546,19 @@ impl SettingsScreen { self.view .translation_settings(cx, ids!(translation_settings)) .set_app_language(cx, self.app_language); + self.view + .label(cx, ids!(contribute_title)) + .set_text(cx, tr_key(self.app_language, "settings.contribute.title")); + self.view + .label(cx, ids!(contribute_description)) + .set_text(cx, tr_key(self.app_language, "settings.contribute.description")); + self.view + .label(cx, ids!(about_title)) + .set_text(cx, tr_key(self.app_language, "settings.about.title")); + self.view + .label(cx, ids!(about_description)) + .set_text(cx, tr_key(self.app_language, "settings.about.description")); + self.sync_update_widgets_text(cx); self.view.redraw(cx); } @@ -470,14 +583,17 @@ impl SettingsScreen { let show_account = self.selected_category == SettingsCategory::Account; let show_preferences = self.selected_category == SettingsCategory::Preferences; let show_labs = self.selected_category == SettingsCategory::Labs; + let show_contribute = self.selected_category == SettingsCategory::Contribute; self.view.view(cx, ids!(account_settings_section)).set_visible(cx, show_account); self.view.view(cx, ids!(preferences_settings_section)).set_visible(cx, show_preferences); self.view.view(cx, ids!(labs_settings_section)).set_visible(cx, show_labs); + self.view.view(cx, ids!(contribute_settings_section)).set_visible(cx, show_contribute); let mut category_account_button = self.view.button(cx, ids!(category_account_button)); let mut category_preferences_button = self.view.button(cx, ids!(category_preferences_button)); let mut category_labs_button = self.view.button(cx, ids!(category_labs_button)); + let mut category_contribute_button = self.view.button(cx, ids!(category_contribute_button)); if show_account { apply_primary_button_style(cx, &mut category_account_button); @@ -494,13 +610,89 @@ impl SettingsScreen { } else { apply_neutral_button_style(cx, &mut category_labs_button); } + if show_contribute { + apply_primary_button_style(cx, &mut category_contribute_button); + } else { + apply_neutral_button_style(cx, &mut category_contribute_button); + } category_account_button.reset_hover(cx); category_preferences_button.reset_hover(cx); category_labs_button.reset_hover(cx); + category_contribute_button.reset_hover(cx); self.view.redraw(cx); } + fn set_update_checking(&mut self, cx: &mut Cx, is_update_checking: bool) { + self.is_update_checking = is_update_checking; + self.sync_update_widgets_text(cx); + self.view.redraw(cx); + } + + fn sync_update_widgets_text(&mut self, cx: &mut Cx) { + let current_version_text = tr_fmt(self.app_language, "settings.update.current_version", &[ + ("version", env!("CARGO_PKG_VERSION")), + ]); + self.view + .label(cx, ids!(contribute_current_version_label)) + .set_text(cx, ¤t_version_text); + let check_button_text = if self.is_update_checking { + tr_key(self.app_language, "settings.update.button.checking") + } else { + tr_key(self.app_language, "settings.update.button.check") + }; + self.view + .button(cx, ids!(contribute_check_update_button)) + .set_text(cx, check_button_text); + } + + fn show_update_check_result(&mut self, result: &UpdateCheckOutcome) { + match result { + UpdateCheckOutcome::UpToDate { current_version } => { + enqueue_popup_notification( + tr_fmt(self.app_language, "settings.update.popup.latest", &[ + ("version", current_version.as_str()), + ]), + PopupKind::Info, + Some(4.0), + ); + } + UpdateCheckOutcome::UpdateAvailable { current_version, latest_version } => { + enqueue_popup_notification( + tr_fmt(self.app_language, "settings.update.popup.available", &[ + ("latest", latest_version.as_str()), + ("current", current_version.as_str()), + ]), + PopupKind::Warning, + Some(5.0), + ); + } + UpdateCheckOutcome::NotConfigured => { + enqueue_popup_notification( + tr_key(self.app_language, "settings.update.popup.not_configured"), + PopupKind::Warning, + Some(4.0), + ); + } + UpdateCheckOutcome::UnsupportedPlatform => { + enqueue_popup_notification( + tr_key(self.app_language, "settings.update.popup.unsupported"), + PopupKind::Warning, + Some(4.0), + ); + } + UpdateCheckOutcome::Error(error) => { + enqueue_popup_notification( + tr_fmt(self.app_language, "settings.update.popup.failed", &[ + ("error", error.as_str()), + ]), + PopupKind::Error, + Some(6.0), + ); + } + } + } + /// Fetches the current user's profile and uses it to populate the settings screen. pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState, translation_config: &crate::room::translation::TranslationConfig, app_language: AppLanguage) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { @@ -511,6 +703,7 @@ impl SettingsScreen { self.view.bot_settings(cx, ids!(bot_settings)).populate(cx, bot_settings); self.view.translation_settings(cx, ids!(translation_settings)).populate(cx, translation_config); self.set_app_language(cx, app_language); + self.set_update_checking(cx, false); self.set_selected_category(cx, SettingsCategory::Account); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); diff --git a/src/updater.rs b/src/updater.rs new file mode 100644 index 000000000..7df03efca --- /dev/null +++ b/src/updater.rs @@ -0,0 +1,136 @@ +#[derive(Debug)] +pub enum UpdateCheckOutcome { + UpToDate { + current_version: String, + }, + UpdateAvailable { + current_version: String, + latest_version: String, + }, + NotConfigured, + UnsupportedPlatform, + Error(String), +} + +const DEFAULT_UPDATER_ENDPOINT: &str = "https://github.com/Project-Robius-China/robrix2/releases/latest/download/latest.json"; + +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +fn check_latest_version_without_signature(endpoint: &str) -> Result, String> { + use serde_json::Value; + use tokio::runtime::Runtime; + + let runtime = Runtime::new().map_err(|error| format!("Failed to create async runtime: {error}"))?; + runtime.block_on(async move { + let response = reqwest::get(endpoint) + .await + .map_err(|error| format!("Failed to fetch updater metadata: {error}"))?; + if !response.status().is_success() { + return Err(format!("Updater metadata request failed with status {}", response.status())); + } + let payload: Value = response + .json() + .await + .map_err(|error| format!("Failed to parse updater metadata JSON: {error}"))?; + let latest_version = payload + .get("version") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); + Ok(latest_version) + }) +} + +fn resolve_updater_pubkey() -> Option { + option_env!("ROBRIX_UPDATER_PUBKEY") + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| std::env::var("ROBRIX_UPDATER_PUBKEY").ok()) + .or_else(|| std::env::var("CARGO_PACKAGER_SIGN_PUBLIC_KEY").ok()) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +fn resolve_updater_endpoint() -> String { + option_env!("ROBRIX_UPDATER_ENDPOINT") + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| std::env::var("ROBRIX_UPDATER_ENDPOINT").ok()) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| DEFAULT_UPDATER_ENDPOINT.to_string()) +} + +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +pub fn check_for_updates() -> UpdateCheckOutcome { + use cargo_packager_updater::{Config, check_update}; + use semver::Version; + use url::Url; + + let current_version = env!("CARGO_PKG_VERSION").to_string(); + let current_version_semver = match Version::parse(¤t_version) { + Ok(version) => version, + Err(error) => { + return UpdateCheckOutcome::Error(format!("Invalid current version format: {error}")); + } + }; + + let endpoint = resolve_updater_endpoint(); + let pubkey = resolve_updater_pubkey(); + + if let Some(pubkey) = pubkey { + let endpoint_url = match Url::parse(&endpoint) { + Ok(url) => url, + Err(error) => { + return UpdateCheckOutcome::Error(format!("Invalid updater endpoint URL: {error}")); + } + }; + + let config = Config { + endpoints: vec![endpoint_url], + pubkey, + ..Default::default() + }; + + match check_update(current_version_semver.clone(), config) { + Ok(Some(update)) => UpdateCheckOutcome::UpdateAvailable { + current_version, + latest_version: update.version.to_string(), + }, + Ok(None) => UpdateCheckOutcome::UpToDate { + current_version, + }, + Err(error) => UpdateCheckOutcome::Error(error.to_string()), + } + } else { + match check_latest_version_without_signature(&endpoint) { + Ok(Some(latest_version)) => { + let latest_semver = match Version::parse(&latest_version) { + Ok(version) => version, + Err(error) => { + return UpdateCheckOutcome::Error(format!("Invalid latest version format: {error}")); + } + }; + if latest_semver > current_version_semver { + UpdateCheckOutcome::UpdateAvailable { + current_version, + latest_version, + } + } else { + UpdateCheckOutcome::UpToDate { + current_version, + } + } + } + Ok(None) => UpdateCheckOutcome::NotConfigured, + Err(error) => UpdateCheckOutcome::Error(error), + } + } +} + +#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] +pub fn check_for_updates() -> UpdateCheckOutcome { + UpdateCheckOutcome::UnsupportedPlatform +} From 5f0228b60b8583f44dd2f14d007f8e148540ad7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 7 Apr 2026 17:00:36 +0800 Subject: [PATCH 122/283] fix: use matrix_sdk::reqwest for cross-target builds Replace direct reqwest imports/usages in media and sliding sync code paths to avoid Android unresolved-import failures. --- src/home/room_image_viewer.rs | 2 +- src/media_cache.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/home/room_image_viewer.rs b/src/home/room_image_viewer.rs index ba8eaa3a3..12a1b4429 100644 --- a/src/home/room_image_viewer.rs +++ b/src/home/room_image_viewer.rs @@ -2,9 +2,9 @@ use makepad_widgets::*; use matrix_sdk_ui::timeline::EventTimelineItem; use matrix_sdk::{ media::MediaFormat, + reqwest::StatusCode, ruma::events::room::{message::MessageType, MediaSource}, }; -use matrix_sdk::reqwest::StatusCode; use crate::{media_cache::{MediaCache, MediaCacheEntry}, shared::image_viewer::{ImageViewerAction, ImageViewerError, LoadState}}; diff --git a/src/media_cache.rs b/src/media_cache.rs index 672a16cf1..ef7fa8a04 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -1,8 +1,7 @@ use std::{ops::{Deref, DerefMut}, sync::{Arc, Mutex}, time::SystemTime}; use hashbrown::{hash_map::RawEntryMut, HashMap}; use makepad_widgets::{error, log, SignalToUI}; -use matrix_sdk::{media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, ruma::{events::room::MediaSource, OwnedMxcUri}, Error, HttpError}; -use matrix_sdk::reqwest::StatusCode; +use matrix_sdk::{media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}, reqwest::StatusCode, ruma::{events::room::MediaSource, OwnedMxcUri}, Error, HttpError}; use crate::{home::room_screen::TimelineUpdate, sliding_sync::{self, MatrixRequest}}; /// The value type in the media cache, one per Matrix URI. From dc8cb29016716c75ff05904277143ce654ec8d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 8 Apr 2026 11:38:25 +0800 Subject: [PATCH 123/283] fix: use clickable text link for contribute repo Render the GitHub repository URL in Settings as a blue underlined Html link and open it on click. Also keep updater fetch compatible with default features by using matrix_sdk::reqwest and serde_json::from_str, plus related i18n/lockfile updates. --- Cargo.lock | 2 +- resources/i18n/en.json | 2 +- resources/i18n/zh-CN.json | 2 +- src/settings/settings_screen.rs | 29 +++++++++++++++++++++++++++++ src/updater.rs | 8 +++++--- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b70a8a8c7..62acc0128 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,7 +1043,7 @@ dependencies = [ "log", "minisign-verify", "percent-encoding", - "reqwest", + "reqwest 0.12.28", "semver", "serde", "serde_json", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index d46db6c20..f3da6e37c 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -6,7 +6,7 @@ "settings.category.contribute": "Contribute & About", "settings.category.about": "About", "settings.contribute.title": "Contribute", - "settings.contribute.description": "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2", + "settings.contribute.description": "Contribute to Robrix on GitHub:", "settings.about.title": "About Robrix", "settings.about.description": "Robrix is a multi-platform Matrix chat client built with Makepad and Robius.", "settings.update.current_version": "Current version: {version}", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 53c8f956c..e4cd0495c 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -6,7 +6,7 @@ "settings.category.contribute": "贡献&关于", "settings.category.about": "关于", "settings.contribute.title": "参与贡献", - "settings.contribute.description": "欢迎在 GitHub 参与 Robrix 贡献:https://github.com/Project-Robius-China/robrix2", + "settings.contribute.description": "欢迎在 GitHub 参与 Robrix 贡献:", "settings.about.title": "关于 Robrix", "settings.about.description": "Robrix 是基于 Makepad 和 Robius 构建的多平台 Matrix 聊天客户端。", "settings.update.current_version": "当前版本:{version}", diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 04ce5bd9b..746ee4543 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -3,6 +3,8 @@ use makepad_widgets::*; use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr, tr_fmt, tr_key}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{expand_arrow::ExpandArrow, popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id, updater::{UpdateCheckOutcome, check_for_updates}}; +const CONTRIBUTE_REPO_URL: &str = "https://github.com/Project-Robius-China/robrix2"; + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -254,6 +256,21 @@ script_mod! { text: "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2" } + contribute_repo_link := Html { + width: Fit + height: Fit + flow: Flow.Right{wrap: true} + margin: Inset{left: 5, right: 8, top: 0, bottom: 4} + padding: 0 + font_size: 10.5 + font_color: #x2A6FDB + draw_text +: { + color: #x2A6FDB + text_style: REGULAR_TEXT { font_size: 10.5 } + } + body: "https://github.com/Project-Robius-China/robrix2" + } + about_title := TitleLabel { text: "About Robrix" } @@ -447,6 +464,18 @@ impl Widget for SettingsScreen { } for action in actions { + if let HtmlLinkAction::Clicked { url, .. } = action.as_widget_action().cast() { + if url == CONTRIBUTE_REPO_URL { + if let Err(e) = robius_open::Uri::new(&url).open() { + error!("Failed to open URL {:?}. Error: {:?}", url, e); + enqueue_popup_notification( + tr_fmt(self.app_language, "room_screen.popup.open_url_failed", &[("url", url.as_str())]), + PopupKind::Error, + Some(10.0), + ); + } + } + } match action.downcast_ref() { Some(SettingsUpdateAction::CheckFinished(result)) => { self.set_update_checking(cx, false); diff --git a/src/updater.rs b/src/updater.rs index 7df03efca..9addd0c9d 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -21,15 +21,17 @@ fn check_latest_version_without_signature(endpoint: &str) -> Result Date: Wed, 8 Apr 2026 12:09:46 +0800 Subject: [PATCH 124/283] fix(ci): align android artifact path and silence non-desktop updater warnings Add Android release workflow workaround for target/makepad-android-apk path compatibility. Gate desktop-only updater helpers with cfg so Android builds do not emit dead_code warnings. --- .github/workflows/release.yml | 6 ++++++ src/updater.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ffa294210..f10723af4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -360,6 +360,12 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable + - name: Workaround Android APK output path + shell: bash + run: | + mkdir -p target/android + ln -sfn android/makepad-android-apk target/makepad-android-apk + - name: Package (android) uses: project-robius/makepad-packaging-action@v1 env: diff --git a/src/updater.rs b/src/updater.rs index 9addd0c9d..10427c92d 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -12,6 +12,7 @@ pub enum UpdateCheckOutcome { Error(String), } +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] const DEFAULT_UPDATER_ENDPOINT: &str = "https://github.com/Project-Robius-China/robrix2/releases/latest/download/latest.json"; #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] @@ -43,6 +44,7 @@ fn check_latest_version_without_signature(endpoint: &str) -> Result Option { option_env!("ROBRIX_UPDATER_PUBKEY") .map(str::trim) @@ -54,6 +56,7 @@ fn resolve_updater_pubkey() -> Option { .filter(|value| !value.is_empty()) } +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] fn resolve_updater_endpoint() -> String { option_env!("ROBRIX_UPDATER_ENDPOINT") .map(str::trim) From 967343b1cd650c0caec54add61a111fc3491bc19 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 12:25:06 +0800 Subject: [PATCH 125/283] docs: add OpenClaw + Matrix integration guide (zh/en) Add complete documentation for using Robrix with OpenClaw AI agents: - 01: Deployment guide with tested config, Palpo vs matrix.org differences - 02: Usage guide with screenshot placeholders for DM and room workflows - 03: Architecture guide comparing client mode vs Octos AppService mode - Update README.md with OpenClaw section Based on hands-on testing with OpenClaw v2026.4.7 + local Palpo. --- docs/README.md | 17 + .../01-deploying-openclaw-with-matrix-zh.md | 356 ++++++++++++++++++ .../01-deploying-openclaw-with-matrix.md | 356 ++++++++++++++++++ .../02-using-robrix-with-openclaw-zh.md | 124 ++++++ .../02-using-robrix-with-openclaw.md | 124 ++++++ ...ow-robrix-and-openclaw-work-together-zh.md | 215 +++++++++++ ...3-how-robrix-and-openclaw-work-together.md | 215 +++++++++++ 7 files changed, 1407 insertions(+) create mode 100644 docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md create mode 100644 docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md create mode 100644 docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md create mode 100644 docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md create mode 100644 docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md create mode 100644 docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md diff --git a/docs/README.md b/docs/README.md index 217613645..ffc3070f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,6 +35,23 @@ For users who want to deploy a complete AI chat system — running your own Matr --- +## Robrix + OpenClaw (AI Agent Framework) + +For users who want to connect OpenClaw AI agents to Matrix, then use Robrix to chat with them: + +| Guide | Goal | +|-------|------| +| [1. Deploying OpenClaw with Matrix](robrix-with-openclaw/01-deploying-openclaw-with-matrix.md) | **Get OpenClaw connected to a Matrix homeserver.** Create a bot account, configure the Matrix channel plugin, and verify the connection so Robrix can chat with OpenClaw agents. | +| [2. Using Robrix with OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw.md) | **Use Robrix to chat with OpenClaw agents.** Start conversations via DM or rooms, understand feature compatibility, and learn the differences from the Octos workflow. | +| [3. How Robrix and OpenClaw Work Together](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md) | **Understand the client-based integration model.** Learn how OpenClaw connects to Matrix as a regular client (vs. Octos's Appservice model), how messages flow, and how E2EE works. | + +> Chinese: +> [1. 部署 OpenClaw + Matrix](robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md) · +> [2. 在 Robrix 上使用 OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md) · +> [3. Robrix 与 OpenClaw 协作原理](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md) + +--- + ## Palpo and Octos Deployment Files The [`palpo-and-octos-deploy/`](../palpo-and-octos-deploy/) directory (at the repository root) contains the runnable deployment files for Palpo and Octos, including Docker Compose and configuration templates: diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md new file mode 100644 index 000000000..3f4ff6aa9 --- /dev/null +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -0,0 +1,356 @@ +# 部署指南:OpenClaw + Matrix + +[English](01-deploying-openclaw-with-matrix.md) + +> **目标:** 完成本指南后,你将拥有一个连接到 Matrix 服务器的 OpenClaw AI 代理。之后你可以使用 Robrix(或任何 Matrix 客户端)与 OpenClaw 驱动的 AI 代理对话。 + +本指南将逐步引导你完成 OpenClaw 与 Matrix 的部署:从创建 Matrix Bot 账号,到配置 OpenClaw Matrix 频道插件,再到端到端验证连接。 + +> **想快速体验?** 跳转到 [快速开始](#2-快速开始)。 +> +> **想了解 OpenClaw 如何连接 Matrix?** 参见 [架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + +> **关于 OpenClaw:** OpenClaw 目前仍在快速迭代中,CLI 和插件系统存在不少 bug(例如 `channels add` 向导可能崩溃)。本指南给出的是我们**实测验证过**的配置方式——直接编辑配置文件,跳过不稳定的 CLI 向导。如果你遇到本指南未覆盖的问题,请查阅 [OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues)。 + +--- + +## 目录 + +1. [前置条件](#1-前置条件) +2. [快速开始](#2-快速开始) +3. [创建 Matrix Bot 账号](#3-创建-matrix-bot-账号) +4. [安装 OpenClaw 并初始化配置目录](#4-安装-openclaw-并初始化配置目录) +5. [编写配置文件](#5-编写配置文件) +6. [启动并验证](#6-启动并验证) +7. [故障排查](#7-故障排查) +8. [生产环境配置](#8-生产环境配置) +9. [延伸阅读](#9-延伸阅读) + +--- + +## 1. 前置条件 + +| 条件 | 说明 | +|------|------| +| **两个 Matrix 账号** | 一个作为你自己使用的账号,另一个作为 OpenClaw Bot 使用的账号 | +| **Node.js** | v22.16+ 或 v24+(推荐) | +| **LLM API Key** | 例如 [DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等 | +| **Matrix 服务器** | 本地 Palpo(推荐,参见 [Palpo 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md))或公共服务器 matrix.org | +| **Robrix** | 参见 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) | + +--- + +## 2. 快速开始 + +``` +1. 注册一个 Matrix Bot 账号(记住用户名和密码) +2. 安装 OpenClaw → 运行 openclaw config → 编辑 ~/.openclaw/openclaw.json +3. 运行 openclaw gateway start +4. 在 Robrix 中用另一个账号给 Bot 发消息 +``` + +详细步骤见下文。 + +--- + +## 3. 创建 Matrix Bot 账号 + +Bot 账号就是一个**普通的 Matrix 账号**。OpenClaw 会用它的用户名和密码自动登录,不需要你手动获取 Access Token。 + +| 服务器 | 注册方式 | 说明 | +|--------|---------|------| +| **本地 Palpo**(推荐) | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | +| **matrix.org** | 在 Robrix 或 [Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | +| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | + +注册时记住: +- **用户名**(例如 `chalice`) +- **密码** + +--- + +## 4. 安装 OpenClaw 并初始化配置目录 + +### 4.1 安装 + +```bash +npm install -g openclaw@latest +openclaw --version # 验证安装 +``` + +### 4.2 初始化配置目录 + +```bash +openclaw config +``` + +> **这个命令会报错——这是正常的,忽略即可。** 重要的是它已经在 `~/.openclaw/` 下创建了配置目录。后续所有配置都在这个目录中进行。 + +> **为什么不用 `openclaw channels add` 向导?** OpenClaw v2026.4.7 的 CLI 向导存在多个 bug(Telegram 插件路径错误导致向导崩溃、参数不完整等)。**直接编辑配置文件是唯一可靠的方式。** + +--- + +## 5. 编写配置文件 + +编辑 `~/.openclaw/openclaw.json`。下面提供两种场景的完整配置。 + +### 5.1 连接本地 Palpo(推荐) + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-你的DeepSeek密钥", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "http://127.0.0.1:8128", + "network": { + "dangerouslyAllowPrivateNetwork": true + }, + "userId": "@chalice:127.0.0.1:8128", + "password": "你的密码", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.2 连接公共服务器 matrix.org + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-你的DeepSeek密钥", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "https://matrix.org", + "userId": "@your-bot:matrix.org", + "password": "你的密码", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.3 配置项详解 + +#### `gateway` 配置 + +| 字段 | 值 | 重点说明 | +|------|-----|---------| +| `mode` | `"local"` | **必填。** 没有这个字段 gateway 会拒绝启动,报 "missing gateway.mode" 错误。 | + +#### `models.providers` 配置 + +| 字段 | 值 | 重点说明 | +|------|-----|---------| +| `baseUrl` | `"https://api.deepseek.com/v1"` | **必须带 `/v1` 后缀。** DeepSeek 使用 OpenAI 兼容 API。 | +| `apiKey` | `"sk-xxx"` | **直接写明文密钥。** 不要用 `${ENV_VAR}` 格式——macOS LaunchAgent 服务读不到终端的环境变量。写完后 `chmod 600 ~/.openclaw/openclaw.json` 保护文件权限。 | +| `api` | `"openai-completions"` | **不是 `type`。** 网上很多教程写 `"type"` 是错的,正确字段名是 `"api"`。 | +| `contextWindow` | `164000` | **必须设大。** OpenClaw 系统提示词占 16K+ token,默认 4096 会直接报错。DeepSeek Chat 支持 164K。 | +| `maxTokens` | `8192` | 单次回复最大 token 数。 | + +> **注意 `providers` 的格式:** `providers` 是一个对象(provider 名称作为 key),不是数组。`models` 是数组。 + +#### `channels.matrix` 配置 + +| 字段 | 值 | 重点说明 | +|------|-----|---------| +| `enabled` | `true` | 启用 Matrix 频道。 | +| `homeserver` | `"http://127.0.0.1:8128"` | **本地 Palpo 必须用 `http`**,不是 `https`(Palpo 默认没有 TLS)。matrix.org 用 `https`。 | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **仅本地/内网部署需要。** OpenClaw 默认阻止连接私有 IP(127.0.0.1、10.x、192.168.x),这是防 SSRF 的安全措施。连公共服务器(matrix.org)不需要此项。 | +| `userId` | `"@chalice:127.0.0.1:8128"` | **必须是完整 Matrix ID 格式** `@用户名:服务器`。 | +| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。 | +| `encryption` | `true` | **强烈建议开启。** Matrix DM 默认启用 E2EE。如果不开,Bot 收到加密消息无法解密,表现为"发了消息但没回复"。 | +| `autoJoin` | `"always"` | 测试阶段接受所有邀请。生产环境改为 `"allowlist"`。 | +| `dm.policy` | `"open"` | 测试阶段允许所有私聊。生产环境改为 `"allowlist"`。 | + +#### `plugins` 配置 + +| 字段 | 值 | 重点说明 | +|------|-----|---------| +| `plugins.entries.matrix.enabled` | `true` | 确保 Matrix 插件已启用。 | + +### 5.4 本地 Palpo vs 公共 matrix.org 的差异 + +| 配置项 | 本地 Palpo | 公共 matrix.org | +|--------|-----------|----------------| +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **需要** `true` | **不需要**(删除整个 `network` 块) | +| `userId` 格式 | `@用户名:127.0.0.1:8128` | `@用户名:matrix.org` | +| TLS | 无(`http`) | 有(`https`) | +| 注册方式 | Robrix 连接 Palpo 注册 | Element Web 或 Robrix 注册 | + +--- + +## 6. 启动并验证 + +### 6.1 启动 Gateway + +```bash +openclaw gateway start +``` + +### 6.2 检查日志 + +```bash +tail -20 ~/.openclaw/logs/gateway.log +``` + +确认看到以下关键日志: + +``` +[gateway] agent model: deepseek/deepseek-chat ← LLM 配置正确 +[gateway] ready (6 plugins, 0.3s) ← Gateway 就绪 +[matrix] [default] starting provider (http://...) ← Matrix 开始连接 +matrix: logged in as @chalice:127.0.0.1:8128 ← 登录成功 +matrix: device is verified by its owner and ready for encrypted rooms ← 加密就绪 +``` + +### 6.3 在 Robrix 中测试 + +1. **启动 Robrix**,用你的**个人账号**登录 +2. **搜索 Bot**:点搜索图标,输入 Bot 的 Matrix ID(如 `@chalice:127.0.0.1:8128`),切到 **People** 标签 +3. **发起私聊**:选择 Bot,进入对话 +4. **发送消息**,等待回复 + + + +> **重要:** 如果你在 OpenClaw 加密设备创建之前发送过消息,那些历史消息**永远无法解密**(这是 Matrix E2EE 的正常行为)。必须发送**新消息**才能触发回复。 + +--- + +## 7. 故障排查 + +| 现象 | 原因 | 解决方案 | +|------|------|---------| +| `channels add` 向导崩溃报 ENOENT | v2026.4.7 Telegram 插件路径 bug | 跳过向导,直接编辑 `~/.openclaw/openclaw.json` | +| Gateway 拒绝启动:"missing gateway.mode" | 配置文件缺少 `gateway` 配置节 | 添加 `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw 默认阻止连接私有 IP | 添加 `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix 连接失败,反复重试 | `homeserver` 使用了 `https` 但本地 Palpo 没有 TLS | 改为 `http://127.0.0.1:8128` | +| 启动报 "Invalid input: expected record, received array" | `providers` 格式写成了数组 | `providers` 是对象(key-value),不是数组 | +| 启动报 "Unrecognized key: type" | 字段名写错 | 用 `"api"` 而不是 `"type"` | +| "missing env var DEEPSEEK_API_KEY" | 环境变量对 LaunchAgent 不可见 | API key 直接写进配置文件 | +| 消息发出但 Bot 不回复(无错误) | DM 默认加密,但 OpenClaw 没开 | 添加 `"encryption": true` | +| "encrypted event received without encryption enabled" | 同上 | 添加 `"encryption": true` | +| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | +| Cross-signing bootstrap 报 "unknown db error" | Palpo 的 `keys/signatures/upload` 接口 bug | 不影响基本加密功能,可忽略 | +| Bot 回复为空或报错 | LLM API Key 无效或余额不足 | 检查 DeepSeek API Key 和账户余额 | +| Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | +| 其他 OpenClaw 问题 | — | 查阅 [OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | + +--- + +## 8. 生产环境配置 + +测试通过后,收紧权限。修改 `channels.matrix` 中的以下字段: + +```json +{ + "autoJoin": "allowlist", + "autoJoinAllowlist": ["!room-id:your-server"], + "dm": { + "policy": "allowlist", + "allowFrom": ["@admin:your-server"], + "sessionScope": "per-room" + }, + "groupPolicy": "allowlist", + "groupAllowFrom": ["@admin:your-server"], + "groups": { + "!room-id:your-server": { + "requireMention": true + } + } +} +``` + +| 字段 | 测试值 | 生产值 | 说明 | +|------|--------|--------|------| +| `autoJoin` | `"always"` | `"allowlist"` | 只加入白名单中的房间 | +| `dm.policy` | `"open"` | `"allowlist"` | 只接受白名单用户的私聊 | +| `groupPolicy` | — | `"allowlist"` | 群聊中限制谁可以触发 Bot | +| `requireMention` | — | `true` | 群聊中必须 @Bot 才响应 | + +--- + +## 9. 延伸阅读 + +- **OpenClaw 文档:** [docs.openclaw.ai](https://docs.openclaw.ai/) — OpenClaw 完整文档。 +- **OpenClaw Matrix 插件:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) — 官方 Matrix 频道插件参考。 +- **OpenClaw GitHub:** [github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) — 源码、Issues 和最新发布。 +- **Palpo 部署指南:** [01-deploying-palpo-and-octos-zh.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) — 如何部署本地 Palpo 服务器。 +- **架构原理:** [03-how-robrix-and-openclaw-work-together-zh.md](03-how-robrix-and-openclaw-work-together-zh.md) — OpenClaw 如何连接 Matrix,以及与 Octos AppService 模式的对比。 +- **使用指南:** [02-using-robrix-with-openclaw-zh.md](02-using-robrix-with-openclaw-zh.md) — 如何使用 Robrix 与 OpenClaw 代理对话。 + +--- + +*本指南基于 2026 年 4 月的实测结果编写(OpenClaw v2026.4.7 + Palpo)。OpenClaw 正在快速迭代中,如遇到问题请以 [官方文档](https://docs.openclaw.ai/) 为准。* diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md new file mode 100644 index 000000000..26fe01939 --- /dev/null +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -0,0 +1,356 @@ +# Deployment Guide: OpenClaw + Matrix + +[中文版](01-deploying-openclaw-with-matrix-zh.md) + +> **Goal:** After following this guide, you will have OpenClaw running as an AI agent connected to a Matrix homeserver. You can then use Robrix (or any Matrix client) to chat with OpenClaw-powered AI agents. + +This guide walks you through deploying OpenClaw with Matrix step by step: from creating a Matrix bot account, to configuring the OpenClaw Matrix channel plugin, to verifying the connection end-to-end. + +> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start). +> +> **Want to understand HOW OpenClaw connects to Matrix?** See [Architecture](03-how-robrix-and-openclaw-work-together.md) for the full explanation. + +> **About OpenClaw:** OpenClaw is under rapid development and its CLI and plugin system have a number of bugs (e.g., the `channels add` wizard may crash). This guide documents a configuration approach we have **tested and verified** -- editing the config file directly, bypassing the unstable CLI wizards. If you encounter issues not covered here, consult the [OpenClaw official documentation](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues). + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Quick Start](#2-quick-start) +3. [Creating a Matrix Bot Account](#3-creating-a-matrix-bot-account) +4. [Installing OpenClaw and Initializing the Config Directory](#4-installing-openclaw-and-initializing-the-config-directory) +5. [Writing the Configuration File](#5-writing-the-configuration-file) +6. [Starting and Verifying](#6-starting-and-verifying) +7. [Troubleshooting](#7-troubleshooting) +8. [Production Configuration](#8-production-configuration) +9. [Further Reading](#9-further-reading) + +--- + +## 1. Prerequisites + +| Requirement | Notes | +|-------------|-------| +| **Two Matrix accounts** | One for yourself, one for the OpenClaw bot | +| **Node.js** | v22.16+ or v24+ (recommended) | +| **LLM API Key** | e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | +| **Matrix homeserver** | Local Palpo (recommended, see [Palpo Deployment Guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md)) or public server matrix.org | +| **Robrix** | See [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) | + +--- + +## 2. Quick Start + +``` +1. Register a Matrix bot account (remember the username and password) +2. Install OpenClaw → run openclaw config → edit ~/.openclaw/openclaw.json +3. Run openclaw gateway start +4. In Robrix, message the bot from another account +``` + +See below for detailed steps. + +--- + +## 3. Creating a Matrix Bot Account + +A bot account is just a **regular Matrix account**. OpenClaw logs in automatically using the username and password -- you do not need to manually obtain an Access Token. + +| Server | How to register | Notes | +|--------|----------------|-------| +| **Local Palpo** (recommended) | Register in Robrix | Connect to `http://127.0.0.1:8128` and register a new account | +| **matrix.org** | Register in Robrix or [Element Web](https://app.element.io) | Public server, free, instant | +| **Self-hosted Synapse** | Via Admin API or web registration | Recommended for production | + +When registering, remember: +- **Username** (e.g., `chalice`) +- **Password** + +--- + +## 4. Installing OpenClaw and Initializing the Config Directory + +### 4.1 Install + +```bash +npm install -g openclaw@latest +openclaw --version # verify installation +``` + +### 4.2 Initialize the config directory + +```bash +openclaw config +``` + +> **This command will output an error -- that is expected and can be safely ignored.** The important thing is that it creates the config directory structure under `~/.openclaw/`. All subsequent configuration is done in this directory. + +> **Why not use the `openclaw channels add` wizard?** OpenClaw v2026.4.7's CLI wizard has multiple bugs (Telegram plugin path error crashes the wizard, incomplete parameters, etc.). **Editing the config file directly is the only reliable approach.** + +--- + +## 5. Writing the Configuration File + +Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below for different scenarios. + +### 5.1 Connecting to Local Palpo (Recommended) + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-your-deepseek-key", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "http://127.0.0.1:8128", + "network": { + "dangerouslyAllowPrivateNetwork": true + }, + "userId": "@chalice:127.0.0.1:8128", + "password": "your-password", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.2 Connecting to Public matrix.org + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-your-deepseek-key", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "https://matrix.org", + "userId": "@your-bot:matrix.org", + "password": "your-password", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.3 Configuration Details + +#### `gateway` Configuration + +| Field | Value | Key Notes | +|-------|-------|-----------| +| `mode` | `"local"` | **Required.** Without this field, gateway refuses to start with "missing gateway.mode" error. | + +#### `models.providers` Configuration + +| Field | Value | Key Notes | +|-------|-------|-----------| +| `baseUrl` | `"https://api.deepseek.com/v1"` | **Must include the `/v1` suffix.** DeepSeek uses an OpenAI-compatible API. | +| `apiKey` | `"sk-xxx"` | **Write the key directly as plaintext.** Do not use `${ENV_VAR}` format -- macOS LaunchAgent services cannot read terminal environment variables. After writing, run `chmod 600 ~/.openclaw/openclaw.json` to protect file permissions. | +| `api` | `"openai-completions"` | **Not `type`.** Many online tutorials incorrectly use `"type"` -- the correct field name is `"api"`. | +| `contextWindow` | `164000` | **Must be set high.** OpenClaw's system prompt alone takes 16K+ tokens; the default 4096 will cause errors. DeepSeek Chat supports 164K. | +| `maxTokens` | `8192` | Maximum tokens per reply. | + +> **Note on `providers` format:** `providers` is an object (provider name as key), not an array. `models` inside a provider IS an array. + +#### `channels.matrix` Configuration + +| Field | Value | Key Notes | +|-------|-------|-----------| +| `enabled` | `true` | Enable the Matrix channel. | +| `homeserver` | `"http://127.0.0.1:8128"` | **Local Palpo must use `http`**, not `https` (Palpo has no TLS by default). matrix.org uses `https`. | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **Only needed for local/LAN deployments.** OpenClaw blocks private IPs (127.0.0.1, 10.x, 192.168.x) by default as an anti-SSRF security measure. Not needed when connecting to public servers like matrix.org. | +| `userId` | `"@chalice:127.0.0.1:8128"` | **Must be the full Matrix ID format** `@username:server`. | +| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. | +| `encryption` | `true` | **Strongly recommended.** Matrix DMs enable E2EE by default. Without this, the bot receives encrypted messages it cannot decrypt, resulting in "message sent but no reply". | +| `autoJoin` | `"always"` | Accept all invites during testing. Change to `"allowlist"` in production. | +| `dm.policy` | `"open"` | Allow all DMs during testing. Change to `"allowlist"` in production. | + +#### `plugins` Configuration + +| Field | Value | Key Notes | +|-------|-------|-----------| +| `plugins.entries.matrix.enabled` | `true` | Ensure the Matrix plugin is enabled. | + +### 5.4 Local Palpo vs Public matrix.org Differences + +| Setting | Local Palpo | Public matrix.org | +|---------|------------|-------------------| +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **Required** `true` | **Not needed** (remove the entire `network` block) | +| `userId` format | `@username:127.0.0.1:8128` | `@username:matrix.org` | +| TLS | None (`http`) | Yes (`https`) | +| Registration | Register in Robrix connected to Palpo | Register via Element Web or Robrix | + +--- + +## 6. Starting and Verifying + +### 6.1 Start the Gateway + +```bash +openclaw gateway start +``` + +### 6.2 Check the Logs + +```bash +tail -20 ~/.openclaw/logs/gateway.log +``` + +Confirm you see these key log lines: + +``` +[gateway] agent model: deepseek/deepseek-chat ← LLM config is correct +[gateway] ready (6 plugins, 0.3s) ← Gateway is ready +[matrix] [default] starting provider (http://...) ← Matrix connecting +matrix: logged in as @chalice:127.0.0.1:8128 ← Login successful +matrix: device is verified by its owner and ready for encrypted rooms ← Encryption ready +``` + +### 6.3 Test in Robrix + +1. **Launch Robrix** and log in with your **personal account** +2. **Search for the bot**: Click the search icon, type the bot's Matrix ID (e.g., `@chalice:127.0.0.1:8128`), switch to the **People** tab +3. **Start a DM**: Select the bot to enter a conversation +4. **Send a message** and wait for a reply + + + +> **Important:** If you sent messages before OpenClaw's encryption device was created, those historical messages **can never be decrypted** (this is normal Matrix E2EE behavior). You must send a **new message** to trigger a reply. + +--- + +## 7. Troubleshooting + +| Symptom | Cause | Solution | +|---------|-------|---------| +| `channels add` wizard crashes with ENOENT | v2026.4.7 Telegram plugin path bug | Skip the wizard, edit `~/.openclaw/openclaw.json` directly | +| Gateway refuses to start: "missing gateway.mode" | Config file missing `gateway` section | Add `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw blocks private IPs by default | Add `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix connection fails, keeps retrying | `homeserver` uses `https` but local Palpo has no TLS | Change to `http://127.0.0.1:8128` | +| "Invalid input: expected record, received array" | `providers` format is wrong | `providers` must be an object (key-value), not an array | +| "Unrecognized key: type" | Wrong field name | Use `"api"` instead of `"type"` | +| "missing env var DEEPSEEK_API_KEY" | Environment variable not visible to LaunchAgent | Write API key directly in the config file | +| Message sent but bot does not reply (no error) | DM is encrypted but OpenClaw has encryption disabled | Add `"encryption": true` | +| "encrypted event received without encryption enabled" | Same as above | Add `"encryption": true` | +| "This message was sent before this device logged in" | Historical messages cannot be decrypted | Normal behavior. Send a **new message** | +| Cross-signing bootstrap reports "unknown db error" | Palpo's `keys/signatures/upload` API bug | Does not affect basic encryption, can be ignored | +| Bot replies are empty or error | LLM API key invalid or insufficient balance | Check DeepSeek API key and account balance | +| Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | +| Other OpenClaw issues | — | Consult [OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | + +--- + +## 8. Production Configuration + +After testing, tighten permissions. Modify these fields in `channels.matrix`: + +```json +{ + "autoJoin": "allowlist", + "autoJoinAllowlist": ["!room-id:your-server"], + "dm": { + "policy": "allowlist", + "allowFrom": ["@admin:your-server"], + "sessionScope": "per-room" + }, + "groupPolicy": "allowlist", + "groupAllowFrom": ["@admin:your-server"], + "groups": { + "!room-id:your-server": { + "requireMention": true + } + } +} +``` + +| Field | Testing | Production | Purpose | +|-------|---------|------------|---------| +| `autoJoin` | `"always"` | `"allowlist"` | Only join allowlisted rooms | +| `dm.policy` | `"open"` | `"allowlist"` | Only accept DMs from allowlisted users | +| `groupPolicy` | — | `"allowlist"` | Restrict who can trigger the bot in groups | +| `requireMention` | — | `true` | In group chats, require @mention to respond | + +--- + +## 9. Further Reading + +- **OpenClaw Documentation:** [docs.openclaw.ai](https://docs.openclaw.ai/) -- full OpenClaw documentation. +- **OpenClaw Matrix Plugin:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) -- official Matrix channel plugin reference. +- **OpenClaw GitHub:** [github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) -- source code, issues, and latest releases. +- **Palpo Deployment Guide:** [01-deploying-palpo-and-octos.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) -- how to deploy a local Palpo homeserver. +- **Architecture Guide:** [03-how-robrix-and-openclaw-work-together.md](03-how-robrix-and-openclaw-work-together.md) -- how OpenClaw connects to Matrix, and comparison with the Octos AppService model. +- **Usage Guide:** [02-using-robrix-with-openclaw.md](02-using-robrix-with-openclaw.md) -- how to use Robrix to chat with OpenClaw agents. + +--- + +*This guide is based on tested results from April 2026 (OpenClaw v2026.4.7 + Palpo). OpenClaw is under rapid development -- if you encounter issues, refer to the [official documentation](https://docs.openclaw.ai/) for the latest information.* diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md new file mode 100644 index 000000000..a1395377e --- /dev/null +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md @@ -0,0 +1,124 @@ +# 使用指南:Robrix + OpenClaw + +[English](02-using-robrix-with-openclaw.md) + +> **目标:** 完成本指南后,你将知道如何使用 Robrix 与 OpenClaw AI 代理对话 —— 包括发起会话、使用私聊和房间、以及了解 OpenClaw 功能在 Robrix 中的表现。 + +本指南假设你已经完成了 [部署指南](01-deploying-openclaw-with-matrix-zh.md) 中的配置,OpenClaw gateway 正在运行。 + +**快速索引** + +| 你想做什么 | 跳转到 | +|---|---| +| 与 Bot 发起私聊 | [第 2 节](#2-发起私聊) | +| 邀请 Bot 进入房间 | [第 3 节](#3-在房间中使用) | +| 了解功能兼容性 | [第 4 节](#4-openclaw-功能在-robrix-中的表现) | +| 与 Octos 工作流对比 | [第 5 节](#5-与-octos-工作流的区别) | + +--- + +## 1. 开始之前 + +确认以下条件: + +- [ ] OpenClaw gateway 正在运行(`openclaw gateway status` 显示 `running`) +- [ ] 日志中显示 `matrix: logged in as @bot-name:server` +- [ ] 你有另一个 Matrix 账号(个人账号)用于和 Bot 对话 +- [ ] Robrix 已安装并能连接到同一个 Matrix 服务器 + +--- + +## 2. 发起私聊 + +### 2.1 搜索 Bot + +1. 打开 Robrix,用你的**个人账号**登录 +2. 点击顶部的**搜索图标** +3. 输入 Bot 的完整 Matrix ID,例如 `@chalice:127.0.0.1:8128` +4. 切换到 **People** 标签页 + + + +### 2.2 发送第一条消息 + +1. 选择 Bot,进入对话 +2. 输入消息(例如 "你好"),按回车 +3. 等待 1-3 秒,Bot 应该回复 + + + +> **注意:** 如果 Bot 刚刚部署完成,你之前发送的消息可能无法被解密(因为那些消息的加密密钥没有分发给 Bot 的设备)。这是正常的 Matrix E2EE 行为——发送**新消息**即可。 + +### 2.3 多轮对话 + +OpenClaw 会保持对话上下文。你可以连续提问,Bot 会记住之前的对话内容。上下文窗口大小取决于 LLM 配置(DeepSeek Chat 支持 164K token)。 + +--- + +## 3. 在房间中使用 + +除了私聊,你还可以邀请 Bot 进入群聊房间。 + +### 3.1 创建房间并邀请 Bot + +1. 在 Robrix 中创建一个新房间 +2. 邀请 Bot(输入 Bot 的 Matrix ID) +3. Bot 会自动加入(因为配置了 `autoJoin: "always"`) + + + +### 3.2 在房间中对话 + +- **默认行为:** Bot 会回复房间内的所有消息 +- **如果配置了 `requireMention: true`:** 需要在消息中 @Bot 才会触发回复 + + + +--- + +## 4. OpenClaw 功能在 Robrix 中的表现 + +| OpenClaw 功能 | Robrix 表现 | 说明 | +|--------------|-------------|------| +| **文字消息** | 完全支持 | 标准 Matrix 消息,无兼容性问题 | +| **流式回复** | 部分支持 | OpenClaw 可能分段发送,Robrix 逐段显示 | +| **语音气泡** | 降级显示 | OpenClaw v2026.4.5+ 的语音回复在 Robrix 中显示为附件 | +| **Exec Approval Prompts** | 降级显示 | OpenClaw 的执行审批提示在 Robrix 中显示为普通文本 | +| **多轮上下文** | 完全支持 | OpenClaw 自动维护对话历史 | +| **E2EE 加密** | 完全支持 | 消息在传输中全程加密 | + +--- + +## 5. 与 Octos 工作流的区别 + +如果你之前使用过 Robrix + Palpo + Octos,以下是主要区别: + +| | OpenClaw | Octos | +|---|---|---| +| **Bot 管理** | 无 BotFather 系统。一个 OpenClaw 实例 = 一个 Bot。 | BotFather 可以动态创建多个子 Bot | +| **创建新 Bot** | 部署一个新的 OpenClaw 实例 | 在聊天中输入 `/createbot` 命令 | +| **Bot 发现** | 需要知道 Bot 的 Matrix ID | 可以用 `/listbots` 查看所有可用 Bot | +| **访问控制** | 通过 OpenClaw 的 `dm.policy` 配置 | 通过 AppService 命名空间和 `allowed_senders` | +| **服务器端设置** | 无需任何设置 | 需要注册 AppService YAML | +| **Robrix Bot 设置面板** | 不使用 | 用于配置 BotFather 和创建子 Bot | + +> 想深入了解两种模式的技术差异?参见 [架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + +--- + +## 6. 使用技巧 + +- **私聊 vs 房间**:私聊更适合个人助手场景,Bot 回复所有消息。房间适合团队协作,可以配置 `requireMention` 避免 Bot 过度回复。 +- **切换 LLM**:修改 `~/.openclaw/openclaw.json` 中的 `models.providers` 配置,然后 `openclaw gateway restart`。 +- **Bot 不响应?** 常见原因:LLM API Key 过期、加密设备未验证、`autoJoin` 配置问题。查看 [部署指南 - 故障排查](01-deploying-openclaw-with-matrix-zh.md#7-故障排查)。 + +--- + +## 接下来 + +- [部署指南](01-deploying-openclaw-with-matrix-zh.md) — 配置和部署 OpenClaw + Matrix +- [架构原理](03-how-robrix-and-openclaw-work-together-zh.md) — 了解 OpenClaw 客户端模式 vs Octos AppService 模式 + +--- + +*本指南基于 2026 年 4 月的使用方式编写。最新更新请参见各项目仓库。* diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md new file mode 100644 index 000000000..65a358701 --- /dev/null +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md @@ -0,0 +1,124 @@ +# Usage Guide: Robrix + OpenClaw + +[中文版](02-using-robrix-with-openclaw-zh.md) + +> **Goal:** After following this guide, you will know how to use Robrix to chat with OpenClaw AI agents -- including starting conversations, using DMs and rooms, and understanding how OpenClaw features appear in Robrix. + +This guide assumes you have completed the [Deployment Guide](01-deploying-openclaw-with-matrix.md) and the OpenClaw gateway is running. + +**Quick Reference** + +| What you want to do | Go to | +|---|---| +| Start a DM with the bot | [Section 2](#2-starting-a-dm) | +| Invite the bot to a room | [Section 3](#3-using-the-bot-in-rooms) | +| Understand feature compatibility | [Section 4](#4-openclaw-features-in-robrix) | +| Compare with Octos workflow | [Section 5](#5-differences-from-octos-workflow) | + +--- + +## 1. Before You Start + +Confirm the following: + +- [ ] OpenClaw gateway is running (`openclaw gateway status` shows `running`) +- [ ] Logs show `matrix: logged in as @bot-name:server` +- [ ] You have another Matrix account (your personal account) to chat with the bot +- [ ] Robrix is installed and can connect to the same Matrix server + +--- + +## 2. Starting a DM + +### 2.1 Search for the Bot + +1. Open Robrix and log in with your **personal account** +2. Click the **search icon** at the top +3. Type the bot's full Matrix ID, e.g., `@chalice:127.0.0.1:8128` +4. Switch to the **People** tab + + + +### 2.2 Send the First Message + +1. Select the bot to enter the conversation +2. Type a message (e.g., "Hello"), press Enter +3. Wait 1-3 seconds -- the bot should reply + + + +> **Note:** If the bot was just deployed, messages you sent earlier may not be decryptable (because those messages' encryption keys were not distributed to the bot's device). This is normal Matrix E2EE behavior -- send a **new message** instead. + +### 2.3 Multi-Turn Conversation + +OpenClaw maintains conversation context. You can ask follow-up questions and the bot will remember previous messages. The context window size depends on the LLM configuration (DeepSeek Chat supports 164K tokens). + +--- + +## 3. Using the Bot in Rooms + +In addition to DMs, you can invite the bot to group chat rooms. + +### 3.1 Create a Room and Invite the Bot + +1. Create a new room in Robrix +2. Invite the bot (type the bot's Matrix ID) +3. The bot joins automatically (because `autoJoin: "always"` is configured) + + + +### 3.2 Chat in the Room + +- **Default behavior:** The bot responds to all messages in the room +- **If `requireMention: true` is configured:** You need to @mention the bot to trigger a reply + + + +--- + +## 4. OpenClaw Features in Robrix + +| OpenClaw Feature | Robrix Support | Notes | +|------------------|---------------|-------| +| **Text messages** | Fully supported | Standard Matrix messages, no compatibility issues | +| **Streaming replies** | Partially supported | OpenClaw may send in segments; Robrix displays them incrementally | +| **Voice bubbles** | Fallback display | OpenClaw v2026.4.5+ voice replies appear as attachments in Robrix | +| **Exec Approval Prompts** | Fallback display | OpenClaw's execution approval prompts appear as plain text in Robrix | +| **Multi-turn context** | Fully supported | OpenClaw automatically maintains conversation history | +| **E2EE encryption** | Fully supported | Messages are encrypted end-to-end | + +--- + +## 5. Differences from Octos Workflow + +If you have previously used Robrix + Palpo + Octos, here are the key differences: + +| | OpenClaw | Octos | +|---|---|---| +| **Bot management** | No BotFather system. One OpenClaw instance = one bot. | BotFather can dynamically create multiple child bots | +| **Creating new bots** | Deploy a new OpenClaw instance | Type `/createbot` command in chat | +| **Bot discovery** | Need to know the bot's Matrix ID | Use `/listbots` to see all available bots | +| **Access control** | Via OpenClaw's `dm.policy` configuration | Via AppService namespaces and `allowed_senders` | +| **Server-side setup** | None required | Must register AppService YAML | +| **Robrix Bot Settings panel** | Not used | Used to configure BotFather and create child bots | + +> Want to understand the technical differences in depth? See [Architecture Guide](03-how-robrix-and-openclaw-work-together.md). + +--- + +## 6. Tips + +- **DM vs Room**: DMs are better for personal assistant use cases -- the bot replies to all messages. Rooms are better for team collaboration; configure `requireMention` to prevent excessive replies. +- **Switching LLMs**: Edit the `models.providers` section in `~/.openclaw/openclaw.json`, then run `openclaw gateway restart`. +- **Bot not responding?** Common causes: expired LLM API key, unverified encryption device, `autoJoin` configuration issues. See [Deployment Guide - Troubleshooting](01-deploying-openclaw-with-matrix.md#7-troubleshooting). + +--- + +## What's Next + +- [Deployment Guide](01-deploying-openclaw-with-matrix.md) -- set up and configure OpenClaw with Matrix +- [Architecture Guide](03-how-robrix-and-openclaw-work-together.md) -- understand OpenClaw client mode vs Octos AppService mode + +--- + +*This guide covers usage as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md new file mode 100644 index 000000000..ee5ceb246 --- /dev/null +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md @@ -0,0 +1,215 @@ +# 架构原理:Robrix 与 OpenClaw 如何协作 + +[English](03-how-robrix-and-openclaw-work-together.md) + +> **目标:** 阅读本文档后,你将理解 OpenClaw 如何以普通客户端身份连接 Matrix,消息如何在 Robrix、Matrix 服务器和 OpenClaw AI 代理之间流转,以及这与 Octos 使用的 Application Service 模式有何本质区别。 + +本文档解释 Robrix + OpenClaw 系统背后的**机制**。如需部署,请参见 [部署指南](01-deploying-openclaw-with-matrix-zh.md)。如需使用,请参见 [使用指南](02-using-robrix-with-openclaw-zh.md)。 + +--- + +## 目录 + +1. [两个项目概览](#1-两个项目概览) +2. [OpenClaw 如何连接 Matrix](#2-openclaw-如何连接-matrix) +3. [消息生命周期](#3-消息生命周期) +4. [客户端模式 vs Application Service 模式](#4-客户端模式-vs-application-service-模式) +5. [端到端加密(E2EE)](#5-端到端加密e2ee) +6. [延伸阅读](#6-延伸阅读) + +--- + +## 1. 两个项目概览 + +| 项目 | 角色 | 作用 | +|------|------|------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 使用 Rust + [Makepad](https://github.com/makepad/makepad/) 构建的跨平台 Matrix 聊天客户端。这是用户界面——你在这里读写消息。 | +| [**OpenClaw**](https://github.com/openclaw/openclaw) | AI 代理框架 | 开源 AI 助手平台,通过 Matrix 频道插件以**普通客户端**身份登录 Matrix 服务器。接收用户消息,调用 LLM 生成回复,再发送回房间。 | + +两个项目完全独立。OpenClaw 不是专为 Robrix 设计的,也不是专为 Matrix 设计的——它支持 Telegram、Discord、Slack 等多种频道。Matrix 只是其中之一。 + +--- + +## 2. OpenClaw 如何连接 Matrix + +OpenClaw 通过 **Client-Server API** 连接 Matrix,方式和 Robrix 一样——它就是一个普通的 Matrix 客户端。 + +### 连接流程 + +``` +1. OpenClaw 启动时,用 userId + password 调用 POST /_matrix/client/v3/login +2. 服务器返回 access_token,OpenClaw 缓存到 ~/.openclaw/credentials/ +3. OpenClaw 开始 Sliding Sync 循环,持续拉取新事件 +4. 收到 m.room.message 事件时,提取消息内容,调用 LLM +5. LLM 返回回复后,OpenClaw 通过 PUT /_matrix/client/v3/rooms/{roomId}/send/ 发送回复 +``` + +### 关键特征 + +- **登录方式**:用户名 + 密码(和普通用户一样) +- **消息获取**:通过 Sync 主动拉取(不是服务器推送) +- **权限级别**:和普通用户完全一样(受速率限制、需要被邀请才能加入房间) +- **底层 SDK**:OpenClaw 的 Matrix 插件基于 [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk)(官方 JavaScript SDK) + +--- + +## 3. 消息生命周期 + +### 数据流图 + +``` +用户在 Robrix 中输入 "你好" + | + v ++-----------------+ +| 1. Robrix 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 通过 CS API | -> Palpo (http://127.0.0.1:8128) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo 存储 | 事件写入 PostgreSQL +| 事件 | 房间状态更新 ++--------+--------+ + | + v ++-----------------+ +| 3. OpenClaw | 通过 Sliding Sync 拉取到新事件 +| 收到消息 | (OpenClaw 是普通客户端,主动同步) ++--------+--------+ + | + v ++-----------------+ +| 4. OpenClaw | POST https://api.deepseek.com/v1/chat/completions +| 调用 LLM | 携带对话历史作为上下文 ++--------+--------+ + | + v ++-----------------+ +| 5. OpenClaw | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 发送回复 | -> Palpo (http://127.0.0.1:8128) +| 通过 CS API | 认证:Bearer {access_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo 存储 | Bot 的回复事件写入数据库 +| 并推送 | Sliding Sync 推送到 Robrix ++--------+--------+ + | + v +用户在 Robrix 中看到 AI 回复 +``` + +### 架构图 + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Client-Server API | OpenClaw | HTTPS | LLM | +| (客户端) | --------------------> | (服务器) | <------------------> | (AI Bot) | ------> | | +| | <-------------------- | | Sliding Sync | | <------ | | ++----------+ Sliding Sync +----------+ +----------+ +-----+ + 你的电脑 Docker :8128 你的电脑 外部 API +``` + +**关键观察:** + +- **Robrix 和 OpenClaw 对 Palpo 来说地位完全平等** —— 都是通过 Client-Server API 连接的普通客户端。 +- **OpenClaw 不依赖服务器端配置** —— 不需要修改 Palpo 的任何配置文件(对比 Octos 需要注册 AppService YAML)。 +- **只有 LLM 调用离开本机** —— Robrix ↔ Palpo ↔ OpenClaw 全部在本地网络,只有 DeepSeek API 调用走公网。 + +--- + +## 4. 客户端模式 vs Application Service 模式 + +Robrix 生态中有两种接入 AI Bot 的方式:OpenClaw 使用的**客户端模式**和 Octos 使用的 **Application Service 模式**。它们的核心区别如下: + +### 4.1 连接机制对比 + +| | OpenClaw(客户端模式) | Octos(Application Service 模式) | +|---|---|---| +| **连接方式** | 和普通用户一样,用密码登录 | 通过 YAML 注册文件在服务器端注册 | +| **消息获取** | **Sync 拉取**——OpenClaw 主动轮询服务器获取新事件 | **服务器推送**——Palpo 主动将事件推送到 Octos 的 HTTP 端点 | +| **认证方式** | access_token(用户级别) | as_token / hs_token(服务级别,双向认证) | +| **服务器端配置** | **无需任何配置**——Bot 就是一个普通用户 | **需要注册**——在 Palpo 的 `appservice_registration_dir` 放置 YAML 文件 | +| **用户命名空间** | 只有一个用户 ID | 可以声明排他的用户命名空间,动态创建子 Bot | +| **速率限制** | 受限(和普通用户一样) | 不受限(`rate_limited: false`) | + +### 4.2 能力对比 + +| 能力 | OpenClaw | Octos | +|------|----------|-------| +| 基本对话 | 支持 | 支持 | +| 多模型切换 | 支持(14+ LLM provider) | 支持 | +| E2EE 加密 | 支持(Rust crypto SDK) | 不需要(AppService 绕过加密) | +| 动态创建子 Bot | 不支持(一个实例 = 一个 Bot) | 支持(BotFather 模式) | +| 服务器端管理 | 不需要 | 需要管理员权限注册 AppService | +| 多频道(Telegram、Discord 等) | 支持 | 仅 Matrix | +| 对 homeserver 的要求 | 任何标准 Matrix 服务器 | 需要支持 AppService API | + +### 4.3 消息延迟 + +| 环节 | OpenClaw | Octos | +|------|----------|-------| +| 消息到达 Bot | Sync 间隔(通常 1-5 秒) | 即时推送(< 100ms) | +| LLM 响应 | 取决于 LLM provider | 取决于 LLM provider | +| Bot 发送回复 | 即时 | 即时 | + +> OpenClaw 使用 Sliding Sync 的长轮询模式,实际延迟通常在 1-2 秒,对于聊天场景几乎感觉不到。 + +### 4.4 部署复杂度 + +| | OpenClaw | Octos | +|---|---|---| +| 服务器端 | 无需配置 | 需要放置注册 YAML、配置 token | +| 客户端 | 安装 OpenClaw + 编辑一个 JSON 文件 | 需要 Docker Compose 编排三个服务 | +| Token 管理 | 密码自动登录,token 自动缓存 | 需要手动生成并同步 as_token / hs_token | +| 架构复杂度 | 简单(一个进程) | 复杂(Palpo + Octos + PostgreSQL) | + +### 4.5 什么时候用哪个? + +| 场景 | 推荐方案 | +|------|---------| +| 快速测试 AI 对话 | **OpenClaw** —— 5 分钟配好,不需要碰服务器 | +| 个人 AI 助手 | **OpenClaw** —— 简单、灵活、支持多频道 | +| 团队内多个专业 Bot | **Octos** —— BotFather 可以动态创建多个子 Bot | +| 需要服务器端管理 | **Octos** —— AppService 由管理员注册和控制 | +| 高并发消息 | **Octos** —— 服务器推送 + 无速率限制 | +| 跨平台 AI 代理(同时接入 Telegram/Discord) | **OpenClaw** —— 原生支持多频道 | + +--- + +## 5. 端到端加密(E2EE) + +### OpenClaw 如何处理加密 + +OpenClaw 的 Matrix 插件使用 matrix-js-sdk 的 **Rust crypto 路径**,实现了 Olm(一对一密钥交换)和 Megolm(群组加密)协议。 + +当 `"encryption": true` 配置启用后: + +1. **首次登录**:OpenClaw 创建加密设备,生成 cross-signing identity +2. **自动引导**:执行 secret storage bootstrap,设备被标记为 "verified by its owner" +3. **接收消息**:OpenClaw 解密 Megolm 加密的消息 +4. **发送回复**:回复自动加密发送 + +### 注意事项 + +- **历史消息不可解密** —— 在 OpenClaw 设备创建之前发送的消息,其 Megolm 会话密钥未分发给 OpenClaw,永远无法解密。 +- **Palpo 的 cross-signing bug** —— `keys/signatures/upload` 可能返回 "unknown db error",但不影响基本加密功能。 +- **vs Octos** —— Octos 作为 AppService 接收的是**服务器端解密后的明文事件**,不需要处理 E2EE。OpenClaw 作为客户端必须自己处理加密。 + +--- + +## 6. 延伸阅读 + +- **OpenClaw 文档:** [docs.openclaw.ai](https://docs.openclaw.ai/) — OpenClaw 完整文档。 +- **OpenClaw Matrix 插件:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) — 官方 Matrix 频道插件参考。 +- **Matrix Client-Server API 规范:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) — OpenClaw 使用的协议。 +- **Matrix Application Service API 规范:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) — Octos 使用的协议。 +- **Octos 架构原理:** [02-how-robrix-palpo-octos-work-together-zh.md](../robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md) — Octos AppService 模式的完整解析。 +- **部署指南:** [01-deploying-openclaw-with-matrix-zh.md](01-deploying-openclaw-with-matrix-zh.md) — 如何部署 OpenClaw + Matrix。 +- **使用指南:** [02-using-robrix-with-openclaw-zh.md](02-using-robrix-with-openclaw-zh.md) — 如何使用 Robrix 与 OpenClaw 代理对话。 + +--- + +*本文档基于 2026 年 4 月的实测结果编写。最新更新请参见各项目仓库。* diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md new file mode 100644 index 000000000..7e8e75968 --- /dev/null +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md @@ -0,0 +1,215 @@ +# Architecture: How Robrix and OpenClaw Work Together + +[中文版](03-how-robrix-and-openclaw-work-together-zh.md) + +> **Goal:** After reading this document, you will understand how OpenClaw connects to Matrix as a regular client, how messages flow between Robrix, the Matrix homeserver, and the OpenClaw AI agent, and how this fundamentally differs from the Application Service model used by Octos. + +This document explains the **mechanisms** behind the Robrix + OpenClaw system. If you want to deploy it, see [Deployment Guide](01-deploying-openclaw-with-matrix.md). If you want to use it, see [Usage Guide](02-using-robrix-with-openclaw.md). + +--- + +## Table of Contents + +1. [Two Projects Overview](#1-two-projects-overview) +2. [How OpenClaw Connects to Matrix](#2-how-openclaw-connects-to-matrix) +3. [Message Lifecycle](#3-message-lifecycle) +4. [Client Mode vs Application Service Mode](#4-client-mode-vs-application-service-mode) +5. [End-to-End Encryption (E2EE)](#5-end-to-end-encryption-e2ee) +6. [Further Reading](#6-further-reading) + +--- + +## 1. Two Projects Overview + +| Project | Role | What it does | +|---------|------|--------------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client built with Rust + [Makepad](https://github.com/makepad/makepad/). This is the user interface -- where you read and send messages. | +| [**OpenClaw**](https://github.com/openclaw/openclaw) | AI Agent Framework | An open-source AI assistant platform that connects to Matrix via its channel plugin as a **regular client**. Receives user messages, calls an LLM to generate replies, and sends them back to the room. | + +Both projects are completely independent. OpenClaw is not designed specifically for Robrix or for Matrix -- it supports Telegram, Discord, Slack, and other channels. Matrix is just one of them. + +--- + +## 2. How OpenClaw Connects to Matrix + +OpenClaw connects to Matrix via the **Client-Server API**, the same way Robrix does -- it is simply a regular Matrix client. + +### Connection Flow + +``` +1. On startup, OpenClaw calls POST /_matrix/client/v3/login with userId + password +2. Server returns an access_token, OpenClaw caches it at ~/.openclaw/credentials/ +3. OpenClaw starts a Sliding Sync loop, continuously pulling new events +4. When an m.room.message event arrives, it extracts the content and calls the LLM +5. After the LLM responds, OpenClaw sends the reply via PUT /_matrix/client/v3/rooms/{roomId}/send/ +``` + +### Key Characteristics + +- **Authentication**: Username + password (same as any regular user) +- **Message retrieval**: Via Sync (actively pulls from server, not pushed by server) +- **Permission level**: Identical to a regular user (rate-limited, must be invited to join rooms) +- **Underlying SDK**: OpenClaw's Matrix plugin uses [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) (official JavaScript SDK) + +--- + +## 3. Message Lifecycle + +### Data Flow Diagram + +``` +User types "Hello" in Robrix + | + v ++-----------------+ +| 1. Robrix sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| via CS API | -> Palpo (http://127.0.0.1:8128) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo stores | Event saved to PostgreSQL +| the event | Room state updated ++--------+--------+ + | + v ++-----------------+ +| 3. OpenClaw | Receives new event via Sliding Sync +| gets message | (OpenClaw is a regular client, actively syncing) ++--------+--------+ + | + v ++-----------------+ +| 4. OpenClaw | POST https://api.deepseek.com/v1/chat/completions +| calls LLM | With conversation history as context ++--------+--------+ + | + v ++-----------------+ +| 5. OpenClaw | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| sends reply | -> Palpo (http://127.0.0.1:8128) +| via CS API | Auth: Bearer {access_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo stores | Bot's reply event saved +| & delivers | Sliding Sync pushes to Robrix ++--------+--------+ + | + v +User sees AI reply in Robrix +``` + +### Architecture Diagram + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Client-Server API | OpenClaw | HTTPS | LLM | +| (Client) | --------------------> | (Server) | <------------------> | (AI Bot) | ------> | | +| | <-------------------- | | Sliding Sync | | <------ | | ++----------+ Sliding Sync +----------+ +----------+ +-----+ + Your machine Docker :8128 Your machine External +``` + +**Key observations:** + +- **Robrix and OpenClaw are equal peers to Palpo** -- both connect via the Client-Server API as regular clients. +- **OpenClaw requires no server-side configuration** -- no need to modify any Palpo config files (compare with Octos which requires AppService YAML registration). +- **Only the LLM call leaves local network** -- Robrix ↔ Palpo ↔ OpenClaw all stay on localhost; only the DeepSeek API call goes to the internet. + +--- + +## 4. Client Mode vs Application Service Mode + +The Robrix ecosystem offers two ways to integrate AI bots: OpenClaw's **client mode** and Octos's **Application Service mode**. Their core differences are: + +### 4.1 Connection Mechanism Comparison + +| | OpenClaw (Client Mode) | Octos (Application Service Mode) | +|---|---|---| +| **Connection** | Logs in with password, same as a regular user | Registered on the server via a YAML registration file | +| **Message retrieval** | **Sync pull** -- OpenClaw actively polls the server for new events | **Server push** -- Palpo actively pushes events to Octos's HTTP endpoint | +| **Authentication** | access_token (user-level) | as_token / hs_token (service-level, mutual authentication) | +| **Server-side config** | **None required** -- the bot is just a regular user | **Registration required** -- place YAML file in Palpo's `appservice_registration_dir` | +| **User namespaces** | Only one user ID | Can claim exclusive user namespaces, dynamically create child bots | +| **Rate limiting** | Subject to limits (same as regular users) | Exempt (`rate_limited: false`) | + +### 4.2 Capability Comparison + +| Capability | OpenClaw | Octos | +|------------|----------|-------| +| Basic conversation | Yes | Yes | +| Multiple LLM providers | Yes (14+) | Yes | +| E2EE encryption | Yes (Rust crypto SDK) | Not needed (AppService bypasses encryption) | +| Dynamic child bots | No (one instance = one bot) | Yes (BotFather pattern) | +| Server-side administration | Not needed | Requires admin access to register AppService | +| Multi-channel (Telegram, Discord, etc.) | Yes | Matrix only | +| Homeserver requirements | Any standard Matrix server | Must support Application Service API | + +### 4.3 Message Latency + +| Stage | OpenClaw | Octos | +|-------|----------|-------| +| Message reaches bot | Sync interval (typically 1-5 seconds) | Instant push (< 100ms) | +| LLM response | Depends on LLM provider | Depends on LLM provider | +| Bot sends reply | Instant | Instant | + +> OpenClaw uses Sliding Sync's long-polling mode, so actual latency is typically 1-2 seconds -- barely noticeable in a chat context. + +### 4.4 Deployment Complexity + +| | OpenClaw | Octos | +|---|---|---| +| Server side | No configuration needed | Requires registration YAML, token configuration | +| Client side | Install OpenClaw + edit one JSON file | Requires Docker Compose orchestrating three services | +| Token management | Password auto-login, token auto-cached | Must manually generate and synchronize as_token / hs_token | +| Architecture complexity | Simple (single process) | Complex (Palpo + Octos + PostgreSQL) | + +### 4.5 When to Use Which? + +| Scenario | Recommended | +|----------|-------------| +| Quick-test AI conversation | **OpenClaw** -- 5 minutes to configure, no server changes needed | +| Personal AI assistant | **OpenClaw** -- simple, flexible, multi-channel support | +| Team with multiple specialized bots | **Octos** -- BotFather can dynamically create child bots | +| Server-side administration needed | **Octos** -- AppService is registered and controlled by admins | +| High-concurrency messages | **Octos** -- server push + no rate limits | +| Cross-platform AI agent (Telegram/Discord simultaneously) | **OpenClaw** -- native multi-channel support | + +--- + +## 5. End-to-End Encryption (E2EE) + +### How OpenClaw Handles Encryption + +OpenClaw's Matrix plugin uses matrix-js-sdk's **Rust crypto path**, implementing the Olm (one-to-one key exchange) and Megolm (group encryption) protocols. + +When `"encryption": true` is configured: + +1. **First login**: OpenClaw creates an encryption device and generates a cross-signing identity +2. **Auto-bootstrap**: Executes secret storage bootstrap; device is marked "verified by its owner" +3. **Receiving messages**: OpenClaw decrypts Megolm-encrypted messages +4. **Sending replies**: Replies are automatically encrypted + +### Important Notes + +- **Historical messages cannot be decrypted** -- Messages sent before OpenClaw's device was created did not have their Megolm session keys distributed to OpenClaw. They can never be decrypted. +- **Palpo cross-signing bug** -- `keys/signatures/upload` may return "unknown db error", but this does not affect basic encryption functionality. +- **vs Octos** -- Octos, as an AppService, receives **server-side decrypted plaintext events**. It does not need to handle E2EE at all. OpenClaw, as a client, must handle encryption itself. + +--- + +## 6. Further Reading + +- **OpenClaw Documentation:** [docs.openclaw.ai](https://docs.openclaw.ai/) -- full OpenClaw documentation. +- **OpenClaw Matrix Plugin:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) -- official Matrix channel plugin reference. +- **Matrix Client-Server API Spec:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- the protocol OpenClaw uses. +- **Matrix Application Service API Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the protocol Octos uses. +- **Octos Architecture Guide:** [02-how-robrix-palpo-octos-work-together.md](../robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) -- full explanation of the Octos AppService model. +- **Deployment Guide:** [01-deploying-openclaw-with-matrix.md](01-deploying-openclaw-with-matrix.md) -- how to deploy OpenClaw with Matrix. +- **Usage Guide:** [02-using-robrix-with-openclaw.md](02-using-robrix-with-openclaw.md) -- how to use Robrix to chat with OpenClaw agents. + +--- + +*This document is based on tested results from April 2026. For the latest updates, see the respective project repositories.* From 82cd640606b735ecb149c6670f39ad98d5c141ec Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 12:30:23 +0800 Subject: [PATCH 126/283] =?UTF-8?q?docs:=20reorder=20guides=20to=20deploy?= =?UTF-8?q?=20=E2=86=92=20usage=20=E2=86=92=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap doc ordering for both Palpo+Octos and OpenClaw series: - 02: Usage guide (was 03) — let users get started first - 03: Architecture/principles (was 02) — deeper reading for those interested Update all cross-references across 11 files and README. --- docs/README.md | 8 ++++---- .../03-how-robrix-and-openclaw-work-together-zh.md | 2 +- .../03-how-robrix-and-openclaw-work-together.md | 2 +- .../01-deploying-palpo-and-octos-zh.md | 8 ++++---- .../01-deploying-palpo-and-octos.md | 8 ++++---- ...s-zh.md => 02-using-robrix-with-palpo-and-octos-zh.md} | 6 +++--- ...d-octos.md => 02-using-robrix-with-palpo-and-octos.md} | 6 +++--- ...h.md => 03-how-robrix-palpo-octos-work-together-zh.md} | 8 ++++---- ...ther.md => 03-how-robrix-palpo-octos-work-together.md} | 8 ++++---- 9 files changed, 28 insertions(+), 28 deletions(-) rename docs/robrix-with-palpo-and-octos/{03-using-robrix-with-palpo-and-octos-zh.md => 02-using-robrix-with-palpo-and-octos-zh.md} (98%) rename docs/robrix-with-palpo-and-octos/{03-using-robrix-with-palpo-and-octos.md => 02-using-robrix-with-palpo-and-octos.md} (98%) rename docs/robrix-with-palpo-and-octos/{02-how-robrix-palpo-octos-work-together-zh.md => 03-how-robrix-palpo-octos-work-together-zh.md} (98%) rename docs/robrix-with-palpo-and-octos/{02-how-robrix-palpo-octos-work-together.md => 03-how-robrix-palpo-octos-work-together.md} (98%) diff --git a/docs/README.md b/docs/README.md index ffc3070f7..db21b13e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,14 +23,14 @@ For users who want to deploy a complete AI chat system — running your own Matr | Guide | Goal | |-------|------| | [1. Deploying Palpo and Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) | **Get Palpo homeserver and Octos AI bot running.** Clone, configure, and launch all backend services with Docker Compose so Robrix can connect to your own server. | -| [2. How Robrix, Palpo, and Octos Work Together](robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) | **Understand the Application Service mechanism.** Learn how Octos registers as a Matrix App Service on Palpo, how messages flow from Robrix through Palpo to the AI bot, and how the BotFather system manages multiple bots. | -| [3. Using Robrix with Palpo and Octos](robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md) | **Use Robrix to chat with AI bots on your Palpo server.** Step-by-step with screenshots: log in, create rooms, invite bots, have conversations, and manage bots through the BotFather system. | +| [2. Using Robrix with Palpo and Octos](robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md) | **Use Robrix to chat with AI bots on your Palpo server.** Step-by-step with screenshots: log in, create rooms, invite bots, have conversations, and manage bots through the BotFather system. | +| [3. How Robrix, Palpo, and Octos Work Together](robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md) | **Understand the Application Service mechanism.** Learn how Octos registers as a Matrix App Service on Palpo, how messages flow from Robrix through Palpo to the AI bot, and how the BotFather system manages multiple bots. | | [4. Federation with Palpo](robrix-with-palpo-and-octos/04-federation-with-palpo.md) | **Enable cross-server communication.** Configure Palpo for Matrix federation so users on different servers can chat with each other and access your AI bots. | > Chinese: > [1. 部署 Palpo 和 Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) · -> [2. Robrix、Palpo、Octos 协作原理](robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md) · -> [3. 在 Robrix 上使用 Palpo 和 Octos](robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md) · +> [2. 在 Robrix 上使用 Palpo 和 Octos](robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md) · +> [3. Robrix、Palpo、Octos 协作原理](robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md) · > [4. Palpo 联邦功能](robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md) --- diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md index ee5ceb246..26ac77ebb 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md @@ -206,7 +206,7 @@ OpenClaw 的 Matrix 插件使用 matrix-js-sdk 的 **Rust crypto 路径**,实 - **OpenClaw Matrix 插件:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) — 官方 Matrix 频道插件参考。 - **Matrix Client-Server API 规范:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) — OpenClaw 使用的协议。 - **Matrix Application Service API 规范:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) — Octos 使用的协议。 -- **Octos 架构原理:** [02-how-robrix-palpo-octos-work-together-zh.md](../robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md) — Octos AppService 模式的完整解析。 +- **Octos 架构原理:** [03-how-robrix-palpo-octos-work-together-zh.md](../robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md) — Octos AppService 模式的完整解析。 - **部署指南:** [01-deploying-openclaw-with-matrix-zh.md](01-deploying-openclaw-with-matrix-zh.md) — 如何部署 OpenClaw + Matrix。 - **使用指南:** [02-using-robrix-with-openclaw-zh.md](02-using-robrix-with-openclaw-zh.md) — 如何使用 Robrix 与 OpenClaw 代理对话。 diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md index 7e8e75968..f9d51d268 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md @@ -206,7 +206,7 @@ When `"encryption": true` is configured: - **OpenClaw Matrix Plugin:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) -- official Matrix channel plugin reference. - **Matrix Client-Server API Spec:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- the protocol OpenClaw uses. - **Matrix Application Service API Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the protocol Octos uses. -- **Octos Architecture Guide:** [02-how-robrix-palpo-octos-work-together.md](../robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) -- full explanation of the Octos AppService model. +- **Octos Architecture Guide:** [03-how-robrix-palpo-octos-work-together.md](../robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md) -- full explanation of the Octos AppService model. - **Deployment Guide:** [01-deploying-openclaw-with-matrix.md](01-deploying-openclaw-with-matrix.md) -- how to deploy OpenClaw with Matrix. - **Usage Guide:** [02-using-robrix-with-openclaw.md](02-using-robrix-with-openclaw.md) -- how to use Robrix to chat with OpenClaw agents. diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md index cdaabb7f5..000c67041 100644 --- a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md @@ -8,7 +8,7 @@ > **只想快速试试?** 跳到 [快速开始](#2-快速开始) — 5 步即可运行。 > -> **想了解每个配置背后的原理?** 参阅 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md) 了解完整解释。 +> **想了解每个配置背后的原理?** 参阅 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md) 了解完整解释。 --- @@ -115,7 +115,7 @@ docker compose ps 本节解释 `palpo-and-octos-deploy/` 目录中的每个配置文件。快速开始已经让你跑起来了——当你需要自定义时再来这里查阅。 -> **注意:** 想了解架构以及每个组件为何如此配置,请参阅 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md)。 +> **注意:** 想了解架构以及每个组件为何如此配置,请参阅 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md)。 ### 3.1 目录结构 @@ -287,7 +287,7 @@ client = "http://127.0.0.1:8128" | `port` | Octos 监听 Palpo 应用服务事件的端口。 | | `allowed_senders` | 允许与机器人对话的 Matrix 用户 ID。空数组 `[]` = 所有人都可以对话。 | -> **重要:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关但不能混用。详见 [架构原理](02-how-robrix-palpo-octos-work-together-zh.md)。 +> **重要:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关但不能混用。详见 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md)。 **Gateway 设置:** @@ -493,7 +493,7 @@ docker compose up -d - **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo 主服务器文档。 - **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix 客户端、构建说明和功能追踪。 - **Matrix Appservice 规范:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — 应用服务的 Matrix 协议规范。 -- **架构原理:** [02-how-robrix-palpo-octos-work-together-zh.md](02-how-robrix-palpo-octos-work-together-zh.md) — 应用服务机制如何运作、消息生命周期和 BotFather 系统。 +- **架构原理:** [03-how-robrix-palpo-octos-work-together-zh.md](03-how-robrix-palpo-octos-work-together-zh.md) — 应用服务机制如何运作、消息生命周期和 BotFather 系统。 --- diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md index 2d7b971ab..a741bac7f 100644 --- a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md @@ -8,7 +8,7 @@ This guide walks you through deploying the backend services step by step: from c > **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start) -- 5 steps to get running. > -> **Want to understand WHY things are configured this way?** See [Architecture](02-how-robrix-palpo-octos-work-together.md) for the full explanation. +> **Want to understand WHY things are configured this way?** See [Architecture](03-how-robrix-palpo-octos-work-together.md) for the full explanation. --- @@ -115,7 +115,7 @@ You should see three services (`palpo_postgres`, `palpo`, `octos`) all in `runni This section explains every configuration file in the `palpo-and-octos-deploy/` directory. You already have a working setup from the Quick Start -- come here when you want to customize. -> **Note:** To understand the architecture and WHY each component is configured this way, see [Architecture](02-how-robrix-palpo-octos-work-together.md). +> **Note:** To understand the architecture and WHY each component is configured this way, see [Architecture](03-how-robrix-palpo-octos-work-together.md). ### 3.1 Directory Layout @@ -287,7 +287,7 @@ This file defines the bot's identity, LLM provider, and Matrix channel configura | `port` | Port Octos listens on for Appservice events from Palpo. | | `allowed_senders` | Matrix user IDs allowed to talk to the bot. Empty `[]` = everyone. | -> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related but not interchangeable. See [Architecture](02-how-robrix-palpo-octos-work-together.md) for why. +> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related but not interchangeable. See [Architecture](03-how-robrix-palpo-octos-work-together.md) for why. **Gateway settings:** @@ -493,7 +493,7 @@ docker compose up -d - **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver documentation. - **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client, build instructions, and feature tracker. - **Matrix Appservice Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the Matrix protocol specification for application services. -- **Architecture Guide:** [02-how-robrix-palpo-octos-work-together.md](02-how-robrix-palpo-octos-work-together.md) -- how the Appservice mechanism works, message lifecycle, and BotFather system. +- **Architecture Guide:** [03-how-robrix-palpo-octos-work-together.md](03-how-robrix-palpo-octos-work-together.md) -- how the Appservice mechanism works, message lifecycle, and BotFather system. --- diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md similarity index 98% rename from docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md rename to docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md index 33a507532..e25303513 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos-zh.md +++ b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md @@ -1,6 +1,6 @@ # 使用指南:Robrix + Palpo + Octos -[English Version](03-using-robrix-with-palpo-and-octos.md) +[English Version](02-using-robrix-with-palpo-and-octos.md) > **目标:** 按照本指南操作后,你将掌握如何使用 Robrix 连接 Palpo 服务器、注册账号、创建房间、邀请 AI 机器人、进行对话,以及通过 BotFather 系统管理机器人——全部配有分步截图演示。 @@ -145,7 +145,7 @@ Octos 支持"BotFather"模式:主机器人(`@octosbot`)可以创建**子机器人**,每个子机器人拥有自己的个性和系统提示词。这对于构建专业化的 AI 助手非常有用。 -如需深入了解其工作原理,请参阅 [架构指南](02-how-robrix-palpo-octos-work-together-zh.md)。 +如需深入了解其工作原理,请参阅 [架构指南](03-how-robrix-palpo-octos-work-together-zh.md)。 ### 6.1 在 Robrix 中启用 App Service 支持 @@ -284,4 +284,4 @@ BotFather 和子机器人的角色不同: ## 接下来 - [部署指南](01-deploying-palpo-and-octos-zh.md) -- 搭建和配置服务 -- [架构指南](02-how-robrix-palpo-octos-work-together-zh.md) -- 了解各组件如何协同工作 +- [架构指南](03-how-robrix-palpo-octos-work-together-zh.md) -- 了解各组件如何协同工作 diff --git a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md similarity index 98% rename from docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md rename to docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md index 7bb020bcb..dc3937b2b 100644 --- a/docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md +++ b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md @@ -1,6 +1,6 @@ # Usage Guide: Robrix + Palpo + Octos -[中文版](03-using-robrix-with-palpo-and-octos-zh.md) +[中文版](02-using-robrix-with-palpo-and-octos-zh.md) > **Goal:** After following this guide, you will know how to use Robrix to connect to your Palpo server, register an account, create rooms, invite AI bots, have conversations, and manage bots through the BotFather system — all demonstrated with step-by-step screenshots. @@ -146,7 +146,7 @@ If someone else has already created a room with the bot and invited you, or if a Octos supports a "BotFather" pattern: the main bot (`@octosbot`) can create **child bots**, each with its own personality and system prompt. This is useful for building specialized assistants. -For a deeper understanding of how this works, see the [Architecture Guide](02-how-robrix-palpo-octos-work-together.md). +For a deeper understanding of how this works, see the [Architecture Guide](03-how-robrix-palpo-octos-work-together.md). ### 6.1 Enable App Service Support in Robrix @@ -284,4 +284,4 @@ For remote deployments, replace `127.0.0.1:8128` with your configured `server_na ## What's Next - [Deployment Guide](01-deploying-palpo-and-octos.md) -- set up and configure services -- [Architecture Guide](02-how-robrix-palpo-octos-work-together.md) -- understand how the components work together +- [Architecture Guide](03-how-robrix-palpo-octos-work-together.md) -- understand how the components work together diff --git a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md similarity index 98% rename from docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md rename to docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md index dedc150ea..9fa3ffa3d 100644 --- a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together-zh.md +++ b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md @@ -1,10 +1,10 @@ # 架构原理:Robrix + Palpo + Octos 如何协同工作 -[English Version](02-how-robrix-palpo-octos-work-together.md) +[English Version](03-how-robrix-palpo-octos-work-together.md) > **目标:** 阅读本指南后,你将理解 Matrix Application Service(应用服务)机制如何运作,Octos 如何作为 App Service 注册到 Palpo 以接收和回复消息,以及消息从 Robrix 经过 Palpo 到达 AI 机器人再返回的完整生命周期。 -本文档解释 Robrix + Palpo + Octos 系统背后的**工作机制**。如需部署请参阅 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md)。如需使用指南请参阅 [03-using-robrix-with-palpo-and-octos-zh.md](03-using-robrix-with-palpo-and-octos-zh.md)。 +本文档解释 Robrix + Palpo + Octos 系统背后的**工作机制**。如需部署请参阅 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md)。如需使用指南请参阅 [02-using-robrix-with-palpo-and-octos-zh.md](02-using-robrix-with-palpo-and-octos-zh.md)。 --- @@ -302,7 +302,7 @@ Robrix 内置了通过 BotFather 系统创建和管理子机器人的 UI。在 R 2. 创建新的子机器人,自定义用户名、显示名称和系统提示词 3. 查看和管理现有机器人 -详细的操作步骤请参阅使用指南中的[机器人管理](03-using-robrix-with-palpo-and-octos-zh.md)部分。 +详细的操作步骤请参阅使用指南中的[机器人管理](02-using-robrix-with-palpo-and-octos-zh.md)部分。 --- @@ -314,7 +314,7 @@ Robrix 内置了通过 BotFather 系统创建和管理子机器人的 UI。在 R - **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix 客户端源代码和功能跟踪。 - **Matrix 规范 (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- 完整的 Client-Server API 规范,包括 Sliding Sync。 - **部署指南:** [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) -- 如何部署和配置系统。 -- **使用指南:** [03-using-robrix-with-palpo-and-octos-zh.md](03-using-robrix-with-palpo-and-octos-zh.md) -- 如何使用 Robrix 与 AI 机器人交互的分步指南。 +- **使用指南:** [02-using-robrix-with-palpo-and-octos-zh.md](02-using-robrix-with-palpo-and-octos-zh.md) -- 如何使用 Robrix 与 AI 机器人交互的分步指南。 --- diff --git a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md similarity index 98% rename from docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md rename to docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md index 677cb216a..483760cd9 100644 --- a/docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md +++ b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md @@ -1,10 +1,10 @@ # Architecture: How Robrix + Palpo + Octos Work Together -[中文版](02-how-robrix-palpo-octos-work-together-zh.md) +[中文版](03-how-robrix-palpo-octos-work-together-zh.md) > **Goal:** After reading this guide, you will understand how the Matrix Application Service mechanism works, how Octos registers as an App Service on Palpo to receive and respond to messages, and how the complete message lifecycle flows from Robrix through Palpo to the AI bot and back. -This document explains the **mechanisms** behind the Robrix + Palpo + Octos system. If you want to deploy it, see [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md). If you want to use it, see [03-using-robrix-with-palpo-and-octos.md](03-using-robrix-with-palpo-and-octos.md). +This document explains the **mechanisms** behind the Robrix + Palpo + Octos system. If you want to deploy it, see [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md). If you want to use it, see [02-using-robrix-with-palpo-and-octos.md](02-using-robrix-with-palpo-and-octos.md). --- @@ -302,7 +302,7 @@ Robrix has a built-in UI for creating and managing child bots through the BotFat 2. Create new child bots with a custom username, display name, and system prompt 3. View and manage existing bots -For step-by-step instructions, see the [Bot Management](03-using-robrix-with-palpo-and-octos.md) section in the usage guide. +For step-by-step instructions, see the [Bot Management](02-using-robrix-with-palpo-and-octos.md) section in the usage guide. --- @@ -314,7 +314,7 @@ For step-by-step instructions, see the [Bot Management](03-using-robrix-with-pal - **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client source and feature tracker. - **Matrix Spec (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- The full Client-Server API specification, including Sliding Sync. - **Deployment Guide:** [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) -- How to deploy and configure the system. -- **Usage Guide:** [03-using-robrix-with-palpo-and-octos.md](03-using-robrix-with-palpo-and-octos.md) -- How to use Robrix with AI bots, step by step. +- **Usage Guide:** [02-using-robrix-with-palpo-and-octos.md](02-using-robrix-with-palpo-and-octos.md) -- How to use Robrix with AI bots, step by step. --- From c79e94134f719e5a176ce3d8e6ffb4504df3ef33 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 12:34:22 +0800 Subject: [PATCH 127/283] docs: add Palpo-to-matrix.org migration notes and OpenClaw disclaimer - Add clear 3-step migration guide from local Palpo to matrix.org - Strengthen OpenClaw disclaimer with links to official docs and issues - Clarify Robrix is fully decoupled from OpenClaw via Matrix protocol --- .../01-deploying-openclaw-with-matrix-zh.md | 18 ++++++++++++++++++ .../01-deploying-openclaw-with-matrix.md | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 3f4ff6aa9..93a1150ba 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -250,6 +250,16 @@ openclaw config | TLS | 无(`http`) | 有(`https`) | | 注册方式 | Robrix 连接 Palpo 注册 | Element Web 或 Robrix 注册 | +> **从本地 Palpo 切换到 matrix.org:** 本指南以 Palpo 为例,但同样的配置可以直接用于 matrix.org 或任何标准 Matrix 服务器。只需修改 `openclaw.json` 中的 3 处: +> +> 1. `homeserver`:`http://127.0.0.1:8128` → `https://matrix.org` +> 2. `userId`:`@用户名:127.0.0.1:8128` → `@用户名:matrix.org` +> 3. 删除整个 `"network": { "dangerouslyAllowPrivateNetwork": true }` 块(公网服务器不需要) +> +> 其他配置(LLM、加密、autoJoin 等)**完全不变**。改完后 `openclaw gateway restart` 即可。 +> +> 如果你使用其他自建 Matrix 服务器(如 Synapse、Dendrite),同样只需要修改这 3 处,将域名和协议替换为你的服务器地址即可。 + --- ## 6. 启动并验证 @@ -308,6 +318,14 @@ matrix: device is verified by its owner and ready for encrypted rooms ← 加 | Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | | 其他 OpenClaw 问题 | — | 查阅 [OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | +> **重要说明:** 本指南仅覆盖我们实测验证过的配置流程(OpenClaw v2026.4.7)。OpenClaw 本身仍在快速迭代中,其 CLI、插件系统、Gateway 行为可能在后续版本中发生变化。如果你遇到本指南中未列出的 OpenClaw 问题(如 CLI 报错、插件加载失败、Gateway 行为异常等),这些属于 OpenClaw 自身的问题,请参考以下资源: +> +> - [OpenClaw 官方文档](https://docs.openclaw.ai/) — 最新配置参考 +> - [OpenClaw Matrix 频道插件文档](https://docs.openclaw.ai/channels/matrix) — Matrix 插件专项 +> - [OpenClaw GitHub Issues](https://github.com/openclaw/openclaw/issues) — 已知问题和社区讨论 +> +> Robrix 作为标准 Matrix 客户端,与 OpenClaw 之间通过 Matrix 协议通信,两者完全解耦。Robrix 侧无需任何特殊配置。 + --- ## 8. 生产环境配置 diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index 26fe01939..e4f47f24f 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -250,6 +250,16 @@ Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below | TLS | None (`http`) | Yes (`https`) | | Registration | Register in Robrix connected to Palpo | Register via Element Web or Robrix | +> **Switching from local Palpo to matrix.org:** This guide uses Palpo as the example, but the same configuration works on matrix.org or any standard Matrix server. You only need to change 3 things in `openclaw.json`: +> +> 1. `homeserver`: `http://127.0.0.1:8128` → `https://matrix.org` +> 2. `userId`: `@username:127.0.0.1:8128` → `@username:matrix.org` +> 3. Remove the entire `"network": { "dangerouslyAllowPrivateNetwork": true }` block (not needed for public servers) +> +> Everything else (LLM, encryption, autoJoin, etc.) **stays exactly the same**. After editing, run `openclaw gateway restart`. +> +> If you use another self-hosted Matrix server (e.g., Synapse, Dendrite), the same 3 changes apply -- just replace with your server's domain and protocol. + --- ## 6. Starting and Verifying @@ -308,6 +318,14 @@ matrix: device is verified by its owner and ready for encrypted rooms ← Encry | Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | | Other OpenClaw issues | — | Consult [OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | +> **Important note:** This guide only covers the configuration workflow we have tested and verified (OpenClaw v2026.4.7). OpenClaw is still under rapid development -- its CLI, plugin system, and gateway behavior may change in future versions. If you encounter OpenClaw issues not listed above (CLI errors, plugin loading failures, gateway behavior anomalies, etc.), these are OpenClaw-side issues. Please refer to: +> +> - [OpenClaw Official Documentation](https://docs.openclaw.ai/) -- latest configuration reference +> - [OpenClaw Matrix Channel Plugin Docs](https://docs.openclaw.ai/channels/matrix) -- Matrix plugin specifics +> - [OpenClaw GitHub Issues](https://github.com/openclaw/openclaw/issues) -- known issues and community discussions +> +> Robrix, as a standard Matrix client, communicates with OpenClaw through the Matrix protocol. The two are fully decoupled -- no special configuration is needed on the Robrix side. + --- ## 8. Production Configuration From 8e99f48a53221b69d1fd4a417f480e835c8d7ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 8 Apr 2026 12:34:39 +0800 Subject: [PATCH 128/283] fix(settings): show contribute url as visible link label Use LinkLabel for the contribute repo URL and bind its url field so click actions are emitted reliably. Add pre-release updater fallback from /releases/latest/download/latest.json to the current tag's latest.json when latest returns 404. --- src/settings/settings_screen.rs | 20 ++++++----- src/updater.rs | 64 ++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 746ee4543..8437e586d 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -256,19 +256,16 @@ script_mod! { text: "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2" } - contribute_repo_link := Html { - width: Fit - height: Fit - flow: Flow.Right{wrap: true} + contribute_repo_link := LinkLabel { + width: Fit, height: Fit, + flow: Flow.Right{wrap: true}, margin: Inset{left: 5, right: 8, top: 0, bottom: 4} - padding: 0 - font_size: 10.5 - font_color: #x2A6FDB draw_text +: { - color: #x2A6FDB text_style: REGULAR_TEXT { font_size: 10.5 } + color: #x0000EE, + color_hover: (COLOR_LINK_HOVER), } - body: "https://github.com/Project-Robius-China/robrix2" + text: "https://github.com/Project-Robius-China/robrix2" } about_title := TitleLabel { @@ -581,6 +578,11 @@ impl SettingsScreen { self.view .label(cx, ids!(contribute_description)) .set_text(cx, tr_key(self.app_language, "settings.contribute.description")); + let contribute_repo_link = self.view.link_label(cx, ids!(contribute_repo_link)); + contribute_repo_link.set_text(cx, CONTRIBUTE_REPO_URL); + if let Some(mut contribute_repo_link) = contribute_repo_link.borrow_mut() { + contribute_repo_link.url = CONTRIBUTE_REPO_URL.to_string(); + } self.view .label(cx, ids!(about_title)) .set_text(cx, tr_key(self.app_language, "settings.about.title")); diff --git a/src/updater.rs b/src/updater.rs index 10427c92d..f633cce13 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -16,8 +16,29 @@ pub enum UpdateCheckOutcome { const DEFAULT_UPDATER_ENDPOINT: &str = "https://github.com/Project-Robius-China/robrix2/releases/latest/download/latest.json"; #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] -fn check_latest_version_without_signature(endpoint: &str) -> Result, String> { +fn parse_latest_version_payload(payload_text: &str) -> Result, String> { use serde_json::Value; + let payload: Value = serde_json::from_str(payload_text) + .map_err(|error| format!("Failed to parse updater metadata JSON: {error}"))?; + let latest_version = payload + .get("version") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); + Ok(latest_version) +} + +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +fn endpoint_with_current_tag(endpoint: &str, current_version: &str) -> Option { + endpoint + .strip_suffix("/releases/latest/download/latest.json") + .map(|base| format!("{base}/releases/download/v{current_version}/latest.json")) +} + +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +fn check_latest_version_without_signature(endpoint: &str, current_version: &str) -> Result, String> { + use matrix_sdk::reqwest::StatusCode; use tokio::runtime::Runtime; let runtime = Runtime::new().map_err(|error| format!("Failed to create async runtime: {error}"))?; @@ -25,22 +46,31 @@ fn check_latest_version_without_signature(endpoint: &str) -> Result UpdateCheckOutcome { Err(error) => UpdateCheckOutcome::Error(error.to_string()), } } else { - match check_latest_version_without_signature(&endpoint) { + match check_latest_version_without_signature(&endpoint, ¤t_version) { Ok(Some(latest_version)) => { let latest_semver = match Version::parse(&latest_version) { Ok(version) => version, From 75d1e64a1edb13b041ef16e850de440d9caa7bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 8 Apr 2026 12:45:22 +0800 Subject: [PATCH 129/283] revert(readme): restore upstream repository links Restore README issue/pull/release URLs to the upstream project-robius/robrix addresses. --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e1963f3f9..cb1c8d6ca 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,8 @@ These are generally sorted in order of priority. If you're interested in helping - [x] Cache fetched media on a per-room basis - [x] Fetch and display user profile avatars - [x] Backwards pagination to view a room's older history -- [x] Dynamic backwards pagination based on scroll position/movement: https://github.com/Project-Robius-China/robrix2/issues/109 -- [x] Loading animation while waiting for pagination request: https://github.com/Project-Robius-China/robrix2/issues/109 +- [x] Dynamic backwards pagination based on scroll position/movement: https://github.com/project-robius/robrix/issues/109 +- [x] Loading animation while waiting for pagination request: https://github.com/project-robius/robrix/issues/109 - [x] Stable vertical position of events during timeline update - [x] Display simple plaintext messages - [x] Display image messages (PNG, JPEG) @@ -161,33 +161,33 @@ These are generally sorted in order of priority. If you're interested in helping - [x] Display reactions (annotations) - [x] Handle opening links on click - [x] Linkify plaintext hyperlinks -- [x] Show reply previews above messages: https://github.com/Project-Robius-China/robrix2/issues/82 +- [x] Show reply previews above messages: https://github.com/project-robius/robrix/issues/82 - [x] Send standalone messages -- [x] Interactive reaction button, send reactions: https://github.com/Project-Robius-China/robrix2/issues/115 -- [x] Show reply button, send reply: https://github.com/Project-Robius-China/robrix2/issues/83 +- [x] Interactive reaction button, send reactions: https://github.com/project-robius/robrix/issues/115 +- [x] Show reply button, send reply: https://github.com/project-robius/robrix/issues/83 - [x] Edit existing messages -- [x] E2EE device verification, decrypt message content: https://github.com/Project-Robius-China/robrix2/issues/116 -- [ ] Re-spawn timeline as focused on an old event after a full timeline clear: https://github.com/Project-Robius-China/robrix2/issues/103 +- [x] E2EE device verification, decrypt message content: https://github.com/project-robius/robrix/issues/116 +- [ ] Re-spawn timeline as focused on an old event after a full timeline clear: https://github.com/project-robius/robrix/issues/103 ### Auxiliary features, login, registration, settings -- [x] Persistence of app session to disk: https://github.com/Project-Robius-China/robrix2/issues/112 -- [x] Username/password login screen: https://github.com/Project-Robius-China/robrix2/issues/113 -- [x] SSO, other 3rd-party auth providers login screen: https://github.com/Project-Robius-China/robrix2/issues/114 -- [x] Client logout, with server-side logout and app state reset: https://github.com/Project-Robius-China/robrix2/pull/432 +- [x] Persistence of app session to disk: https://github.com/project-robius/robrix/issues/112 +- [x] Username/password login screen: https://github.com/project-robius/robrix/issues/113 +- [x] SSO, other 3rd-party auth providers login screen: https://github.com/project-robius/robrix/issues/114 +- [x] Client logout, with server-side logout and app state reset: https://github.com/project-robius/robrix/pull/432 - [x] Side panel showing detailed user profile info (click on their Avatar) - [x] Ignore and unignore users (see known issues) -- [x] Display read receipts besides messages: https://github.com/Project-Robius-China/robrix2/pull/162 -- [x] Mention users within a room (or the whole `@room`): https://github.com/Project-Robius-China/robrix2/issues/452 -- [x] Dedicated view of direct messages (DMs): https://github.com/Project-Robius-China/robrix2/issues/139 -- [x] Keyword filters for the list of all rooms: https://github.com/Project-Robius-China/robrix2/issues/123 -- [ ] Collapsible/expandable view of contiguous "small" events: https://github.com/Project-Robius-China/robrix2/issues/118 -- [ ] Display multimedia (audio/video/gif) message events: https://github.com/Project-Robius-China/robrix2/issues/120 +- [x] Display read receipts besides messages: https://github.com/project-robius/robrix/pull/162 +- [x] Mention users within a room (or the whole `@room`): https://github.com/project-robius/robrix/issues/452 +- [x] Dedicated view of direct messages (DMs): https://github.com/project-robius/robrix/issues/139 +- [x] Keyword filters for the list of all rooms: https://github.com/project-robius/robrix/issues/123 +- [ ] Collapsible/expandable view of contiguous "small" events: https://github.com/project-robius/robrix/issues/118 +- [ ] Display multimedia (audio/video/gif) message events: https://github.com/project-robius/robrix/issues/120 - [x] User settings screen -- [x] Dedicated view of spaces: https://github.com/Project-Robius-China/robrix2/pull/636 -- [x] Link previews beneath messages: https://github.com/Project-Robius-China/robrix2/issues/81, https://github.com/Project-Robius-China/robrix2/pull/585 -- [ ] Search messages within a room: https://github.com/Project-Robius-China/robrix2/issues/122 +- [x] Dedicated view of spaces: https://github.com/project-robius/robrix/pull/636 +- [x] Link previews beneath messages: https://github.com/project-robius/robrix/issues/81, https://github.com/project-robius/robrix/pull/585 +- [ ] Search messages within a room: https://github.com/project-robius/robrix/issues/122 - [ ] Room browser, search for public rooms - [x] Accept/reject room invites - [x] Join room by accepting invite @@ -197,13 +197,13 @@ These are generally sorted in order of priority. If you're interested in helping - [ ] Room settings/info screen - [ ] Room members pane - [ ] Administrative abilities: ban, kick, etc -- [x] Offline mode with persistent event cache: https://github.com/Project-Robius-China/robrix2/pull/445 +- [x] Offline mode with persistent event cache: https://github.com/project-robius/robrix/pull/445 ## Packaging Robrix for Distribution on Desktop Platforms > [!TIP] -> We already have [pre-built releases of Robrix](https://github.com/Project-Robius-China/robrix2/releases) available for download. +> We already have [pre-built releases of Robrix](https://github.com/project-robius/robrix/releases) available for download. 1. Install `cargo-packager`: From 5bb99e382687fce395ecf28d8f8da7cf2c5aa991 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 13:32:37 +0800 Subject: [PATCH 130/283] docs: add search screenshot, People tab hint, gateway.mode explanation - Add openclaw-search-bot.png screenshot to usage guide - Clarify users must search under People tab (bot is a regular user) - Explain gateway.mode=local means gateway binds locally, not LLM location --- docs/images/openclaw-search-bot.png | Bin 0 -> 16809 bytes .../01-deploying-openclaw-with-matrix-zh.md | 4 ++-- .../01-deploying-openclaw-with-matrix.md | 4 ++-- .../02-using-robrix-with-openclaw-zh.md | 4 ++-- .../02-using-robrix-with-openclaw.md | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 docs/images/openclaw-search-bot.png diff --git a/docs/images/openclaw-search-bot.png b/docs/images/openclaw-search-bot.png new file mode 100644 index 0000000000000000000000000000000000000000..97cf34d4251b78b3f3be73cc5aefec6abcf16f19 GIT binary patch literal 16809 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 literal 0 HcmV?d00001 diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 93a1150ba..8d43131a5 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -207,7 +207,7 @@ openclaw config | 字段 | 值 | 重点说明 | |------|-----|---------| -| `mode` | `"local"` | **必填。** 没有这个字段 gateway 会拒绝启动,报 "missing gateway.mode" 错误。 | +| `mode` | `"local"` | **必填。** 没有这个字段 gateway 会拒绝启动,报 "missing gateway.mode" 错误。`"local"` 指的是 OpenClaw gateway 本身运行在本地(只监听 127.0.0.1),与 LLM 是否远程无关——DeepSeek API 调用仍然走公网。 | #### `models.providers` 配置 @@ -289,7 +289,7 @@ matrix: device is verified by its owner and ready for encrypted rooms ← 加 ### 6.3 在 Robrix 中测试 1. **启动 Robrix**,用你的**个人账号**登录 -2. **搜索 Bot**:点搜索图标,输入 Bot 的 Matrix ID(如 `@chalice:127.0.0.1:8128`),切到 **People** 标签 +2. **搜索 Bot**:点搜索图标,输入 Bot 的 Matrix ID(如 `@chalice:127.0.0.1:8128`),切到 **People** 标签(Bot 是普通用户,必须在 People 中搜索) 3. **发起私聊**:选择 Bot,进入对话 4. **发送消息**,等待回复 diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index e4f47f24f..defab4fdc 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -207,7 +207,7 @@ Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below | Field | Value | Key Notes | |-------|-------|-----------| -| `mode` | `"local"` | **Required.** Without this field, gateway refuses to start with "missing gateway.mode" error. | +| `mode` | `"local"` | **Required.** Without this field, gateway refuses to start with "missing gateway.mode" error. `"local"` means the OpenClaw gateway itself runs locally (listens on 127.0.0.1 only) -- this is unrelated to whether the LLM is remote. DeepSeek API calls still go over the internet. | #### `models.providers` Configuration @@ -289,7 +289,7 @@ matrix: device is verified by its owner and ready for encrypted rooms ← Encry ### 6.3 Test in Robrix 1. **Launch Robrix** and log in with your **personal account** -2. **Search for the bot**: Click the search icon, type the bot's Matrix ID (e.g., `@chalice:127.0.0.1:8128`), switch to the **People** tab +2. **Search for the bot**: Click the search icon, type the bot's Matrix ID (e.g., `@chalice:127.0.0.1:8128`), switch to the **People** tab (the bot is a regular user, so you must search under People) 3. **Start a DM**: Select the bot to enter a conversation 4. **Send a message** and wait for a reply diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md index a1395377e..4df23956e 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md @@ -35,9 +35,9 @@ 1. 打开 Robrix,用你的**个人账号**登录 2. 点击顶部的**搜索图标** 3. 输入 Bot 的完整 Matrix ID,例如 `@chalice:127.0.0.1:8128` -4. 切换到 **People** 标签页 +4. 切换到 **People** 标签页(Bot 是普通用户身份,必须在 People 中搜索) - +在 Robrix 中搜索 OpenClaw Bot ### 2.2 发送第一条消息 diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md index 65a358701..8e1b67052 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md @@ -35,9 +35,9 @@ Confirm the following: 1. Open Robrix and log in with your **personal account** 2. Click the **search icon** at the top 3. Type the bot's full Matrix ID, e.g., `@chalice:127.0.0.1:8128` -4. Switch to the **People** tab +4. Switch to the **People** tab (the bot is a regular user, so you must search under People) - +Searching for OpenClaw bot in Robrix ### 2.2 Send the First Message From 21dd6dc72decb226bb081d2be509bd6b2504e928 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 13:35:38 +0800 Subject: [PATCH 131/283] docs: add bot reply and room join screenshots - openclaw-bot-reply.png: conversation with OpenClaw bot (used in deploy + usage guides) - openclaw-bot-join-room.png: bot accepting room invitation - Replace all remaining screenshot placeholders --- docs/images/openclaw-bot-join-room.png | Bin 0 -> 55370 bytes docs/images/openclaw-bot-reply.png | Bin 0 -> 157579 bytes .../01-deploying-openclaw-with-matrix-zh.md | 2 +- .../01-deploying-openclaw-with-matrix.md | 2 +- .../02-using-robrix-with-openclaw-zh.md | 8 +++++--- .../02-using-robrix-with-openclaw.md | 8 +++++--- 6 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 docs/images/openclaw-bot-join-room.png create mode 100644 docs/images/openclaw-bot-reply.png diff --git a/docs/images/openclaw-bot-join-room.png b/docs/images/openclaw-bot-join-room.png new file mode 100644 index 0000000000000000000000000000000000000000..4444422a84f2f1fa60f2e7199c55182b2559fa71 GIT binary patch literal 55370 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 SCMy~6=k6UH^@7{xf&T{|m0{}u literal 0 HcmV?d00001 diff --git a/docs/images/openclaw-bot-reply.png b/docs/images/openclaw-bot-reply.png new file mode 100644 index 0000000000000000000000000000000000000000..479ac5eeacc7be7cfd6501cdca579363b64150d8 GIT binary patch literal 157579 zcmZsDcU)6Vw>3?%AgHK_G!-=h0t!m6q9W2o2)&3D>4X{}AflquRC-5HDWORTozPq8 z5PF9Y2t5!Wfxvg-z3=*QFkW%6|*6ebbd3m{Wp3cRLL3Ld;eYu`N1q^teGBZn6|E}mg1>21n- z-^n&ZZ{XU=8~!)CC29C{B8A+RvmpD`d~Ki6v?x68L5hc-CGPVZXRGcvXp5FNkrs`g zFIu3pPPu-#@rCZ2-1(<8>KvT38I`KYLU^6vfeRx?2-zvHt~w zsW=@Ntvv4@`Q6CCJ@Gl|&%K}%cZ~Fgn^(Y5+@ZS9qp~?)T*_H~MxRe>8X{SLJeN1E zJSv*FiH&XyVu_}v?JZuV8fm{yvGfa_#M7x?@eW??)XSzxvD$zUN>_mk_EA*nj?;DT zEhZ|V7E?82UIiVz4|SyebdKrdP~ywzXQ(7~Fki|3#hPYMri{GmTtn%UY~2Sbu9){j zz8N2Qo&!zA$GzcC;p45)#knEx2Pa|#oP@ z7;gAB`cmY^Y;!bvKW&6{i8;)U->iZ+&vCIW5>v2e6*V~_nRZ97IyEgcr9AlZn0Dxh z+YUa-${swi(N1llP4crXmiJYnQ8FF9QFDQj>;4ahOfHi4@E-1=;RnWZ8Jrn~UW9H# z;?mj9_FMn!Y<1J2d^5BHir31n zh1yg-hOoz};eJ_%|Cr_f4$ObL0Qk->jc!2dn5)*nAyc7Mq;MYcmx{?1A6x#+xc?o3yc1iAJLq3=EMqPn#CPNaI=f%JEwzpCSYQasQtW2BQ9|GoSRJ zdzP5}vdJ!5QxQdt)o&M3sKGoW!fUs^Dqi8;9pNMP=j)x^Dx>vWs&_%@i5eS-oA9;O zbjD$|krsS`@xf6-w z9@{s(nt=I1Sy2DI4;Z(Lbfk9b%Gs}hD8a0B-x2p=u7H&c>^Mv@_F!TE;I?sF#6MjN zxMo4jUuL6IC&$>x*K3f&VxsqEJh>XV`q7yRt7D+$r_)+iPC|V-a@)|z$*oVc3m!YU zUQ^XV8OgZ^kJCuvpHXYcdX_RIYxcIC|6|wx7+IsEoTAQNGdV6<)t=|;kteE7F8W;R zgKk6QS)ex}(Zq@4ck62Pl8K@Zg%^?O zD@DrCRR`~g5~;;6kj=3&Ol@P3h*zVt9EM83@OR~yPR?!jUg`vK7fWITNJbMe>AQ#j z&IHR~;F9rChSzW$Nqc(on6SFU3eY0)1GW{ z{DoiD8Rx*sY?}ugy?d4ykmI22EHy)EZ=F~nOS@PhFO^}Jaj@LA+|slxs9nS z7Td;n{q_RW+P8SP2{~&ScoJoU3F8>6ImDN)*PXTnLG+4?MUKwo6j3@a4_YJT)}W}o z+rsdbh(t*0t2KH4064nA^GptV?axRBf>ZIa7YhwA@3}-xGUrsU4W>Y=wr!^Sy8`OV zHHEg6kjfGb&R*{`oKmG)sTMMScMk|v66G|d86(eHPtKLP@_JrvFNoxWOL3ukc2+(x z@JWV^yN@aTrnA!`a}@`qB@`5!2%hhV2OF=yWr>KcTDGvo zw=n%}*$w|B!0{#Zng%*KscxNQK8uo-C(*K#R>)pLslxqIl6|d$dxGu7g z^_3wGry6PkLbG$K6_ji^LCEz$r|L>q?o4>26lF<05@En?>7C}lmWv_e`N2Sq9hHBF z$Dy{9Tb(X8{hwun{iQWA--es7 zQ$yu)A{@Ogx2yZ&!y%-_^g*>T(dvzmf&z$$VCmFb((Xx6f`;!_8doIO;chdgRoxdt zq;y6IM%wZk#o|8mU^mRU_Hz@5KKhl^gqd3@U&_U#1Tbc;(MypQ(ol1) z;izC%;Wh;(=@+@xB%D`J27e;AVraIBA}91!+017?9Dj0etNBdBCrk}3)1CScXZY|j z5^gptS36^c(>)(r{})XC|B+Td;o48VRdWdmA8cKpN_~%s8)nLep30~^)F5~JWa>0c z9L%LhftQONSE#lT?YaJm(TOUnlLp%3@Y|(%UG;`h;2k!vVH%w7=Cykp`O%NjVPlHc z^V7O>zkjQy#{lHG?bqDi3Z z*>jI$q`lo&F6fIFk;6AZOv%S~4y5rd+-{s@B$j^s5lbkun|1-j`hn?+p*SBJryZz6 zBTaC5T>Q4_TyVY}CaYlf<5$AqF;p~~$?u z0d5`c@9kkIJ2@wHz+SzT9Q0s3Y5}wIX0}igu~=lQ5p>3DH6$ zJvP@nt2d>r>liUEiqc5qli{_=1JugmJDsDbJS!D=LVbwG!5pwxkD6}{731A2Ma_1` zJp`?iVeTvaE2XocgsjAzguT@Y$Epz)C~{HNkynRiHZyr|xlp}IVqxE*VnwH#*x@)Z zDhpDLd*Og2ZvYJaDD?JXJ;TvQh5)-Xy;I)r*|r87)JzrHvA>ab;=?J>I0nr-b8%-9 zh4g-?d$6nZfJ`L?$`q}o)pyVJwex*eOcV1bclrm2$$!9XEu&w^O<)&(WevqMGNam> z6N^GRiVhY7c<<5Y6EL4rjH_J8oZ)sBk$Yy6YP_o^?f+=u&xfJ7ldfabbW3^_%R1sO z6bANPYny21ri#7o?R;|{{TdXVRiwbIln+ZQOZ4x$70Pn5k|7|%eMU& zh$xrR#V!Z8X(T&U2V zb?fIa4ImD~Cudln9}1ZIr2tR;$}Z`uE51<^u(We{@7>wVR; zlIItzD>_&sWUXszAa%f=L|Zax=JNTQ#nI(1BJMBaL4|G!&@l#!`;Q?zFm$|wh<;HH zCnxB{YWSzff0+-e=Pnmz+U#Y)wgm&SxEv2V`9&}7Ou1=y4*fcXfHAIIc`;FRD358> z+8KG#XeoYldip*#4HthiRt45!de16q=Sdt;9gG$ax)fDwWCUY0A8w1ZNdKCaWuHtB zmeoj>-JQVJ6u$WzBm*u}ou@kPpXM=MwJc&&0om>ljgXZQgEM4ozjxQ>3rJ*4lCnWz z``ky$Du3$}~O?)GDgSoDodeYuT zl5tMAk(A35;!0H?Dp(QAVjwckC6@1{S8HIRYa~6^7*_7xa$3<5f-#gy*SGNL6sQ1? zss`o=LX*^wG;JqLClXJ4`5jqjYQuZY!1yloS($HV0A)Xoi06va;?yrFy`ziB_Jvqy z9*QQ)+rrozo+H?#e%P5{ejo0hM60W*+)>Z+oiLl5s{IQ4mo4C1%`BZr1?^@2ds&dV z(4p7SB*FsT0#l2-cCT-~i_e8cFcZL|7So?Gggym&@aQ8&#w&&hHi;t0{)gz+(GVWV z;LDmnx{glNr!?ct3!;psT94pfqam!%e}%FRN+l#8%`%iySm-1UcRP$vge(2q)wL5p zXoo!Pgt325if?kr>t7g6kv5?h{x{qIKf~SaLDx&YEQyG9`A=y6@9ilj=bqB%C*N;Q z3zN}bt=#M^C?Ih({7Yo5z6vyNqA~ta3Q5Ruj0{(oDQi!b_2YhS_3gin{U_C|A)Wj| zTk88H{zg+Jbh9n>aZp>9He5srs`vD714cQ4bd7Q9?XlBs9io$lO~I^M+rw7L4<3n& zluY`1m#tOrwY7ZZMjh;}nNZS~2iSP7?6>1yOF0(HL%%K7s&qDJqH|KFg5pJ2ioVc23PS2BCf^ zg%5mYGr7xt-&$kXxtU{Dqh!qm+sR-0iBFYoMLy}fLf}AP%Lp2MF&#zTnKU`haJf5& zp;x(2pR4&@i!A|%c8lk;s(W|R7Qf?f*bDcdlVaC$KY|1$lv9?(yP%z!U9eRb$!~nk zD|M#@?=Z>in#KxSsjbX4K03&P=Vm3Ak9$m3Nv+q=Vu1eg0l*x4Me!Ql)c!tc5ShE_ zx$r#@Cp9y65p1X_=~uB_pj@_;ThX?)*taB=8vJpPYr>DzmmF2tB~!r;+v+I}2xzSU z@P2vq^kq2Kk38rXi3KtOOaV%O&tnqivGRgEiXVVxj{Ch_MHom@Js(gO>UW;-`3C9?l0Ccqlnrl+>JlIS&VU$=%c`lx40VFzvXVD)A zjXaE2mUn#!XmB)-)~^ep>b6tMl~czAZ?hlAihtDk8dr-;3Cwh2^xGSq98mkHeOvy$ z6#y=CtF}k#Vs8XQ3T&(bu<86p9MOImOX2cRg2>ADBrEGvX=)z7ZPJQGK%}D(EbkL{7t-nu5+yn7(705Vk@gF^f ztvc@@_r}+3uwVLG7xQmwulr&m!2^6YIw`VT8=StCA-UOpb)o-P@CDz0#{{8Eh?$=3i=UIwv2NuVmfns-GMSM7=cBB zk4TsLX>1e}iKh4oa6vnyiPfqf?jc5QG}(V_1hzQMu0CpYxVm)zjTP0%i9MDOQ| zK#%IpMB6rpS0*m9+e7B4L-OQdj&og(HLoqMKNBO0B&x31`K||WWp6^8gp)L7=jLaE z1an76MY;x7l3lNt_=)gf|BAWYdNh9JJlV9wCbH*}zLG(~E2R|%=L<8Oa-omL=n>UY z*+Y6aXYT5i&D_5qP@oucrMT&n>ssvYs`v$ul5ux9$NAA~U9yM!BR#*XdfL8MCayio zrzssY_C_<_whCrg^WCXf;*S*_chwp;50gH1DWU3wv#hZX_H2_)47z%yWU|^=GiY;l ziMs+VrDkS3mEi0tUV#|MLZU^liM`iay-cqK)w!y03BYjrtwlnk42WEy$;lkBx_EGc zWaux`#}{X<&6~&i7AEA>Zgnp8GH(d~gDC_)0Da_eqZ$OoMau4+fEZ?*KDL%gxDo2M z)7i5Q$%~7u28fW=lRGsJHVbV^Up9UoF`J`$pT^_IWPK9U(55A(I38K`z1d#tCU;<% z3PNwB$>zZqtwyCW0LKr$Vk^iN*MOa%2a`N&o+W_tz=hS$omKBwH@!FGCk?Z+AXn{s z?2luL2V(2Df!J5=OBJ@sF2Yw_v082`{F*$RwF>0xOF22XG*}oAAxMTTquE*!AoZ2H zR=cFIa+&WbQb%@T#S^Zsr>&n}U?zHcjGgK~#$u>3CTaON8ydRVjT^h)rQG!?$Ue1t z6v2}SwmmD#e%j!PxJN>$*uWKJUbVO9sn{;*oiQ&$Fn1$s;T3DQP~oSDB)ZXiV(+0+G!ufKkbW>)4N!naYEkgOiK1ai3R!ev%nUL+sclDE$Ihvrhn zEqha)w(5E{0W;MpliF?EmSYS~rpf2E#I#;g;xkRS$iy@4M; zZL$2R?DV52eomhdp{1`MXwzMpadm1Ci)dp2b$h!I;KZ_66GxfwSScVp?X;!Kk{2cT z3^a2h%1k=zHLp9p5jVM(Jo_86C@f;Unpv2I_2;)rf?7fsl}+>~T|jEWx9P2R#3B6- zrPCk08dm~k_sw7?$s|OS$9gUOAfG3vh)dl`P(R#N=bZBaz+3(vz`c%eYrogruYL#9 zpJgt#y2?}ZoKH|K193PniqtdSDId(yBh1AlX-oG+Fm|8bk(}v%brv({{WqGZ0(JK! z`%gtUjg19Z#e=MBq;1$m%(fPKikYXynp?yi#YtmRvCm{vv>417}b8HR8% zng{E6Fz;xiNJ>6fOpepbCXOXCoHJ9vF~2^_^PJ(HvRd?-e{82)9sd9ksQ`aZzNrhf zlW?1ZWTQ7`tc#mmveCrwYLHs2=R--XU}#C32FPCx=v9Du>cuE+7bS2=QRD@jh`O_f!Q_}}ptC~Tbo3~@Nu5%KWW zky3}3`c;ma3E_73fA)-0x-g&v3@Q0&`Rv-!ETReoAo(>`OU9%5DgRr*5Q9g;KffHk zHo*)e3OfwKfk)x~C3(OQ{?yp#Ek~}&tpTaPkoSk!qs+f)(AB%Qe;8JIx%spotpv%P zA3Ak1+nI`<;j#wU5uK&o$ak~}po$_*w+pppudytKJjhq#mOYy2BN%C*Oj83rp?Z{> z?LS>r<8`vgh}zIGOZBXoY4J@rQ&tVm$=DG@gErbe2o6VTLl0jX$UyHNogX*4^Vn+y z)JzC7-O>eiuVay}uc4lc-|#?2VKI%zSXj9Ebk|T@%%#*?0Mv%Xe)o3^SHczHR||2j zGyhTuyx|WzKeWu4qS)nnhFw=j2L;{XH`H*5kxU=Mua7~nm5ObZZ6^&2UOv_{9?vcn z;#ASPxp3ufK0{_N1Ka(K;7s-!X~i&p`ru>@%wlN@D^S#-RqDJN zfB|_-YUaY6-1RCUk6B;jET4fm)Qof*S6F_nHx-M zMiW2hLABDoH5J9j8KW)ZLRjO&I;z3^jO~uC@O9%$W;|CChA~=T6XOG2`v=7x_m9qc zQ-d4%N|o2tiIsK(RT$+#BO~&U`cy9z_SZZ6Naq2dh_hvzCjjoKmQActJt>QN_E*43 znLNATLf=h>`T4B-q~_H8kKH4(>Fm!O(SP`o2hF7IUe6OX6%0i~bivI44mqS^Nzxd~)5O=p~dm z&o3IW2j}vfH01NkIkO;tUzjt|q_djPR8ZulZ=FnS%`f&ZE))^=m)Z0?&d9Fedyl+| z4;(JU(O28ed*y0Nf!24>(w~q?&g!hXu+G01I}lMWo!I)cGuu427AuR~-(QvFI{dwT z@DU;>UP3xY+Jo>}``IN#@cg~nWv+IE7^iU0x=nQ8eoQ({MvGYqwFj45+zx?i&Rf<# z-VIWEF!`&&5n8k+N;zeg@oF93#! z*r`jy;Xx&?jZAhkA%~w%Bz@n}hJ&Lm(6k0oIr@50V8LXLCHtwAPUE5s!+W+mtTD1@ zCk+x}W&XC@SItxdgqdCMFhOJ+>Phcf$OR&_1x$*wf>|YsmSuhSgkCpf;F9W|b7mwU z;^C8cZ_n0m#F(cSP@JGW3uD+a6G$JO#C z4nHre1R@3QM-^nHQ{MrR-kXc^`a}}Ou_(bWaW+yaYfqFP*2kf zSun(`cl^wi7qVBaexlpkTE}C~LbG)>m5ju5jf{*4TB(j} z9i`y1GjLp+E8ssW9Qf6d0t-F9{`!egDy)~ z8O!`w@!Owv*SgpSx>V0(UjT}Nj0sfl&fpJrkVr!}w07gpnKQoCGYKx8JY4%z^r-e& z;Uy`DGE5YtjVC=~vrE=54L_#AD&+e&3^lk!$))+=k?D(QT2rsR7pk^S+`Q=v4YZ5` z_4H>1tj0Z!h;>}Kx1+}-(+qW_i!i7`W24T}Rh$hb4=!bkHIjUoFqT-Fmk+9Q3iShW zXnc%a_DrE$VtVe9zfRf4Od-e#z|NB zoAp6&Yac3W%p%%IXEM!WcRDbdJ5=nY>K$yffRVl<{Y6J0)xVh__BEkm>h0+^w?QND z2L{&Xj551Z_+=~Ey_1YauUcZ+9@z9>N&h-q;xt_cnSRf5X;h&Z;3Sha6R;7qjAAL( ziW)wAUs#M>HK9e2F%CZptMnxJ^6w`9;L4+wSpH;6r$%MSQUUZO?*b4Tp2s6j)&~x@ zav7KZfczN2zS7QTmf!Y$`F4N&LUC=QQ?;7pN)aCL-S6P+Rq)(03}|0)&1vfZb#COMW901Jvy;38>|O=H4fBKP0TZ%|Cf~u(PDyJYb);^fDTI zWSt0|aCJJC|01-*Ja3{x<0q+hr#WeP_*11U5cz$3cnRuQip3C_dwj&jgF) zqY$NzA~9IbilUYdd(VwFXtpfr3F3iP_^Ly{~U} zZmZYYB-0}ra3-+)jV@=nL&C#k>5Rsx_B9`@BtaUMfY@6@BHb6tQE6X|$URZdp^Sb$ zlKiZ5+@+!#dI>e^0L@Iehv#%3GJBtPGy}^C`?H!`r*i$#aIcwHvpnH!0d{VfhZE9k zUnLef$Hll4kUePk=3|H#I`_P1ou0}5_*M}GGpBP1Z#u<6AH;l1_fuipJPaT}tixy2 z1Etm*Pt8z-sgQn6#Yl>tgivoZvD+j%z-S=FdY@e$ia6aiz2&@3x3LPSI=p^51Lc8Y zmxWK?DKlqKYU{BfV~Xn$)^n)*gzGXV5O(r&Ytdb|T?t-%7K3m6F$i4XFcz#nFXdU+ zP%t>OVez{7fhMc5%-UU_SK@?^R?(th$9iaJ7qyt^@L3)=k5 zmz&Y0G{~UH1#{&OvQJLhCf0KuDSPkj;zYy8F%zEe<8(+ZTdXVs-=#HGF+{5-loh9xNvYau_ql z<(m`g*Jzg}X~(R_cJZAGOoHKxVL4o5y`x-C*cWP7?q!iSh_zKwMjSaw&{7n=%)0+b zEdEvJVD)w}0T=JRWY>D3S)@z1@wCWl?Iu2)i6WKRSiih;(s$J*nXj44&u4|ot2B74 znZv_yn(hFF+*d9m&c`o#?`BJU;9mHYl%U20o!*7gWNOR9KO=r6dEqiG0Kf22g27Uy zx;Q36|CbMyO3{_j}3u)SCEFY7QYMV42xOl90LSXYvqi-1=DSOd3T!GT~x?lE-wPxcc)ExnquF7<;j*P^F38@oJxa6%h$>rCW^-q#HP?9xhE}9;6 zS7$DJyRpj6xpL8|xyoneBdRATCuRn!G$@elH1y+j5*Ts+TpUJx;7+{<@niTAjwL6E zihf-^NuD0pWHwq+ZsNF~KTVM*;rUZL*IrWlFA^vg0JyLLTu-FLEd^7b8)0})$I zL~gcNkT&0Et>)mRA;-%iOYDp$n=xH0p$3t*$2)b!+?^{QsM*Tobh26HKTw^+R2i{$ zY!b!)Mz%SDa?PvM?;4mg)iYKdC7t$8qOqOX*P+>;K(1bQ+h=Snf;XE_y4tNMCfOOL zeIZWa4(goI%#Xzbj>G_fE#af!wX0>KYaf*=`qk0PMUoc9U2x`5F1=RKf)P$=!;i#4 zyFOQ`qd3&m_69|9^0@f3<+1fQ8>54x_xFVFk3MA@@eM3+B)rh%^Z&O83j2pjzF8G# zGtSAXath1j*fXlCwx(J&=Eu%8V^rw#1@BrV{SCIaxG7lyx}$BxOcE$$Z@%aV*;o2h zZTTqIYPCpNyi}k1hT{=B@LSD*8lb3^gWD=(O;Y<1CibF~e6@0u#dYU~i%Ha5vVby! zl+-x3Useo<4ha6`W@?>gg@+G@s@gh?2hBS*??NN)HwRsut7=32T1GqS+!!2`gC5CyZglfd zZapuAFsyb)(xRNTehsJ%{hDCBf~oSsO`xOtX>t6SVPye{buq-FbOlM)Uq1ffBgUb8 zTy+IAFUV=E+YGbkF{XbhPghg&CCsSWK7*T{Ngyv10XrC;lU@p}ey1=rXkm5m_n6)A zKX_ysF7}4EeG!+%d3+P=y<*@F=d8xBj5-JangTdSfj?~)+olA21h#p6=ZY|*(hN!4 zrsW>h+d;Rt>a8QS z*!&#kp*coVs*>}!m!;het5i3CYF&zeTghr1+y%tMWm7%)_1>GdOB;S1(GT&ULo zWrjXAnQvpTxPXt~07X(HPwrRl6)0&=6um+72|E$`yH~Cz6eTK#@>ZyE z7#WxD%?Xu24p!rKo zT&&%Evu0EGU^8fbxeqYOJZ$M%&PQa8Q6dn$NT11*2JUFjz+$?uiJN zSmS)i=WT?xb-L(iT1*0SWOFDO$VKPrvkqg9R4RapQgoIg32DLX5Os9CrObmeX4z%b z&l)lFS~>oVtlrugxZ3(Jf?>kB1rJ73N&hA91KxDOqX7w^M5X+j3Pzf&zVZCcWdZ(6 zIRt22C*h)=8XHNtH){!6W3qV|U7VxiS2)P#CsCRi04cGkGT~b5=()8N&eOFcNaOk= z+^ctSUj&ph0r~1{TdXeqyNdk%pvu0!BhxR(!jUm)qzoQsV4veP)>f)Dxq-7Cx3!PA zgvO`tyJw4#@8l<5FqpmzkJ(FyuQgelTz;X17l^*1t>`P0DZ^`!Y@~Odm>@UtJ5XP1?Q5yH}(K}wuP_0iFwKs&~U&o`x);jF% z#uau^i@VUW=?`o+dcNc(lfgR_p~aJsE7~TBBtT+eD6>=66`2+)krm7-EkUqn7GVTf z1(s?JO40p~%)`yw?(@uW#7-qv`j40-vSWBoVmRB}BRs`2L6sO=b$5ffggjV!c~%zi zl_+1bs@~3l{=j_yx$U^|N)isfbN~U4qD6g0t8<2MNFw(CfHzzg94FJy#`+TtqRBcW#ZA5`7wCPj~t^Xo8^ z4Mw6INmvTMCpz!(U3QrXyG~y7^DCoh($6Wf|88A!>~tbxqN9eY%LtK+|W1FH5g8-pPhm*+0I z6k)s%P~5*Wxj!$8ir~>Y6}TyQh!?aSrwB@yX8#l~DT(|CMvn4dVcU-mLPCTA1$ysB zV9i}#-u9Mt&-mJn6A~VmQ^hW=CAM(mN7&aGYA8x^N*Xjm+)UIo*JY&(Q1RfqhMSn{ zB~=y6nAtJ-MjN^70b(YY%>mGdLxXK#R(RFjeck&5RP^5Gy`^49<)DZ0n0vvZ$|QN! z1BC>%IOyZn08qYz+{XwmncorwL^K{%q-Obnlllr3QUa7j>#Xo{FbvTaX!g=)at?W%kW5Lkj2;Z2e%YrwFy<;F9KP9YnK2Zt z^(Ac=JwR}-Q3#BEfWs364iX&dV(pDn4-$@l*Am*k&sDzg)=j0xAQ71~uXTEIev;I? zv@?H)G%z}`jQ|CoSyG;qS}T;1a+z=E%&`{fL!7Hp44iWY1ZmL}Iz&rN{3k^|uAXm{ z!>YvXI;fst3o@uMpK)i=pDpoq{N*Y_9Yn6q7tj8|{++Sl8tti|vzpIi;W$CAgglgr=XGWUAO2kq2W`oL^Zmu3}MSqWiITG z4Lx>z3B=i0g0Bx4(#)lcLZXx0Oi+|^c}oRr9zpkqEg0e2=`AjJe=n-siE5JA=`q0a zsiPjXP!2aNwLY9|?}pKuGWr4ALr zL~;=uKAoKm=pc<~?0+%hlo8!L4P8GQ!6jd5Y1nMgr)apAvjKDDm#yy09IRS(!)WJu zq2tP^nkS|1;PC>!KWY`Px>U%CILCkI33utWp~rYMk2pIw#!5jUE0s=kiy>2$1$V>! zw2~~L*A$n2ySHNo8LIgXw?`fG1Vc%j&eyBF#0KmN=Xbscc1orj&z$l31teDAcb>V8) zYQARdiEH8b)Z|v~hr(T#*WMP3;e*!9=;Mh&%nWmD?x*2D*I;YjOQ+Q@S!WM90CKAb z${1)X_vJ?i`15hZ;m+2L?SgUr5zGAR?NPFT*euJV?$2Q$u7hbV-0A9>$K<%z5i4Tj zHkgZq#_M)Qwy=1p+YV8kzuPF7lM{4SUVx&;`vPA^8E+TpJDq`!yA7g8uemui1^)JP zC??#G`1}ppS|4?RS9l!Q~DnH zB-bzWgCy7016q<(GeCOpJa>o`j)6i|qD}*6$RL2cvfs^X`m~Ae1VD5~IKq|;2$8_q zUsXmP9*f|s>S_fmf_Z-ArKMK$1lfg@fHs2(d=x3`(tuXF8o$XQX)uVdjiF(udZ((K z`(QYktj8fZQhRZp!det7DEW@kVP@N6T-cwDo>u8B4#(IQtRN2$igjfViCs(ZkGBL7 z{9l^~z-l&y6Polsw@cyR+;0m~Goy^@g+q$KZ~Urrfy0L@;LV}}-1Rq|k}n%R>RUDZ z4q0UF@@&!%PGw&knM7n2c1pkpKWg#(eUrKpussCQ*i$tNs($&swc$^ z=elr0R+NKpZhL*&@X=nI!#^CfrrX8y2BTMs3QyBgh}=Xj@pUInDg|(o$=(RZ!`8w7 z{4apP0cIocOH9`#-S?V1fJ_fDAkHFU$Bj;3m^ayPH0igO5B#B}DDjBVJ|)L)@mrI- zsNJFrf+4>ey}Wa4bU8G#lu}v3bU6CDjC$i_T6WFIz<~cT!oZGNQm-zeaKO zh*LVw8~KmVnpoIk0buPE(gEkzIa~;ZAGC>Fp57WLsV*L{w>R61l;|m@6aaEpvJW0b zFPbid_zW2v=-ILMHFds*hxzT#ViwZEayLz*^D8%+*pgG~b!-m+O0q0p8V8CA&I*!` z30?4R3Z}@CilxLj{q{F(Ng_rrjS)F@Z5}Z7M3uSzKZOeWfH}uE2ZMkPS%1gwH(J)0 z05?G+nse#M>hXRv%*nU^$VkZ~zr(ddH2I>sg&wP`|7luq(D38Y5rR^AhfHA*DNM%k z?R3RyNQ~&Hr02IBqwXZT0=YpLOR&XOhX=+Nz@3>(E2(e;sv+pgjAY_PEJn}|3hj%Z z*rH=0uXQ(XVa z5+r%Ww&}V1yKJ4sIi4g)2y2rhsq^?LhrN_d9S!s7_Xn?h2BKUCJ)6(n z2q#(7OQ^`)Y01txn3X+9tA;%`uw~l5J))wA^VWIr;ak0kh=h_%N#z0;DA@W zw`~)EWN=jP@q1R9+u_OkbLDw`CjKzUVZ!)X%8rh^KVLbbm6AN-FOv^-eXlyGn7|EQ zcni(;iVJ*wdRzXmXR@7V@q4woim+q7oR@Ql#ay#UUx=g6EE_?0HAdvcEn}RVB71ZD zn2s+gzS0TIC>?JYaxq8VZ)VLJ%^F!S{hfCkO7dgagv2#Cr<=6Io?Rd`% zJ-DP!2PeDZG}LOI%qeMbC7PeznLi6<2dGYGJrh13>>*6m) zUzcUR=*+$&WY)WToS&I7M1k=G>nmpo)|NXBlE*LfU6tQB7XI|JRe^)X1D=`PM1U

@LM|n&(t@$3|Q3YN%kmt)wdTY3CX8S`pMeQ)bUi@xP2E z(rY!#aD0M{C+gvxu1#pKfH0wo({0z%J_UbDQfljo+-Df$3;I-bkQnq*EY^!W$PO8g`JWp@|vGV;; zdzaZg7Tyn-Y5wL4$Gy=Y<=3l%z9beYo^fbxFc;~ZFm}Au0$Vg!Ax^#k40z( zshcg}QrUHDQE2Q9Yj8k<`=&q@UWI+=8trs4P}s@qk?e#NI^G_XOXr-sS+R+q7W;kL z?v7;Ys;h=UY)p^Y#;_&1;m5OKW7p`FK}A@4;VTidyF;;+j}j9=^of>i3$zA}+q-Hq z1=aw@A>L7!;4u+VPqxJ7i?Tjv@)@#bW-1R1KWOFrEgZvxYEYFut8$)N{BYEvSXY!JBkf_nQ9o-3!1_%iVdhlJ1=id#$GS%uYeG$)K+I5P7Kb+eHt=g#j`(1dei+sH_Tw?Jhr)(AQ6|a$g=g-xc33+M_yb^L8RT=(wX1aNdEY$ zEhYkZ@&xWXe{hAJ!ou;`A+Bw0-zGZyhFdK`GWpZ5gs0Qf*&kN^9!ySfF+=Yw!-i#8 zeu|y%Abs#@x{47~d@?y~R$KYjZkftE^|0{fOhFnc9m-f1SwBz!C0#p?lgk^UnR-dZ zaRbD0otsrG+U^75RIJndyJv#360c-lvfoYbf0A%!Oca}K{&YC^ zkZWcFjOQ}yuoZI|iT7O$0z_;|gMb)YeXzV$Cf|ZYO(cll8NRd0JUM(}CGCLSj8vEO zRXZ`xOa2z&CkmHkzoj73(nw|R&o^H8)UW>%K1H%LPLW4TpXDT-+x>}!u91kuRA!R8 z;g_`2Tdj?ZR zU7OPoF!(45J5`1bIaqLXWtZJ_*knT(9KJr&yO8HP{$3bXlkk*nI@M>Bv)%BlmWjx- zieiMK?L^KbeKF_I(At%Ve5%X$FGKI=H2KR< z@*5pjha=OUL~@122(d*NN{h%nzx2%hZ9@i6eM8;74_dRg15{w=`fD4ywUKwvzjANC z@u~in_0#ZEpFww3A|~d(EUOYP8{)NcEk718T)K7YZRhKhZ+VO}xbO0PXgRRf?dZ@~ zGf3gUyn1; zcAkMV_r}*|E85&oFCzk3Vp-XQ+*pdviQEeIJf&l%TDir;2O=SAKWefxzpA54G#o)a zdgE`tUSyjP?y(T&(|EB#TOFLz_>+tM!HTLeL4z}`U9agmS@jC5QaCjj&Aut*ANl02 zS&CwCG4**_=7&OTo~_RW#~edvOTG9a?Cy;KAKpikD)=iaUi;+}$MsTHIYiaSQGzyx;rH zoSAceG8y*Ho~*sr-p_ij>mDhs^hwOZafPFre`SJdwu)NTFsCk+@~IheW|D9_6V*j7 zzW@xwH3V;}LI67l8P_kVs8nP}r#?5^g-v5)q1xvEy!~I_deLJhRg2uVUWnKQfxp7trZq+G89;XcZGB2f zJ~QsK8cv;JTuWk@K9g1{UvMN}drdG|i~Y-f&NMT{V*TAZ;o^kHwaU2H8>E%TY5@(l~A5JfBOb3O`NZ*neG=G{h2?GZL@ z1N~3w8V2`GztZOtQ|$H%M@dIdOnt%p$2&F&M=v84`Tf5F@ov?LZZ;>$D2mSjZHNc$ zcPX^U5%Grmfo^K#332yBZ}f8uA`Ymx^|>Nayqi?jIy66nxZ^Ks;yx z)c|D!jhn8oGw>XBQ!2WEHsyTpJ(_@yILb{)gm*&|43`wDjswS>NR-XtEqYDCLVWv& zX-?e4Wvgid*`CF5*J+Jrz|wn9+r+YtU$ood+a%Ty1NGe?8l}N*ONaan0d*tZo&zeL zwDXqjl0d}UKd1Ac%V>@tO#Y_Dbt%o{{&ajE6s)%-XXL~NN$N&(R2LSVI}PwZuIJ!Q z7E>vvG+(fBUB|hj^)7kiz)NIL#7s0?Zv|u?{#vg*kGoyxC1nQ$w5+hJKNabRneKPa9v4QeC65z5^u5erRyhki9Pk zX;Z-`aV_ha zk~w&Eq5ziNJSTV&v+eG_^E4?sP52tV#5^is;UO6|zF`JA=75DM19<#(I2;{w`l48u04#jkx zTvj|A6Tonk|B z;3rJD|Ar=4gQcNN>H>>H1wiAmEwj*G26YjxN(FL}KiM+m@&Sh{gEU5oT;4MNeHWz4 zrDGNB>ZJ!iFeL#EXg2m1WAz(WrllQd7R0|%eEtnJb*HY7;`T&-`cbnP7BYxK?mVMw zIsKsXpsIDjaWQK&ICYR{W?9=?!Ish?xY(-8wC#&86;Yo|6E`Nhnz8j8SvOf3cQKiG zx4!`WTuOY79|?qTV!TDiO(YdqaE9!Fgg(G)GX{i-W9u5hoJ&5=xq8x7muhJ41+xpO zz=!}Bg-qAel)qNkFm4u2s^&Sg7G^7H`{7G+L&=mE!LeQ9l$_E zYfx2d()ztO{{D$L9|+Vz0MV9RM-84Ef~ zCNw02WvqSJO{ri3% zAN#$%IIjT5_|-y%Q`Soqw74&!7e}()C^@sq5oc&*;k~d?!dEm>R+{p)Mif8+m8`N6j&cnUV~qbesL>K+V~cc*d{fu zI%7)HzeuQuwwlWo8hw$nmm`L9Jfz`p65Hg(R?I&Z)kJ4bC*Tc-Ef{}ig!6w))?(s| z2ea?SnL81@&f>117&gi-Hy4%*C{-{siFS87&Ck@DU+{MvNY?5&{|Gs!q>^o)LXaPAAp%KX-E?h}C)>3L@m#l<{{1j{_RrgS0o6a8z=XWQ zhnw9^c|qa02}$8Gx~_ZBlC9LO&!4>kkjC)4fDlFdBe>@67FnjjGO;$$Ou(CW9IKby zIr*>7qKgU5%;m-}Unrq-h`itG+?56J@Vfp-X{JT`(a@n{rp1ECDJ8hF>|gWuFZJ&$ z%TW?J&Q~o@+Z%_LZVjjohg{o8cdAO)?bslreKTuHm0=#NDdw*opzK>%{$8z-%+-hjewU)!l0(nXt#*g92t^;kHR|5|B>U`0h zb4i3%Z6VGISsdgVEh@unyje--+>=7Pw0Gg}aN2|ZQY&w0Nz-l}E%zaJP8vq4n;s7n zRHIw8m5m7yqOOri_re`0-v)IGZn~Pn9Phbx->r-}wtk#L_Ev^AU z-N#cR*wJ?><^Nr?ccd0Ilau(R5I=?V0Vu>zZ`d|w^2oQ4=h)W#?J5srl=I*GqSJ4sV>YEaxw$(dx_S*H5&?$s#bPLnEcUp?XIkfTMHZkmzNRq;^K@0-xT?io zp>YGNA#&$@F*b*Pd0IPjc$gR-)Itrk_5?QqtEVTX#XSKcj%BsK)-pRRuv~ef9#f@R zSFbxc+Z}#vx+26_F+^Os`IE?l7+?k=%Zh||*qQ`xg;!MGLY0#zUkzBCM$9JHM&{n3 z(L7m_Y&MVZD_o#lTZFb!* z+4vZ+uBc#BK+{HkZ`vQ#3N(3do17-z{AFgrBQ3lZ%#U2(c7KQcbU_XXt^Hk!&DlS%|hEh%6>0Lp zpnAfVu&I@XOQg6rjI-4xl#lLSml_+v-iwAMvM^CjBNu~z!xzXcqPA7nZrc-gaEBldjm46K9IQ?!G0hSEf&p9%a zu^My(10MeO&8&VauipDsDg_$ z`n#Co@yEBpeu4}g_jQb<=g8Wcs1FJthl*Tm1FLG;VVdxtLCg#6Fno0h9w#ew$yQr! z{~Z5U%dzkFeIXy7YLG)z1?TJFME>6>h(CkYcdA@ai55j`43wXa#%YQu4^jcj&y@D_ZqUBk_$N*OD7)Zm^k_-m^u zk;q6}6=U{dWb7t2i}zPH0(4=fC?v*NT9(=!-JjC%W_>4LoWIpk&rJvlZek5Wr8oVs z##~pS#oEY{a_{drM5IJm6M@yS&m8@#Um&PjPd>7&Y>u_NWCCr2S0C-8pcS}WTtC@` zE6tCDDXWK&$=meb5MkP(5$=3?8Ei3W^#?X`Fqp*XKHZHp=q8UrP>B1Wj5;Mh8taY-d4QQZs!J+`%Z?V&#@mI{5mj!q;aOwy=^h`& z?J>`eOjF*k7$3Ec9(svXU~RtiEsv{NhJ9;^DyXcQ$ z=d!87r@k5a=y0*ju+ZdiU*+;|Bergj6yd}ma_U*OEHtzWs)h<3>+J7E2_1Aqxu(|c z8@lxlDX8R@?@W+`{z2!khU~WvQ@soU%A83g5U9OM-EB`^l}qnRmALcptF(G9%Yl9* zwv+*8_&lwo;Vl^wI;8CNB!7z;7FGj=RVnHSr9rt16jNLKVBY^yHO=xfvC?jiFc#(4 z^N(t0%U<@Gz2pQz9b`tZ2%|dXTGf6{qeQb!-Uonhm5bqr2JoTTcP@W3TNU8EW==mp z=kLTQE}^FPrhfbw5W!3?Z$at0yiM488R;;SHcosCNoBP3c*=$D7PFE5W|%CJ|K~;0 z&2#n5Uy(JgQYup!=22W+4=Z8>pv`jc%$t{@(jKdWxle|lVmtxM4qM+X|ER}AbSKyo zK;L`L8Q)s)Ph{%oZT@;R!2%D1n#cCDLz($D&_=B!ww*r->d#ZVH{4pdN8*g{&iEBN zUOL}24n*>{H~!eyvAu^*ic)@bJlvN8il5Fev3uMMYEu1OYuyYM4!snJ%MG+^$;C}8 z7?*U#@TtT$sC!pb%V(mUadStp`PENqg?V%-?SUJeJ0&p(2m#i&LZ?{+VcP~Yoz4kr zq}nmFb|L7OsgMbKk6BrwhUxnWvB?oZcMZ>nzHMmjh?n?N3#)X%@0WB6ZM38^B2}MW zkYr)#hlJ(Qr+wwML$ew9D}3@!>#@+A9ke`k+O#WcVQbC{eRuNK(p5lXRbCRm+zx(aF zH}tB+cj@Gqm3XuNL#aA5?YN)CD|f?Pl`>Ve>9F2@T1}w9TOzK31=U?Z&qym=N^L+X z!gxnz#Et5kbt$R0Q}TdD3)1H?;0F11U8I13$?PE33+J$j#2l<(m^n>-o|=yvKgobB z5}cI8fS-?qRoM{GjD~VCAxzQx{ZI6-K|F&uSnsregU}sxw$o|Z#a|gUgL%l!(D%3* z$N)nCyW7AXRR{<6W(j!DYC*mEeA;-#O4va|Wt&tpd>MwMU1Md0oD@ZmMg&ik28}C= z7;7+dZM)k&B!)zCcq3}8BukJHKwJ4R&+E@Lc&J)2klNJ=cTU$HHt*xD_(7^c=oOq#{&4ZW>L{NGZ8+KomyLdz2{0n^}sU}A9q=K_+j)=@ zt4ua_X2(|sHUX|RJHt&it$Vc8)=Bh%-nI3?l=OS-M#L-Uv;tjoTt+^lfN@6RZ#&50 zEA_}3vW@ek#EQuUk9KSsejm*H)v`^T!O4-Z|Q(jrlip3N6O;g>(xKzB~Rsg83CIT~Yh4mGCBAyK}O zxGfa^>Qa~Sn5UMy6LDL}(<7Edd0s`kS=*vsGJt(HlPXbe_#P z%H8_u0n*WMwNsA6;9~sOj zn}|<57&7DTOX1>#;c0hi&c_O``lvJmVi6ovI!}?R!H>8)H2kd+a^1Qh5&0|_TehE@ z#)u1)KE{vE$l8yoRqQ0G_@0u9FJyy|H{yOmsOU_Iz*~e`tjc+%*^1u&YluLKp>V7$ zxzI?iupDkxiWUf(@n@ljVJb9_G~Yh|VOsui!Y5@0Q+f$XR0Qf*DKlh&r!9})- zLFM+Oa^N2Ijhyh|kQN}VJ=n7_1zi|gD&F_GVH1%5_j+%Sq=NV}0T~-X1t5dL+Q^+Z z^38^&1dL5)@mH%$;W*B?zn(~L? zSl&L?#9OU!2Shud-QEx0g$rG8+t{{#!>I(m+$}vYC22a4ik2 z*)J`(FNx`iRUq%S^md!=q&I9scNL%;kaEFTQ0&tGV8O>6<>!V}{43%(R$e0RekZEI zZo%fJq_{ zEabe0rf`Pnl%XUyBC)E`(i6gRu|3O}XO;%z)u4EA>m1mAQdysnGY`%Bcirj3iOZ~dJCAvHyqZjSJlA2pVQWDY@HUq@F zK_kDQOS!~z!hDEVpF`O`!AzsC0Gm)#5)?w@#itA&omKp$F#LOC14J|MdxV$SxLSjai(C0)1dd+4t7KQd0P$8`!7WsCk)IUWIXEjc zjF$w2y9PnSV>iPZDS1*%gi72!wxX>|*jr>Gan#J=lu;XAn7jqTC3p{+_&g)zwmc>m3#Ad-YwmCbofbkWVa2cO!O0)TT|F_^zsa; znrvo*JLJ>7ERD0yY=}uHzmbO&-s@pgj>LXU*r6$Zz1iwVf@-cvU@k(ns~7kDB?hAC z5?enLtuS?P!(uR)bQ{pr4FP2Zq@ zpVQGf-%!=yd&Il)hj2gG8`vV5-X_F*caF$fI-6NLbH>Ag1C%51MJ{cm@zQ^Z!qIbt zXG^hQ1Z}{2n$TWuY1-f_8Hp69 z0*|C|gtSbqTNw!?9GtV?w)$a3PoP&q8*eJGYGg?BN@I1gYY4g+AV;AVz>glSK(@de zlzoM567L>Pk(DyX(xXW77P?Wb0mKFaDD%}sxSexZeVb56-jT1`$5Ekw%gSB4 zPq-s_f+J~#@9g8i!F3z%vz^pY7sXHAZE4zdi9D(S%REjMOt<7WsGys$k?)~xJcIng zuCa$H71LCYif{;UT$oKn>-A5*Jgg!-?Z@+P1~@?H5#P)>;JVJ_!1W3i+^SytDX0DJ zOGDwWOSBb7e@9a80@E%hJiP_-J7r%Lq>)9X5UwZp0IY$)>>z$s53xp$C)}uS0ub6mSMhg{N1;W>-Cn|5Y zTY=*X;hY;-)K??o0r%!)T<3c{isFZRP5T|3hj;1n3;Pji(RGKmdfe;sK(i61hHZ49 z+Xr~SAIRYNrRqh9vsFn!h37R_<5+zRlYP!B`?k|UZ7wd;T~fkLaT$KPm&OUy|TdP&m zFOFs(h{Z*NJ-nnx9Vi2KSNB+U>Ql!gIz9~NToY5;_>5!x-2p8+2|w2UCU_rgKm0Pu z001h8WqaWS`a$AOOFCt1Zht)fb#pYo@r^oYDA;{*7nEMR6FtatX-AuO`s;#XQ@1lm zuJYgC#}PG5tXdK2WwhlVahBh}qWCX@Fb=IRrtGbKid1}KXV`sK)`hhOtQtlaE;+ca z4r@=yd}n9uZ+@2V*Li|RA=zOwsTB4PdKH(;?{o+D!nSQXO08pdCx9S3|HA_t;g|bO zX}Z^hgW%V;q&idZ5vLUaiUibwl^-@A^IRx(c`#S-_gowbb9$VE$Vfj>Wkvp5}q< znB7C&WyD)ijJWOAFYBq}g@)^&b;lOsIa;1t8EvixzaFIA7cc9nYIhnB2lis8OSW`} zu71p<{*`u|Y5g&hx>UOm*M3&U!I5*>;V{{e0npr!TG-7{>%6(cRtskt;3oKoa642| zv^(7Gu?OUBGZ?6j7LF^Dh{kZV^Qh=if!)Py-C19&Jd&2?ct*8X?BR{|^BU~?B+#y% z*D$_p=oNO<`rP2>yVnHbSS$di$nqc^Ji6_j#A!wm-A}TX(JNn^l9a}y<RuN`E~JUiI+aFckS=&%M%gXip%h3<(VW+K49rj za*+gxrl_H8_v5HX#EYjtU7HFS36>yf(vV#rH8;kMAs4i(@*tz_>JGfIls$TjsCX@n zUv4=dEWLI$dEs`a;#V2sOlacc@ml@SzN{59!jlJD1X^ z5NHp(F2B6fd@THDqj_{L@{%$r_Pu;A{TSZPD>sz?{%LGLJI3?{kqu{%`u~2IOJC*j z>KtQj!DNxe^MC*2|2}|w*!!Fp6>P3M z>(JaC?<8eQqyiKCy12!t?rOh&mA!0!VFnt=RIR5EPkEyqwW2q6I;%Wequ%|ZVshy6 z%lNx_NcJR?*FcWNu7n)eR-Asj1pBEx%wT&GaY30aZ+1xUJj~QX_MQ>5_2<@GL`SV{ zsZ2%A)BSeubEm&Mg{0K!nri8;$)Zc96BGX@hZWh{8Q&cF>f^pA@J zo|Pgr#E%a)(9RVrc0%=wCwNLH7E`8EPt6mBM@*1J$zP+CyW)CVJ==>cyJtu);y>KM zo+jpC*yD3YGW?0Jr;%e3S+4vK=JL~yyad^Opw4af)64F_@S~?p%E>@lJ=>dXG1&c) zXJ>2S6NMj#`Cgsq^H0>(AeF2BqLC0jyk<+hDa++Eeu6Ok+ANowK6_oLpY=P0gN#86 zFFUck)IO^+uq7}z1?)&u>+x1Rid8dsia({94$_fo!?!1Iq6&{9)%`Y3el)iIJb0Op z(b(92Xum~;_N+*bAV!Fr8CeV*Oux;#EIMFspHnaOm{xl&VB8?&u|P2dDn*Ux;9j+> z4r%%1efayCStCUlD(}hEmC@6;T zDsd)uc@$&FA^64O2!t24!yJF2!{}?k%31ft8>T^Hq;(`*uP&$aae3mWXD_qvdE=WZ z{fGQ*8>7LqXB*NSf(aeY*Yh_uuJ?9FjBLaJHEySW9#VF+A@xBPc3o<`X(G*zvJ(P_ z*iFwG?*-PgEATs_Gj?|y!!sEsBlt=(uvtAz6w%h)``n;>gCW17UYg@^owt34z~vmS z5w7lQ$2v_T|Y;2bL;TMzQyW%%k4XI~)x<^Evh^>0)Enj+D#hpsc zT9$=+F3h?wSOSPW;8YjCvG^e)#9_?$&`n5ZR_aO-1f+<(N)P}%TjJ$CQX z(h^bYmr1Li_V>&FAPbYR8T(^U$I&x$LgaszUFC0 zUAAo+`dp#07qfdT3nMKsARD=sUrcxWJyTaS{ZX*y?nuIkh^@$kINdtY*Pgf+Oe;KI z5_MbaS5>jt`9BlNw2ZM|ovtcQv58&+Z)DfAC_O<)$%iku$;a}=cM_p|fb}e(U(~*A zBnOf5^0WM|T2hW2nODzh$`8wDWm>j9sScAX@+h?ojTK|+naziev=a)WlBvYKGlsTUXq@_Cc#Hvh7pF1b)WWFDl(-X20O{9+=4p z)W~SBQX(f6T5X09`L|(KbU*~o->0ltBNm;PMI9~Q*wR!+2ALI0%|D@+8-E3)(HCEJ z$m0^2T=B<|@uf~gset=ANrczPAExOE3{xG@vP2% zo6)R&o#11&nUil&6&bUC)d3+|n7Xn`zqg;O${Rb$_8**Cm=%@v6`c;f68gEw9t#!o343T(e4z>kvk2B5xbL^9t)cbY%O@;v?zB>Gnq% z9H8=Lf6~eH=y=8EZ_jRGivvOmDbcnI*TebBf0VbkR;Hkph>p^pf$J{Fh1j<6D9uRL zp`9gL$B3YKF)U_T_i}Z-CO4=N zw@plw$-b^vq9fyWA#u4?ba_{;_a~RX^=C}nYo8q7I+MZJDjcn!J<);CFF&feB)Gon zkB4FH>`QMX9k0i{GdwpfJ}+2U1zkGHg>>%vRyoCS-R_pUi;C^^1O0mZ&Aw(<>=ZU> zb8Yr(IAO#D9R6qRW0PD<`dWw2+J4wcYux4Jc04AHo3%RKvX1R2w1hB;vy~R1Z6h97 z9Geh^e*IWRVIZTk&jFYkluhczz!^c|0zHY3^DD$6nr)e>A2Jpa6uff0$KrgpPv2mV zAmYN>*Pnxz(vl3eMP^yvF=A=AE=()6J`Zo%if4ql7vU}YFYr46D+l30lraxPaF07_ujCL zqE!SjzX^)1W2lpGdJii9=<9yJtMDJMR}V53!fpQ$LB6~rXP#R@`0hgo!lloAt0+?@ zqAcQom`qY6r=1Hza73(5KyQ)vlPlbQ*3`No`21RxlQq9?Ov5S$(JHZh-BCU6pi26B zG!uKOt0oBcwh%;ksOu@(QmmlN=1zPx1%Y3$BcS=?=L7=plSN*6r+UWyUaHF~y3W3J zgu~2U_%-7=4dUEhkeahTfdEew>DIz$bC$A{OLde%nXwlF)NA^Mi7XHliQ?RA)5k19?0JSQaBRN*v22~w2tj5$oPD0s=%1^jbrv*0DMnwbkDw3!$8NMq~P~F4~6;j$FN>_R0$n3n>{N+()Un?9PW_}#TR)_#d|Hr1HPr(nMTg5aq9ym z&c};qg*yYhsu_`O09@MBYGj?>9nP6kFxs4r#WLE!Phq~K-&Z!eod^mn_?aWVffvUN zEY9gh4L5Urb~d(OPrD=5lt2PV;rsjMUdkixr8P7`W?&DsfGWV5xMeI%NrfJnT%@L~ z^UPc5tMC0fA_Jq#2_Y^d);$M3b_L!5bv1U(^HDVAeUqP|_db5-g~IUiI*2@bXer5n zz`cPluseJgbf1(HiSAWJKpj##NO<6Yc*;oulIyyJsaKsJAjUNK6bX~J{-7j>tzDI5 z(Cq$$r^802S$))yM0EN6@dk0Hmu_lz6vh2#-q!JQHQs(S z$j%;%E+T*xa5EwoOF&+%x;CDdQ7~4;f9kg|nXPrNr7x}dBs{D;!rO+>7I5)=;XLtl zyG8)h|A-pBUdS3zur|gip#? zj8HWF$OPCu;-}CkaWtj-_~VlpLjOP|pqfBTKszYm^No6@VAJqr=_3Szv9BX;d3nWH z#?Jujgg3}l-5?hZlO!?afx(Vm98tSGG71%Ah9^KSJ{O#Hs(fr?>&ght)J;WZH@qVB z3nf;Y)iFX}h$^4>QE_-10mtS?9Rq})(Ll_G_@`&VLK=-YLd(H{QN}R#`sSHRO>k^Y zt}4RS-C>h7aU^hrcVPhRZ0L431)1@A1~L5^Uc4sHU{(@RSsUA8>L#gtCUhC|5+df-ui>g*vo+nlQZGBtGqY3=)*UuXgL`oB_plQOg(Thymtfwg zzPlC`TQif&>3@|--YE*_SzpU>dxuo_>jf^)%Nf8+FQ$)ui@s6B_c^%n1ok!aF>eVf zoWj`NW+;gbTEahYy+$ZB6Y&Vp38;jWHg1Z6k(>r9QYr!r1!lY@w0v!ms+OIq!OOW$Bm#Q>Rh-jAVKjy@jIvjxI-Sz`uk?&RJMwELWpS9W4jeem z9GzcK0Utm zMmdjZ+1J(qo`!%yWkZeo1j<3xf7Yl}w;82j43D^?BxCd~-+P-BwJ(9DydMcOjEhHY zExQMO2?V`t7l~@VuHTk){KQRb>K^8C7wF5Np6H#6i2!06RnNIr$9DJ;18>70%ZyIr zuoHz$Yvk3cH7M3ZO(_os<+lz0^H$rXM?dEIV)lheNCph#5|0s6I2PL7W%3f-#q&Ou zIrW|yZ5AT-kH>thYR|4w&V~8oM+W5t@{l7P$w`|!B;|x+l0Vp=E{vlb@BS!N8%SUz zWwkvEW*B!9n+iKHQq45utCY!9s<67y%CYplZqzPI4KIHG>|hu(W@o7(!$xlva?>bd zbxdctT*=5a{~rERX7+V+WH7IwE=D-HFe4v66-K#?E>Ab_$A{<4RX{zg`M{h@v@?Vb zkNZR7Xl$Gmq79o7uB=mQYlFIq60`zKd7Hi#cxY?HMxa?k0tGv^5_;S+$nWK2fFeI} zzeDQL?qO$X=%qX5med^k^SjnpK92zx;KjYcrqn)6zb=ZnT@9R4&XrUcer2CYu&q5> zg#_c=Fwo&=9JEz6hK}UF$XVi6)O#yR3IMyskBuqgm+6G1%+(OK&q>@W2@sM{dBft( z^SiVTxO7c@6KMq?pc?ZURe8qN5fA!l&Fdx84sJTQ^`v0UdD_qYi?3%T6Kc91tS`;N(B%MSUu@PN_ zX@X6S%_-zawQ8Drj%KjV!-G`y@qlJFKoaMX5#y))2C{$utA5ca8ozFwGShV>{K_1~ zjhjT@hZkrF`*kApwRT${^P`+LoDGm{`l^fpdLNV3kDI9cn1`M1p?OW(i7$FXD?X0h z5N34|ck8ys^~}i+bH>bigzlhNCJG()$IWH?CHO{h*!^7HuoRTzaTXjmM)qF~jtnDg zVRpg`*(t~wr>+3R9N5xH%7;a}AGkZ3Y&A5t+n4l4oHPmzprxI|2?L5N;+lF#Sp0g- z`tzf7m7ioPgnzd=b})ft&C;`g_QMZzM`Cq2_w&BG(FGDrS|CCpK0ZtIv_Ew=nN zT(J{3N8PWaZ{O&Dsu{oO5HIYdueVwHPyzRY#ZzP0RfpUU{kd)r`>_oQsu%HfP-Xy37bVE28?1=1z`Q-SZzf;@_YYpK3|46k7D z3XXCI{8xh?6EsR|60kxTy&03U6`iJ)^CCnNoEZ-f{nltDd7I=dkvk;QaXgfGPbQgA z|KZPw6$o}H;vh{K$f8KYe$7LwSr^~7n8p+TB?y%g!z01s+@{qRhxqo!OFrcqKa(-| zez34F|LaR8)WAcNeDQ<2h*D3tlQs^(_k`(l!c!!)igRPZ4%>XNAc_I?X;dJq$0U2` zcg5%$=*BBY5cG=?;Gb=FsnIQ%)kd+G$K-!5n~o>4}P8or-37IOIYV4gQ3p( zhm)Tfk(iG|sp4Yvd~FvX#SRQUv2|AK2+4z>>|mg8TH(fKL5zV6fsZ0EF!u51$1_}V zD`_t6$M`v0yjQ@D*AJ>YDpM(Ozk&)`>#CLO_$(>`K#%cTcJU&1~ zeLzik1Y;ZI2YIQO)$G& zf=#(gMKJ)otccaE;{KPwm>Ma)Tk2IMzF=2IJ)&ybl!!t!K1N^4qbMmYv8lenxhxhE z_V_(O5C96C`AZ*gB-H+FaghGRN zXDi^~o1E&Tv4zv`oS5;F)oou8*XZd*yZ`DCUug%pTuYo%I`O+wP5BgAF7^zIS6BW( zck4;G`yGx^j!22Mxg_`?ZJPyryl=TDi-XZh?+Y*kyg>7YM8X5e{i^n1msZSeQh`lFmZ1iKZ<7WmYJ(ogb*Fp3B4WwKJG^*{XldPi>x32 zYNG!I!1W*%PzQ8*Ff3{;7WoT$;6k)UPd=@BzB~N05cXq?wF=r39Y&&90d@gI;e#Nr zAWA#a$31Zr9*#TE@S|};CdPvVJJAk5$|5yjYHE*G*?aj6BIMBm%fGX=Emophbyw9P zeH91$E5{PML%7S0hqkwf0YHBEVxs$KG#s0JU@qB`g9G>U1N%2&)ivg-Wp}@W(E0X? zhYtq!eL)9A37`EF8AMY65lVj%$*6yQ>*o4}{>g-`_N4tOu$<7C<&r7B2frM)h|~qR z0cJe(=Y@1;JkFyJJg8Lj4^i|oYsf8wKc(;p*J(pV5c?Anm+cOlzW%E-ab*{-Emk3c zO6570M==CMunq9@Yk0tBRs*l;iwWKyB_ zMjq{jVl0Ccoi%qng(8%}0BySety_(&25zPnH8DNJ)Qi0_b~zb4u@JiEkPm3i2WLzv zNKp`6WMq|9Dd8}LdUP=Wv|4vlHhp5HO29oRA>|+MEn;vI82i7qg8kg_xm9s$l)@Qp z@ck8$R1O4m6xFKXaoL-8z}UC9%cMEbap;ulM1o=!n;*p_R_vb0W8<({N?IpsDU@Dv z*qOBTX0l9?r4V97h78taqVH(?HY$BPf;s|hT`sswdZn&59DOK!_6qVGtc#P3+D8&4c zK)fGN?05ab?=4Rz?tFRdQ$&o%-YKoi6tF0#mf&I6hE}ruq(1|a`FALixyI|1c3coVB3>0Cwl`$!sRbCoWZ+oFM zJRW7j*bW}5ZBVHO7z4yme-jz_PB%{*vHZ`#uM{sG^vO7dv z(0<|50jW{hA&Ea`{GbHBgczmn8M?{==lF8%&cRA(w^gYZFyYLD=T5 zvo(gn`3^3yD;29$o!;(aSycz<2lzy~dnaOc2f2*oxTCJ*cr}vb{a13s#C{ zf@&VL=X`$a(6|f!PBuR4g349t+n92ZiUl7keS-_jK$h`7$$J--Sspn8x`*k)kz?!! zk8kWw4QGQX*U6FS;@7zWDxNiW6>)diT#I)GJa^x_A)X!93G0oX_;z*IXgMEk%9ay} z(JK5Bmd>YFUEk{bIgK#yb3cCIVf4oW1W-47*CmMCgSB2eKrZGX{o*au6gxh zH`3qg+P%RwxCtJ-lA03^*h@m8e)pp%0oNu%9<SgG`QvGF$VJy zUTk+5rNn-rajfEdp)AD+!FL}loz@S@xk`{j_C`}pq>V)JRp3?qs)EJ96&t4~E-O#R zdio2URj~ylA4SfE3rSOKR!++Ji}P_R-7UUUND^TH!Rs0Z=hHUBlw%Hl5gkHY>HQVVGYEJGKdPp8|Ik#G3*q0u)Wto;=is;+067XpU zGbJZOKWMP5hhTa$)SkG@3{eBW1epzdW5;43+ffqOmG)?*GUJ|O1*9IlAaGI8dW(@d z4UQ~D(j!kq@mj-W>kl10cDqN5rkw5u`8P}(N)@C+b#}GlsBSniRx{$!S3Kky;P@Pg zDm5TfL6+1E20wf!-l>lF@+7`Ed|vV6oV0ogf&nbmiFUntYA(?5@D_X?MT!8ozR^8_ z)uNO>R1OzKkKKHZc6J|yo*FQ%vl$bx%Tk5cfYJP(LflKSY_KoDs-#!@28;ZzJh5*W zCrt#H0LIAA=WIJCeRSX*0KiVNxRj=@i&tINO}1GsgD8bhBLiDJ{bE+Fw!L4koz~kXi2U4Vcn-r4suP)2>U&9YDyQk=dF_eU zARa8Px^wctwusoO^+fKX;%k!$o9$?E)$^Tyl!05re(LO9anvWmfnUQ$SZlm|98tgdx~7zKjA&aC zK_UuT&gl|gyALYjPCbv;pn?~l37kX{w4dIlKv}bO=kU0q>38oI;Eq>Ld`Ev&@4p&9VmM`$F+S6D3Y*CW%ua%ic>YFMa_RR5#F%_VtB9a_aVi;pVIw?umcu^ zMD5NhXT(rENO70iG~@q`>tA26v$8=a`WAs{wd`l1Q$82gg{Vas9WKaX{VjI&3d8?l z>#f6@eB=Iak&;%rCsHciNDK>UP}I>4(h|}zQW{4oAu-WRz->lnjYZm)&g}f2K?twf62u zDphNr_ypN;N&Fj!ktwwyn zyXXwwL}eSu>iGP5sr^cOPA5i~_I<_4OnxsI*C26SrMfg7zn!nMc4T^ndI3Gb6d7pZ zop7Z^UrF@>&Kjd*@s$Z&(_Pm;s*=R}F+p7KY z-%T!M=UIQUbc{EDkv$fwG@;ee-SUTD8u60lXkZz-^TeTKi=o}SNjyaC^2BR>~~r6ZW))eh1QkE6D{t)-kE}z3$rkhyVvagH^WkJhgM( z`4K9efF!vSzppePWKiE)#cZ*SJ50%8N$B3d$^PH9kO|d}zL8S)C(`z#180_ITSf=J zeE0YT?e?g7^%U&&8HPh(+lJ^OV0;$ zl^>kO(yMs7%-qtiuc6viFPv|~y)02WRWr#s%uJFYP55slPo_Q^)fy{1&*f?14Hv5N=N~J^`{WtBFt5amo=r;D zdq(@Zv#-9`Vw(P`*!UL?Up6{8QO5rC?Ugz1T@=mDAAm8uv3mY&D&X5BBEpQa+(bY&N?Dx

Hvqj1h9-dRJXMKf4v1671bv=;~`j9429fVTATy&4}8uXE7Cv=!#1 z|8o?PUH7?mM&d4$v@M;M@DSp}wJL!#`Tc25-Bb6s#(YZHCQUuX^}3LKHN_kADh;vs zo@G||I$DichJWO0=JX#bw`y~irX2IZU}~-VcRCr(dH^V7N&5Xm0MKHWJPvp%gNR>y zHoag_J;l*(2LHtK{7ev3C+=aK;a)~v_hSlQYfTkT2NuvY`WWN(R=IWxHtyI9bB?ww zn)~lZNsE4XNwc)?zV!n_5eC0jMxT^D^!fJ4`8yrUou}zB4^O)Jum1rG>;GqCSer!m z^lVzH^eFv*L#qIK>OWd#wuQ8X@V~vlGWiOwx)ZiJ`M=Ur$1va_;q4yt_Wu9-dH*-O zrFylo7EkisVC(pp7npM0Gz24YnQaQNz-yin>`vm4#;JroZi(hKVTvkixPD zs{28C0oc`Mr!F5pULdHL&Qdl^u4G-l6vpF1cK5jwf4Jrh)QCVm3sBF<&z<>6k54-! z3?8pfjMX3gE*#D%bd4>FM-OV4(G zB%K0eklKcUAlz>yys>1lj|@q$aJ!NWM*}VF_Ecl21##nqR`HucrcB_Ua;KY@1mvY~ zR8J6>>+(QF!?UhEUqH=?$)$rj-~h$E_+mOFwc+yG#g%Un159-T=JDj5vTh5e)_q*-l*HuYZx`bNm)Rp!u`l_?6lwJ!}b@T|SOG66zk5WT5 z=|dB`_H#hzXtg4RMb7RwXERZ3#C-rnX-XBcq6}AoF8>X_;p{sUezXVhXc?H zr!ckxO>H9J1_(55n|T6$_)YK=#a3Op_mAc3^fnib+mQI8i-Ri;!fzs<`!)XW6`-VQ zQzhd{bpi}g%MKlFs6#A7sMEfa4}pP6EUg&8agc#4Mfyzf0Jvx&MzG>#xp~RGpA=2K z4$y4>Y&!M5O+Mh)P61%dlmH<#{Ra`~VS%ImsKU+%xhVrMq2T()okKlwFgEr- z-fkIyWY&t^MAiC^W(xPNm514R$+xio*9}Qo8|n_AE+GI(GW>$xjX}J$_j+Ysd#K!w z;ro*Xoxnt0ALdC{s{Uj1QHg!Wt-tO6rpCGl1-`51%KK4|jZKVm%WK`R2 zHfvYgHBuV5#W^l2`OloIc2hFG-gjoAcIRQzgD|^$(!7bXhey~p5o{Rb&qNmg!Qod zFg?c(Jx1o@b_pfdRI`rAyUjH=>OJ>pMaGUa_k1QmBbZR4vJxO8#=5Dt=v)O(f^tV)tp*PPIE4O#Hgz-_JXYOkLTa!EMtU0Z`&XjX|9*Kd}K*=FP zoXf_zgWl?ssHJ>e9Wk$WuPZ+ByT0s~@$Qk5?70{}G*xf69N(uTTQ(R>+7lqQ&dw%< zUR-Bs>_mr?aOX0dMFY{K6f5?$1>{)4+e8?B&@Y`MI<`aA_;>g>&glcJnWcpX7FgUB z6skhhv~W~zurdz}IDRgBrnk&7GqjG3_fasWGW9}jX{=7V3j2PaG{60zdamR(Q>uXN z8qwnl_=5~e+$|RvXC~TtR%e(S<|EWC{Mi$Rde-LH&XHNvmPputem<@%8~{{7zBE6d zl_B&U#tbcQf%#?2Jbty{KiAtx2Chdub9vG5`vl{u5^u4lqv$6#BDIG=CI z!jSs@)xWQpzL}RE8|DL1pTG|N`e!uknPvslVPw+1$}Qd9 z3%NX72F`ok2b)&6C2p`&x8<>%z z2lGw)yc!uVQC0-#9wYP^mD!;(Z%1Am9!>?@wIXu!f-)NHZ9~2Q9hRcf8>{H?3cxPC z@ACF-`NR<&C*yG#+Xc`nXVP^on}oNechgu{{9cUGOdrg}@lM@VdCTx%7@X@yErL$V zC)d|(gg3t#?1Cu5taH=_pO!;idedgANL#1%x^n39ez#*E5%h7Z0M@s$te@We>OsJ- zlRck?mQPcgs#8mD*1yE&QX{kZJ6#@ed&+B;aY1)zRpFS4;c{2;zk-La(}$QHEx}Ql zCoZSJ{qx(Bj(t#m>It()8E&7b)oJL?lfX#gS!0SRK|UJDA`@7X8AK|^K!pwMsAe%c zBFERrB`p2;X4drJGzWd}9uKHvRBRwjPWv#7Q86>!UN2ehWzzasas5U6$9JTa~Ua7(bUeT8i1lq;8h(O!7IwbM@%S( zCv^MOx6BXAR(XT1iIlJh+#>Rh6&9QJGdNM$K|YSW<4r_UDzqdmvmIXs9$xQJgMwT= z3zxbV@MOlD&51fI`BiL2&=a_eKgg^{Mis>8=CZ2{VrszgD|Qx%ti4E7wk!k(yJKN6 zTSlscEddrAEQ>k86l(@`J-yS!L{R3HI`8Ede4C(w^dEkwc?pSb-8`)nGg>O?zz|7d zXzC{#Ha5AqOsyiH3EAJW-lsc9r65bwGw5b}#Qc^u$h<7SxqU;%G4)aVbF%5Ls2yTV z14&ha`9O6vh*n7<}nqU2wlyfE)T z;n*(e_>0FLa9RAAc%ra!)y|?%Cz3s6Y0dvZ3T5Z@QQrjd1!m6_H~#$+08C3ce;k;t z^fWQL&gR(|7x2-oubV1R1PBN#{JtU~R~pe!Ko&0oYea(2K8A+B*+~)uy!g>q%hJD@ zydVz2{+V5%CzpA_K^a)|3aiypQ#LeD{`ghJ8uqKds)tuVjhWhfgd}66>p}1Wh~x2_Q0i0u1aKa*eM z?~h`?M$q1mO|^-)jVs?CESzfWlc5Yz;`FxS*gu8dgr3a3LoQ!Ihogx@o4E6I%k?%c zg-7fbepk)2T;`|04AO4%-!(n@ZXnXak7J9mD&aasht>18Qy5oD#@Nc7WbXypXU2@i zx?t`+*yHY`B*(LEQ(N|&hNit>=|_&n zp8*m#HPlF9>bf|S*L3%u!__lE`o;Ieucc=37CZ}tCu-jBA}Kx-VnWzi8C%wETLG}x zFqqddG|6S-5>LLku6VVEj3!rFB06Yi^Es5et+L97ybIQlt=+J?a{(+G%cz$it2Dtu zq_S6i3LEkCKK)ieXkeD9Lq@&*3P!C@Yuv8t=kC6G46<59oPP$PUdtB8g1V@|w%$)u zkVzW)>g#GitrLG5u$}C2$Bx z2t&=3&%@hBQi>}n%&)qOe!)6?tk16iK=JXkvQ4djhrAx2-qOFS`s-xSV*KEjS~;&k z)#=sq-e{L%yyD?s3z%FMXP%BZ;c9y1V^&Z?ZDu|J)?H+xI42&)Z^vZq*JU_3g-G-Xvlu1RAf+F^6?UZqzKIOnX@_ph3@WWW{dLmtG3NN*alqx9*c z^)ktd&8lR2*EZFVu($@nWq^6bo^$dRwgeNq1$3FuHg|@7=w;ilCIvADN}4|yLss-u z05@Q80jQU}Zfe&bAc8T#(_Q0|)5_olTxjj^CGq+5j!^@It>;!u4VKPzKdyzKCDE<` z7ff%Q{{0rx=Xi0OZn>h8J{p&eE78O9LjSyY6`bx-eDe!|IHZ5b0yuL%?d^-pGE3oV zCCK&KztYw4bHv^Pds5>aPJo3-i2Pg6U(aASRUR%%s6q`|z4gF0b614$GR(Kt=2max z!89_sz6WJFfZWd8j}|grraU-L_Tvhu$Z zqK9p#%w7I1zDvC*E7vodP|dkKGg7rKUR}bogCuxlA&67Ve|Zyh2C7r=;rJS`O^T&eu;^19!M0X<34lw^@pMXnyx3u3obN+V7c@293;-WOG=no+TeGn~2d zAs%eu47b`*dnU*D-ulfW=E!U0(n%ozwSAW&bm`kWHVl6wXYcKDVu`{j-OWM&$TpWb_kE|Oa6iezY&$(MyLGQ;k5O`P&Ylj$+gGH4JP z3a$|#SU$a!$I_%`24P=!Ldo7#Yg|J-m6M)Gz0JCYy@lp?R%oH^Y4T>>L42*rNYEH) z)4OHpW$BsqrISNIz(c`E;AwdIL!M$BFOAFXm|Q`PM9UxysjqI&Z@phq zeS>G;CQZ;06uedma1C;XV;}*_2mb`Z^i7D_O-8!aeron9JSVsD#Bv<76pyTT2v8EV z+!?8@dP6oWCBO0AjQWH6q;kW>Aw#^gn}p=TZ!8CTo_jq41m!QeUUac!igV?vP`%@(9xSX;!PvU2A4Gpr34b3*?Lflyq)!tUf86O6LZ+T6rlo>XuE zsdyioaA%YrL>;$Xp{H(%*zk?QXRmebapSIU>o0xfQq-VDY}mXuQ~IfwkgK)5=7f<& z3ss6QZAn?j*!J@E_WWF%P=t2c(wGwzs$$A)<@_Skz{s@i{>14ly{eKmqiX)#k1p!c z8#kwM6`!mxlHRAZYix>G7QbY{r!vt57v^M6EC72$Vkiyn-ua%H+DT;H-$lZEI`x~J z*(qNp<@1GL$!45#x%`T7KP{hu?aW3>tqXyskrVCV(NDNs=j*p6-e+BuA@fUfqYUaitx-rVYa zZ1YSUL;ftmWTr2T*$>5DPI#V^TaobyEb(4cd@6nU>*cZr2&@+3!&otJQYhC}GBN(_ zx^uN{LWLb{JHDy9^!>PBiACpYl~o4Y$;AODteyo#ZL}YOu%Fhrv!mQJ5?%);1Etb? zM71qHV%c$8e3W{*uM9OGwwx0e-LN6(bflVKh;6?46}vt4-9eP87|z+z1;>ajGTEd= z8_G+P#e@3jz(sl(9>SrXk7jOZyS5Uk0$>$NXYA{+a1 zpx6y<;x3gb1LF$Zt|B$@veXc>AqpHfP)60u{;*>^D{uF~6vkitTEw-hu&Qz6IU;Z+ ztYkvxk?jJWGnhvmZA@FZhh~;{kWy^piwS4j_AkgousWtA6{$9FzX*%}qO*$T_SX*A zPQm3UC2M0!;{(qZ^1?H|VyJ?FLW4Ie6#c(bIlEA-S^fg%zecqOEQLf5n5alsWL$7e z9Dap4eV5qB@vgWRwkM{$>H?<`8hrB!{tj#$z08M9?Z3#E=mR&Y#N*2fRwOSs7&`5) zH)Oo|^2l<`-t6FyaD*dNK2jt6+vCa?@?`-Eg}3kD6@2v3RQ(^RA#oXr zhJN?-gB#{ECYehv*JJ13SG^|AHhCz?dILZGr{3B{=BYhTjV^z3l|o^+)|_^d7fk+c z(*r?h*_?&~S{s5)J@^JFzEK^o2LpAY}uY zHsWM2gg$spENi#_xUpOkLHW!t?*}bDJn`uZ&n(@Y?BRg5HMMCnGX{Fq=X~)*MD;0l zsgH<7piq*K(uuDLw2w-#Tq1K%@pQ@pNhoo ze5N;xLavLm4Uic`keHAtx`sPi8J>5SV_FWf_e?jn!k1L-4ZEMZ$HD2PDZWe|$=)`w zZNO=YR;LniF=&S&Z5O0~grD8q(j$Qmsj(=U4IxV{A7IM0bD9ITbZ>b12&8d)2n~yh z&24wGf6A1K+VS+$JL^Eo~ zb3hv*K@ZOveWorqzNyqi_gIhfRq@Nsc7E?I4UU2*UO3)-!{W7}&fS6!vUQDt+XZUe7h~Al)alU@%6pHG#B`63dNjM(5{}T41{+#E*oFW0ZaRHYfx4di|@2 zLqPSn#CjgZHnsBv2dY72BTHg$o!#}r`w1qGyX^uMaV#zS9Pwm+sYJ@EbDa@fNBrlZ zdQ*{R10}Q%m@az75WEY?I&-0=c{ELy#ay#niYbzQaf0-^@=FLGYodkC5h9w^_wQ4Y zKU26yM?v#vFj>VEtER|`%$_7B9aPGbu=MsG!5*vQ24^B0ta`{gmDjSfL(B&Z%haJv z?O{mj(+h2o(uN@m3PV+Oi;S6RRC-y$N{X9Rtxb?PK)i&CeFhiPwTTwQ%U#BFq|kAx zo%Z+}a%drh7I4KD>CMUtgaO#?`6I#j>nU%MOHWx^Tm%l74@Lmwcz14&Pc&0$pZEdo zml_5tV%Ix0_3dh+o*X7jVA`|h&Z$7Gl7B4X%Y{SP!DzI|=U(4aX(LpOMI2ku5Dq{& z(5pZ~fv*FRg!n+0nXW&ePM2!i#0H8eXlKjZ7%KNUT3KS%&KSL?In7o-=3NfhqZrfh)UEUfC_~sVsQ1>z zBtF|7+k!FP^>*a0HWe)z_4)EF$t&oCjrccRt4e9y7Ri^<;c!HHW*brZvbUt_!?8Xm zA^RyK5}8K=VNb&QGlNoY*dCb!7#@VPh&H&+(MvpAA37Wl=X?4bGwg8-xe}!CBV+to+6|N}(4vcE31` zR-_aRc{Gib3g{yr5Z)+q4tk>hM6~ot%5@>1?ZPAqmA%by^O>po;*O~!XhU^h;tysM z*Ow!mPsD8kh3p`jiCMzxo%)?=jFrw_cWH_U440VCW;*X_gs&^NG*J$CrIn3N-M4Xh z2u3A++L^gd1=dEgOO>YOTID`fZqGA(Zpi?ph7x}ydA%&;u#qU^W7Db?!B2QOK2H4` zz|vf{!RKTby9iVoN~LAgVf>cPqKUa{;E4kVCzFo>O3NaFQ#?|67DZ!8)oV?EdMKm> zozRgeZdupT#Me4+$u%ujT0NO|OdinOMj^1gUSr_H{_ETYwH9?7V$FFHtxfY4T zt=*nS;lWFX6n@o^IJyF{3C}`!sgHn-&k*_pSUDWJ83mrR6dUH}j_EG}nJlZk;oXl3 z#K$6XREe5udawC6t?w&5;8b3drVNNXZa-TpXm5{-qQBVv3%7p8{HhixJ;MopS z>ewOLyw{Ww*)|Up62hiK?ZoTk-;gW;7B$`L#8x#BuODkqNf`^{j zs&d}U%~4UqZ>pPsvbfW=Bwe_cQiL5^15!4zF`!V~wRqdE6j=K9SiO}LPgRKsnQ5kc zy!~FyYpAZAoF$d4wAF`P`#9x>qNu~)KRri{h^GT-?Iia~!)3BDwxE4hoHq9kqoKDX zYv$5ZGH;T^Ip>^f8gHsO)cI5@vBU2I?4LWGiXFof&>HH?PS0i3{Y3BPyD^Px%3pd> z$GAD!@GB$Etxrdj!H`M5r7S)3*oKv|2|VS;_{oMoQEAk`L;Z2_(CS-!>%OqC z>W;_0tRvaTJg!9%CV|UFIyu{={)Be8G6h$;s!7Gk|D2w`CG=e?_&2#oT%Cl~y;kG44Qq)$56)CU1iOmQ znqx^lwQjk4zgDXxR&&u-P-(f4PSLtZOar1_X=zXG@VCs{WidI`EnX=1Z%kf(Tpy=W zuV{aU@P0|6N|wJa1lsuxnZft3WNR&q)s6*OUf6COMg|dJhOBI*c<_Q*l@g>@jMt8X zsz~Y7`qBK$l7%ywOj{p*tjWC{zQ0UGE;&~Bfz+L+NPm!AaA#)(o7f{?yF}j7d?EaR z`9QQfPe;J#D{mH-1H|x(ifRN7JlS3tBj|y&p3dJ7me$pm7 zzE-@orZ|n!EC0!i&LsTu-X@Fdwa%JvHR2Ci)`0-f4vIw#FI-nC(j82Y z)^Pi}f^8hFW$!3u6s#Nv{XA?rJN5R{R|9fEE`6v*%;YmzvwzA>Rxj$H@Tk3askQ_6 ze=v~>afQbtq61UaT0fOF*K$Rr8r_rls;M+#(Et6ruI=mZe+q7z^r(24?>~)-{5<-J zR)H})Xh6rHt5MW8&PAY~{u)IQBJQ<#$M}f(wXbwD5Ua97v@Mxvs#?VRus5G!;%|y* zQoDZLV&CD{Km4h7yQ}|Cd%iwAHFFp&;0@8kN*8C|EfI^Mx*&AjZ_a0+cg!FcYc`a5 zLMH%*jG!ieefpZ(MXVG1$p)6Px@2KfKjQ1U0_qxel53jNlGU!Sxxem^@^JTV1YagC zeRur`xu_<(!Fz7it>MF7^}SgN6ugEA6e>70@PrmO9>@cILL4RYM*KZn;K*xt9}@MP zC5|f6Q;e6}Cm45oYX8@pZNg7GBWb-&cILZzytnoiDxNL(uf$rbvgoz1w}8H^mMo&` zDuw>Shsn^81*84WRPG=~_^&xcRZPx-Q7y|$>lXGzJR7@L?NX{yIp?7|Li=!`kg26n;{?YKN&x;BL$pMZX8KnqN8!Uql!Ay6q`%>vq4$ab_ zTkk7R_Q2C6=}aq2qR{Lfxqy5gv86@4_o1g9)p^+Yn!VTK2-rRa)d%Lm1dpStXW)Eu zN@H-1cv6Uiy;U^KOZ*Rl=nDyNf7$W!U#B}(L1ECDn7zX{`Ns7)6Y;^i!Xnt*fFky% zVU`5n&K6$lT=!YC{ZG|k&j9nm=9^}#%sFcDKYB={`zZr4JguDb+Dg)IVz~NlLet8r zm?VG6Ovofp(1rwWz)g)ZC`77%>-0UT(sfDEQ`{{*_cFYHfgWKBjU_Y#$+sW4~js&n%_v(*24}=6lu2< z)fnYTplX^lV-TLQ3FD?)4t%#DpltzvF${$##Vgg&)JvYM(6z(}m(Sa+C1Q7HRcl^4 zc1qlmSt`y7PqQCasO&^oop+i%?|O3fnao&*MslQ^GMVIw{{7{eLH@As@w5V!sYrh( zMX&1_f%2Ofhb)}~4t9gnZbA{2x0Fa3R2-&gm1AF&(w*+px~4uWo5$EuWo&i& zot4uxZ*Mlh1#kj-0_w0LzkbN{>sm_J$0couQlhS8O|a$ELyR~pyZ8u$A*Y=8mq%kx z)wZhUOO^3&@)#_r-&P75#O-xiJmy8u5{1>xEBE&eeAY;h{qS|i_j;u#VZ2QWOJ;g} zZ1VoI*n_3ryYC;xw=Z;8>C_I`SccYstQ4)CfZg)2FQDZ9IOD_DAlbFz(>&EarF_Nn z!wy0+so|FWb@?(K9Hxc=sm8~dY5QGGuY>AqgRS}e;RhT|!uICY84UCH!6^z!*Zz+4 z>Gd7oTM$y`7Kd^Cozvy#>Gd$*nSG**-9$0;iOZ7Tr^WVr1em$47hmt_-P$|dtRXwb zY;$fI!U-`w@?GmAd7@hVkOP9|HxA3=S<8s#`GU?|K0;kM5sEPLo)*dm;E6_9W+K+g9nKXYj zJ*c_9E(mH^C2!|x?V0ZuU#clyij55_Ux|v8^Xkz<85X2xbZNHyfx#bKA3O<4SoXQy z3-yQ{PR=$LSlPb)?|@D5zP0h}BCqo;0sx<$JVK%zu@Pno$@m)i`@^Z9lzW_QM9fg{ zbv``Vj?1yB(JfSezaxtB&$alV_Zw6wVB&N9js)d+)8;$&hMv+PjpS&q2z)u2Ub~y& zS&C(Rbf122kJhDJ;n*}zP~uLMP?k zL!%)BPK29c!rPUqF#n?RIdTyjSR{wX%iX*jbo9bKV8gF%3N&zMcT^+irdUt1ESc)C z@l-RCaV0fm!iF5Gt8{YaUH=M~AE{vj#OlqE))wHQz24`txpLcM>r`F;Kt)ecuwVfL zN|mL4PvIsq@G{f(#GsS`X?ve@`%f}!7(tghF`-~(4djZWH9InC#YT{}xO@@D%b$TSMLst-rml+)V$PIJ6688B6Z( zWRlN&OZrirzCi2~y{|DAtOSIxLoWr*O*BQbjPfEj1?V9tr)e9-n28&ZTeR4Ox8ZTA zvraL$ONle@-takeiYFp@hDpLWB^HKmKR6|o*w^Gn7k9I>TCQ|R-|jdG&r0mHmSj97 zfl{!~Gd&Xad2LWB-ITtDu5}puq{BB4Nhc-J2!9kq=b#39MC9Y5ZBHD{r+r)Y9uIG3 z1bN8lg47NvBe8fH?#qQsQsFySI?n@AMeuc=(^h+X>Cv#dNOIXpbLD!;kpmu~ZM zQe2x0ZhUvhv)U(f3;AWr6#;leY9R}UMv`~Q#2X#;z_QWV^X!ruV|+RUy*%fnNAcT~ znc7J-J)7&Sr2*(3^O>4>9|GSVPQaulK93vN0RqG+{tr$C1LEi)Y&7s#&>^3j50MTd zO5r?dZ~sB6CDnsu9gEJvB{dLonaqa3-@3-AxE~Ff+gkPF>5zplFJ~ST9N$(OQ-TQS zZ(^|^IQHI=rIl5`Z~RkR2P)H%skDx}_^3VRTfioh_O>aeAHAOa{-JWCBe3OpZNrVw zp!~U8ua~iTgjgh)RO@`Ujn2PE_FT1Wq@U1L%xPn1PL5kF!o6$<6HNG|KJl&11U z=>(L-f=XjWV{LWMYl*IkH+xylXZ1!)+^JXbD%|hJ6OB6-%qonoy!nCPp^zTNz1o+! zN?8|)H&W+9=SJtoEQq>DQxB2zkHfZtQsW%5g#IHKwc3vMg2Ks&kiAwObrWm6B|IYai|^`WdPaDtv>xHY_s;dZEYd-& z0o`#aVWf=fz{}Y`bWp^4vVBRL-SDEOMlxm_u9r(dkTr~Z))VqC&&`#QqmT(5_q%ig zm*}K>JApg&Hy`tQo0<44QFWl7!Uz5t{Spoc(wsL%n2z5rI_?@gTe^v_Gxtv`ZBFLD z(9)H5Zr*IDZA%ok@v6nn6_u~UTfu&j9eDrp_HSBhzO4nKqiQWPeKP_7p0J$!7(#11 z@0rbjm{&fb>Qorma3)_Uu+ zaQRK~pO4iXL)X+VUy-XQIYE^xsJOe7s)zQ?4l(*^n{>gD4Ha}`@FrP=1beQ(VIyLX(P9XA_6#|5QerSZP*Y&K^EgNHz)*7#D7PYEO=jq= z8viQuIbf&ePx8!3?%&V4Mk=KpVu$VJTW1+cB@Ee4>qAmgRrCRs#fs0h@+YUEXD$iB z@Z1k-BIMjVQb+cq-1+%B@iu`LTlwwb4L^hZRo)C*zPhh>YM(X~@3@%4A3`}JqubRM zEGytU4|d&m+14B;8E>uF`DEXe^5WbWvX_?UhF)i%p&Df!Z%DH#gNlb{F}+i~4$-hV z!@$g&d|m$@Xt&Lf!RtI7r^XD#?6ikl-(^qA67tjW=nsg^Y&_V;E&&3cJnTC>pr*Ah z$+y1H6Hp}cnb@iqvGAP2m6&X0)i>4em>9KIR26UKhZ!8w{r+sRnJ*DP%xdApYJbTt+%D9+X>U^BG zFQ&Pw<4Gb=xdl7m!@f=7*TaxMpL2X`7E0qm^KT}dUB8<-`y%^rb*?Pcatd()Yu^t> z9VKB_kb}wczL^%ha)52(Ja9V)O z@6WpDt>9UnGVkE6Bq5Pfynp?H=&$sx z?B4}rMmzZP8)zrqnGJ7}XE)KYn>X&BzfiMeyHWPnBf~CcJnJf(DD?E9^_*aU5J{Ds zMZ=A<^~lSY!CkFym$$;b|2NMttkdcCl}0Aax@h!&i?h@dli*3*6~*;gs{U`i@&5#3 z%E+$5Fzk1I*#FlUj^e|wmnxyNVad%gTwy=LLOWi~gd}tvtDlXJ>|9dSADb#h|6gw8 z%PT>z9x?p(^3Y{Ljq*i0i7ll5q@yrX#cHqOaWD5)@44mHrNt&gXI(?g1zPXPnO+hU zB!u-p#-xkQuLlwwWX`&cM>xlw`H}nUEA~e@OZA^oWl#Tos{fsH)4~nkWASF%nrD7$ z46gM_4C?tOM7c6Up>B9<(x)!rJP^oCyU(b81raHKnl?{y@O%Mik`h_n0b(hU?utWe z$l|&od1b>D1uZsAJw66>mMz^a0C+7q`Z*2fMs?2T0mLCx@M~@#y3%j%6GNpKb|um_ zgH8u6xXsTu4d?*p;M5=q1{n5Poa&{t9e@V8-wjC&9`m-_=P`nw0#0DUztDb7=4l5B@C!aNl|!{)Pol1RmfP z_x|}$?tech0 zLIsc_iI&RV(C!9Io=4td2hyf|N@`r*uQYCydwwdV2C@)Mac{>r-F^d6x$731^uI#1 zZ_K<7ZZClU1TPd##A*mAl#dILmqgG3O>SNTe^1Nmfk@w-C$>I61~+tln@ol6nyO70_@BVY+$y)+7eE-sHSF7dz+^EU0{A@ti{C_w zdgWHf902Je_r{sYa#>@bG2OOiF?kgjv{5<%mhX%I01%08bU>=5hV1Yve{zwv=y-Bvj+SN-`dC+K z!_)ffvJ<(sdd0@MXAYVc;-oEtCQHp{cO#XVC(jxu2XL(&mG!`1;^Yy*bP4?SDq*DDkiZ@PBpzle{>0&*p?KK4M%d_QHG z(xCC@e*uxgpv?ob>Q*4UCAyDQJI92xv|4V2tepaEZ-Z*KBb4OXw%1y+8got?Kzz1+ zuYY0z`U4X6F43Z~S6XFjD6tr-Rt=c~t{P<)R~Ly7t}p36yWSBwm2zhnrha^CF8Hj+ zI^@F8Kdb)V=^r%W0*1_x#Ajg2qJmoIkIH>4`&%g7?~8 zRiaLWl?EOQL@JG^@a&CriJ?k3%4*&0j9_KxyIqJABhRj9FD`*|Kvg4XMMyx1USETZ z-Rsj0jfEh&%z5uAvvueotTP&Xn7a%(SM4&(Ot*}u?kmim69Dz*3qKMq3huN2SlRYC zx#kdDHz*27AyjGO_@fp9PiNGXarM9zp#}CT_xRvUeH9iO+;wtSnVlY%HQn`0=NJYA zwQg2aEfjDKQX&; zk;u$U(S*`OgAjrl2q#%4^B${1r#?ruyZYw1nO*Fsi`6c_Xy`^N2OcnJ)P3W0D>(8{ zsEu^ly!S&FK56(z!9Sh8sf~_oJKS8P+wiP+M37e{2)OcrTLDm16w3+5jBqDnes~xE z-8XBKa2Y|oQ&=tvKBtB%Oa@yFmR|zlzSsh!W0tQUo+QyS&3S>#YvfV))kSCdpInE( ze?f52F!ABz?#Xk&dp_q`P(u$L{JoY_WvEk>wQyL-FS}DauG{-;l*Me3=_q{A4p6c# z1JSOR8<+4?%qk$2g?BFm?wM^C!Xh&<1tw@!pV}w9tp0z}An60C0pkueFY%R#gE`yl zx^T4o@4{u49Pf&!>6qVj<$H?$ADe@1sYCW`g!aGb?9^=Db=(U4kbQh- zqG;(JY!wRFcJHd$lK7Se#mU;JUi)~sTZ052RB)|JTF$R>pJar*@qK4)s9$DbOw zcl>(;Owan26My=(bf{)bD(Cx*fRwhY^Vu#kjoPce<{|UWNR__EC2;A=Q`L(|U3G<3 zjb=c9b<~BoC{(7s z@25pCp80*pq_=G05yIYG=GQ02n0CXnGH;ri8NPH$oL_2_bw34 z+QQTpDZ2(A!7Zk%z7n6FhBX63c3?CepaJhT58Qd3a`VK;>FPcu<|LS)jhEs8Mm>?r zsKLp10pHi9emFRj#~}AX^4OQsBMnk=klOA+3Ek8isc#tWEBql0I9cz;@M#0xeKJ0V zBbWt}3*2g=7;>6iM7otXRn}Xjd+Hs7BNg#xipe4)xl>2kxdG^3j=%>2?3x{KfcesO zmb>*gU*hsF_;ilHGu!pXKF@?w0gN!n+US_&)Rs64l>$i(>c!#)#e{2l9r z<`p_~ze<*@t;Md1aB8j|KU8H^#;;-b$E(0C-O;hMJUs2f@k@9_VC(610L(PAcHKa4 zDk}p5jsx31UR5s7uF$qq+0Xb2U$CD)S^hkb`^?Bn@CWXcC=O+%jq4!7c9h8w#RrTR zlZ{kZ)Z#w`>wE5;_Y@tWHu!Yk%5RAhA_{EwINl&8Z(Q!NxK&`avzm{@11QXNoYD*( zMQ4gjj+8|r!rRwvd+E#%d!s96!U{kgeROYJD7OaU8o&qxVV4qQ^hm4-=I?g)oqY+;C&MUA_{wP?f;?cJb;>NzkMws5Q>0wl-@;} zfYQ4l9aNMeT~JVZG4zrkAT5*#NUws5^j;0pRUm*$=%FYb0t5)vyYrpe{ncqt_)JrB4jXW#{?EhV~v9OVjVgx8n3#0!03D5iK z-eABZudF7YpeGg}-2A5l`#!+K{yx~8a*xQ-2NZcW;A@gX-6-?me*nV2Jyk%cg&rr^ zgUF5&Qe-G2PM&TG=yHI6$Eckx2-3@m+!I)VO8UHsyRW>me@czAsnQV%deH+A$5!^61H4yn zqA)Zm_t>saEccfjei3wJc-wa9}DoNs8jo`NQ1cnvTC|RbiqG;gcCapWw z-L$}A+KS}D7c2lbn{yV*$6iCsoXFMCquHia9rAdJWx??c+oRSe^Td%~Z9C+LZaTFP zc>*@_+jj1&HKK9;YJKPmUC7L+CZkFiDfbCcsD3?Q*QkvPxT~)p$wGszx6aXc>{Xn%L?Kt+!^)XI|>v$cw;{NdFF1ThGs(;#G4pSB?))etpVpTFE`i z%{*}T0tans^XPo;O9Q_evU*>u6fZY;Ik>kDicho+ScA_E=#k`i)Y(C48<#yMZ2UQi zK_uFrb&{r%rGkYGS{tePv5rbkH{Q$8VpHBO$SpB;P=#{6{Bi#gqR+BVn80##AdR~k zQy`g#YS`~qUD`m=ds!v$ge%p18E|2~5}!{LS$;0Wgg@!V?$^z=kk9*uk%TQtFELlJ zh=r``b1&h64X|(2o&1njm)I{Kex%`!fGW=b*(+6@6#0MXJ83!nNLf#b+vAOZh{(=_ zd|<@8{%b6dpkvF7rrqk3`Yfz>OJab8GvvA3S3WvxKUc8;8d!MZ+#t-R`l+nb{g^6p zB}droSYgc|`Q)43!MnTUYAPF^whKoB9~dsjL(crI(9hfJ81bWmfhlD38i*P@{NsC1 ztbjBC-89IlR@EUeJFj)Xiimz(V4viqlawk~|JcPr^v{)%A?}6G??mTieyzrAe)&e+ z4)-@pY2EE>YupxeTb*`qnq6o6!!8p&S+cZ1=w<(`wS7i&oJp4`B^C8H@_T|O^mkCd z(ww(E4n@lPGS2w?IAm)L9@gLtaYNcoM1Pj0t=ZLn6tF!tAv za?_qcevP(o$ILNfpE%zhN)FxwL36Y?6KN|9l#qkLGPBP0}murXoI!BG!3i4Y{=MYbqMdr@Se+#~e}1AhTje$6X;dZv_|-l;O5vXRE)j#cn&%j{(4))Ffi^3tUtU@&Wz zg;B1HQ!THzR7mUKzoii9cnjXX*uwph)_40B0!6q4Qv z?_fG4?L-au!!kAdK~HO$EGR9&8Y0V(((+4>QJ14n1AApcYACA{%w-RO45Ie!E0nJ> zl$LCAII47kZUbibwHPCc0!)e2fZ3?L1fe1Fd5ZQT-=GnC>HLFO`fAx{{(wG`U{-s5 zL(^4jO_vY~!@+!2;@F>=3OXftOlr6fZ&dbGOkAdxh}@m$MU!N zXfD`1D`EWC=NkGT#Kr82Qr2W$#6!Ms{S;wj@IG=Y7wVPS^k~;i`6t;ZRi+yLV zW{Dk~;r2o~5BqcpugBUs&a9@D4xIpZFvM5^u@<*Au9UE|PvPdV>^smg4)KH#Y^v_H zQ%l1do>#VH5=(5C+me=tg|XB&Azp4;YF=w_4(y*hgIOwjYpQ2xOlMcz+2~i>$=6X= zYPIUfR8IKpPNe6^w)>R0@GvDZafT~CYS;+U8<%~J80=N)a_!lC|0w!6_ETl?2`leQ z{fWQ*)C2ohAks9dV#AHIhHnmh9-=e@b;mKZscPp1A21{J4F=STHNUwHpnt^UC++|l| zJ?~2NJR@Akjwx~2!(2Uk(}>Ni8j9?j{$_hv`*B+35L4X9OC1jmp;fR1kIn5mxUZotNUhqxM5En!?~e^0 zwa8s0h}N#YGbk{?zG9GvTCKi%dG5g|h?;zh1s_01Rh?Wb)eJUnKykRIRZOIj1?rh1_Z16fgHwtVT;HJ%($F{=yk78r}qT$X+McsfI~1 z))dj$8a6_yNPHWs8M2c`}7M})cd)YTwUhy0g^c_ri^)NXGa;s}iiM_~Kk=0r^h(Fl9KFVJt z2gKA)(YP5#8MK+oVKX;rrKeG+b*Ta*M{+%MJd>}P491E+4}48D`^E!@mpQjo^Q z7+^daAr~up-qp)B3C>N4EXcJb(@5o%bcYtL~x#}N`SoN z7w!*yZ8nx>2(3Twd%4hyN&8Xf8zd;F`i28tmg~(IVRN1G1}BoNqn4S{m?cBZdXj#T zk?hm1h&X5Gk~<&__#iEFcaTDI`HeH-k5Unr*{iOTUO zk+6HXa$_IO=oF<^Ok)XSKK{$mBH)ObE;I?toLh)?6f^fiGiDK3-3PP^P4T-lR8y#@ z6Cqc#5*G;Ps8JS&HQRw~}xNrth{^6s-G1 z*m-rJ>ar=0)2jH7u4`mEHu@WmgFmc+()nPc%~aC_?#Xr!HG8RKCvqFZZ%3A=g>;*& z2EVN+rp_Ie#E8>%{rq)YNf%hB9aQ%k+ZL4+s2$uoY76hX7|XL>8wb6QQo>uj?}3D2 zDBAXC+FfA3LWG{zi%({baWw#=)si7)%} zOi}t-La}WtxD{-C9fr)qHO{+BPFvrxFU3A%~EBxf??I{gS?71|Z|`53USy7-+vUYxuJIC#?aPsO%y;wLBMel}s6#w(*dJH3v0D*SR`LrFiZd zqox#6sIBSPbRVQ|oGRTuZ4>_q`@{BL(t31#4AV!X7HPwK z`VzEjg{Y}7neFWUp1UvSKDpAGs@{rtdaaF)#>wIGD(iW0DW}rj48d4aGcQlvobU7Q z%bBE^hC+T=Q_0nk103}f4%^ww-kjHPu=;4Fg)WbebWg4iA)y!Oc8Ru{d8=!0@JOly z9_-Tym%Lw`bX%~6{%z9pt4HoT3j(Gg{f*d%nn^o-!k?dnj3K|+9!uBn8$bg$^G-;v zbPf<&iF2{j#^=v}3+~2!x1JRZ*j->xP-}dxT3vq0ei&S<+{gmDAcFZd* zbRYRs|9oSN#iK3hY|0^q(~L2t;Ws`(`=PRgIiJoJmZvT-OwjL-BH6Saqf!7vSZl@) zl$3VxbpDxO0m3#bcmTt}ua{~+nlDTE-kxL}%SzB+{rYIuC!k?eEofrGfB#mF>G`Rnb#%i~*2&6SnKsx?RMl5_Hqx53SFp zllQA@4e~)6%1VolM|U(J1h}@6qEoB~vZ`F`hlGYpc8r~7pP5)PI=<%@JT?e6->y!h ze763L98>A2-N)T-7AJo1y^iQPhKz@9DFFe7AoRzw>8u@t1e2HZcjc0jez;N__f-eo zTn+SP(O|TOA`_`=?b}wr%e*vwddJ24;rCrxJq1p^f=$e`9K$Zpje*IJow=dZj+@$i z0$#;R)X(ln5kMF&T>cfXs$cNlewW#)=#d(ZY^pg}ed%OeP4&4) zsN6fp*$O8-su}MOnRtU5)Vi))ePf-Kk8=o%+jWGXYQ)={n>6Vf(*j7yuNpEl@+b=h z#lmv6M9Y?W9r)=;1lv>j$gRyNo(vOggEnh<2Q~MKtRt8WFQMWL?m9Oac#yxCo+Np(x%swrQ3M|28?2+7^~S zXu=@ULeEBlSrZK*`$Jf7xga#Gd-g|%2!=qfu&}VKf4gv6bY#`x>_9T-o=8zELf@I5 z)x$bk%_f%PZ$$}SH?7_(_PX~1P z3_MO2**WxF**TJhvq#O7g(mf5mB;1k-wXRX_6IwFIF%ORsYCbC966l^(gR;>Oywte zkFWMGNl}NN6v;sAIBeWZ8MOP|7-xRT9(cZ#i7xN(c7Q~9B=G7r*&ACqjt;j(ASzPZ z`0E+krNr>#q@Nd>GdUr+H9c;Uk|!Z2+wWzk2DRCD_|_#OnCkac`o2!pX7`w!G_%0eE5jJGUVC&B0wXjDi5&yCeo~x_) z&MJTPq1ySvFFW>&bd^68?_@;^mC91;<%Hk*CU$%PRkjSW4SPG@x^{Y2-OH~!B`d!^nr)aSwP0UYjp}V-DP_^uKMi@xlk$U(?=Lc9<~~@9BSxGe~rN4#UbVK z^nuc$nhi*)0Bp=Qx)TAyFo^le=Df9^+vV;egA5;Mz9HMq$rv=4W*Q#fCErn3_{u|I ze*a?SEj7fgAwF{7^uWFz_7dB?E^}ZQDwI-h4ryAnVHmb3>P<0lkv9~r$h|Lf!;9q8w`wmJjJ&ooexUi@-*lL~e|uNB<*_*wBFG07HA-DA zi(Gw^V^k@ms`LtO=|@rh^AvWq4ZbW}N|(!^5LK;@snk5uM-;9(^q3IZ9soT*JsL`|8jhRkSp;)~d%=@LgZ}>~lgHo;k-W&^Wp_^G7|6|&TFBa@A$xPh15>eIpAj$ ztwws(^^SZFPFq;Y^Z zqVbZ0a!qh3%hw{B{K2Xe&fq=Dt&pgmuhv?gal7At3D|a7ao-9b`~2afT~~Mh5`sl& zGmGr5%LMv}olwi;jGN-sRL*KNcwhHPHVU9Mam z|N2wKN{8+uheB#tpw2BeT)_7%vq{`~VZvJIP@Qplr1_5{<|dn05Q4ncTN@s{5!0i9C1HSc$i7fPBL-`(Zfxel;-xP!%GIJ{=Hy;0r*YSRnJhgB zsTc-^l;FP(K)QX}}bpMXNL@`;{1 zx8x{NC3}yf1MeuOu4I1FRlc$r*KRn>prcdwxNQ`1d95&wE(lW}<~NXz>6CrG5(hb} ze@W0_R5DHCa!mS;HdcF{WY?Zt=S{)V?l;~zYJ6Xq&8dIYI2&bOu5RJ6s!=N4&OIZnz*v%a4`?#pnxVpp-bpq>$tYCVv% zHBPU*c~idYnR!5pF6=`Rk99}Xr9(*iN*{YxLhxRZ@RD+SE0wGu%J17en;l%KeD93k zx({jN?$`yVKS95e+xw|j`$ulzm;7Dir3c}Pf@<=!L7dj z9LCaea|>P6oa~H*F1eOny^!pkyyB$LWX{vwoEg+_QZXJ}hM#tH0DSs1+ojugnWPl% zzmz{b1s9SX3a_ET!(q@MA=7%%0LP>}B>hRAV=gImdc^rBg7RagovUqKxBaZM{!>tu8e}C9&!EHd6cIL>i&oQv+9DiUP+}l&qlkzJ&R_(cPv*)*m9jz>o ztkLA!93yPE7J7_H*_|`M5n~$A9|{s}N0A92 zs@Qj{X^-s)B76>#nRvI%G&gpaI{+Q`dEi56<&d(qV{84mIb~&#>BU>8E_3|89&Fy( zdO$1Irg4PS(RLnXN3|V|&xL2SqHp&#%vs)d-F1P2&^OQ)I^%Qf!2S%x7H=$3SpJUJ?|D_CHVtJKCx>2G zoTJwUV)|$L8y9K>NW#*oJ@8Lw6lZILj_XNuQo4})o5pab&`l(udjHWDAa9;@-O+bso!A<{6Um9qri1EE%78BdbH(BK(FgRgSrZk1!hIvw42XW#zj$FCw1@uH31pd?GB~j?(?? zon7N`pgrFly1fzkXXI^04Q>C3)l@{Rd;Q+?=LlmNiJY$m=8pFm-bnYcW1!!~UdyXE z@=CvR=e}mu@`Ba!UD8Ig(W*&y`sZ}I5gD!!JF3GUb*P~_Gh6Jzm|!)(e!<1#9mD|P z*J5Td{yDWyx-|6lhy3*n=lzrAz{>q8qn}9|Xw2BvxBHbGo6QLhe~uqiuc)whFQTSf zMFKN(nCqi58ckNOcI$_hHwF4VSE>rf)=#7jJpX943UOTzjOU*(Tg_Uu3ubhT3se1; zmR{;T{o7uQEL@}+XLw-q+{R$#VgBJU;grb+C{&N!KA%n6*h`qPnk)-*8#zgODc70* zf-%5!qJ}eZ=iurR+q~;I0vUMgebb4DTwjf3f91{TX=B!rT`?Cq-;u z&zkEZ+@hzD=A`!)>yhGm*YZX#1;ZvarW%=`o9s7W9F{g8Xk~28 zr^4>6t!)ttAC0--6MD3}@F^ltRL}l{qmWhXt1VC>_k(1yqqKAJDhZec;<{&`TR9|B zpem{VXK~WI>wurgrSZ+H#?Q{CI%XM*MM)FyO2+eFQo8+RkUvcPy_KIdtdXIfffCZ^ z5$(*TDD~fASpAP>(F=TIhP0U!EVo_)_P?(oe!w&D!KDDbZUg$?R=F)mg~a;c#yS`Z z9Hsxc+_)(5V6XjwnmcR%@w@+hB^Cz^tCZM}=J$oGfi}VaT=x6A1;P>8ItWgE;BWb# z3-vKW-Q-eJb=)4^OJXFp$KKxyPl~HArU;9&%kLc8<6OQoYg&N5cOZf10n#5OY{tJ{D;wr zOiawYMRo(|N-X2PnghBXxKeD-gTX!)r*Q3p(Um#{D*5GG67X6eGM&4d2q-NOeTYuk z-udD$lRAl*XLnuR!KVeHEInMnE-xH_qu7Y4z;r}Uubl1#`yCmMRTt{Oa{v!IqdRju z23S{vcgMsaYuu)@S}=i_(#@@0UkxqU>B|iF3;QnBe;FVu=pcUs0tFpC^?$MR%Z2?W znm4^x3u9=B0&>~G;mL)6(v<;NFzokpM@IHD&+mjj>Z{j;m+PcdeHE@Y$`80xK@>s& zLG`sfz!7LR&Hb&5aD2WKPW;*7?CT z+q&6Qvguzu;h__4bGO^K+&!(l)k{YJQRDUk5de1bs$=S`*iE<$xV}v;6S-r#%uXWL z+f5X@E$9H^$#4`OB=#H8PZ{TV=jmsKrzCP=cKcZqhU9PZ!U2u>)j5Dk^zieEveTIK zfn~1bAAfg_Cdxy*i6-J})>Jpc_mKGyvqQfy=G{ZSMJb)E7RQj%$`_1ra)ICF?W_vI z6puGQ|Gnr2a@8{y03g)gh+HN6#`0f4JMPrHX5^`;j4!0)yx94B;fZt_go2md;%Vi2 zhXqf$PtY;Za)6byo5(;W7Ey=AUk#YK#~!O}w;N~v4;U#In;|k;9@(wg_s#BNZ-JOy zEkLWRyDHwE$m)g<2f)|cGuzN6_H97PeVOQ%$Gz84d4W8@j8Xq3esi2~yky@T(UHH- zE&H%ie3Ry!HbXm+XwiQuA=9*wdpYRe_X7ij{W7A&3nUrep$J4^C$}UY$UFGQ0{?>H z`a%sdqu|~&c%$+|KA*vZz*=PPCO8R9;rqi5(2FbKeKL98$(SH=bHdB{uizOp zw|wdX&>S~nwmpfOK=Tf$0V-K#;G^^Tz0wbR0;cc`cvIwvL}sND`+9u?KltK<@ym}? zAi6ciYoto%2V24irW5GQ!<3{(-20?YAtrjK)~55e(--dN%_iG6gYDvh>8!j``Qk-uy^Y7T5d1TfwTv& zS#hQWyQOOrY{Q}3z7wcWg=nJz1i+{NxPS0=jZDG|wlFo(dFb`kG_RI?o54s41f0bg zt~^j6{~}PnSH$@KbCPKRNgR_QMg1p2{+gllHRyckk$&-PH}S*V%?!kkpXl+3dU=%` z0MczIo2|n$Kuv5Nr9>Mt@WptwO!eTy0`ahWeK4^3__f;ua?(lU$BDTN>)S#n*rqXK zA?W@v;CrSeN>XEp`uyA2R12b5n?nL1GAE!=MVt{if?xlEap<}!ozhObFO6;=K3O3L zsQ!H-`j5N+iBPa!Q8b@@$`en_#ctrLn$m~kB0l3xpzD03de=I!0;cjR6RZ`TbtJcv z;t!p6i65YU$n8&-b){U)jT@JQXEEA^-1A8ie#nWJKw?Bzdl!wW5>FlRh#WL;5U*9* zZAfg&!Ou!)1B@n`E_Al8jd%^}u58@0tWGkbH9 z4-n;-?8rV#c<%T{8N(i4mRx-NMdpMga;xSWjrb?x!JDA{Wua zV%9EI2s@eKA$|hX)3Av3Gm)a*sJuP3{Q%mUYj%@-bjINS?qg7gyfEaOO98Vlfdi)f z!U&|=;g$s+K&KdWMHcipc}O#(v)+WVR?9Q2Q=}fk-|{2mi^%2O3tlmV60OHSN}NYj zjy`EL7Z5IvK8Mrf8J;l6GS*Pqpj?kkaGrZC1=L(yb13Gsjb9bzUPfmi5oAj)1-C}> zr61nKfSWMn_DxuSupbbC!S7n&^qc)0l;fQ$AkTQl?K8leM(!t{S89zfV&$|2W*UAJ z*gg=x#9*)M17>4T=%pPz1+JW}trD-(4d;NFkPlr_?Fg^Jq5HR$sddS`0?0($zi4nX z2=4sQl|`k?>$i@Yq+v7yZtjjGyEy{$RtP5rhF~QAoyyvV71uM;OzXfxWE1#4_0w}@>0ZSNF6V(v=C61|C6 z@ewe;F0;XO9(Cy?>Mgd9_g4~l-M1`iNS302 z=x(-oxt2kr7f&P!BX6aI)?g~SuSUObqXjj{^GB*FqGX{ZpLbmnbd%AXSpz+9Zz1p# zecqaii`)b*y^;%TK?cp@1~DV&U52MR>EqRW+AcZU$$>jSy34xE=8wWy4>|y?{3xu) z8xDVVmIsjh?@vMjOSf6Ss7?0<|MOJrxM#V}$+8vUww^CT-|A%AX6_gJ`M&#S1^xtJ zXg_6lFdx~VP3jn0C?>?l?kB0y`Ln{~(e6H{)3$2-Ra|lzBeUBRKo9)5p5HN6f|%#> zP@%?Bc`2!Cq4-$6dK0&6wcN$}#ZxDHq-8hJ(>k<5n_gPTL{B;s*ym1E#S4y#2mSZn z#Uhsh+jdHeWkDzEp(|sLV8k(xA5M4CA_EM{`rC$iaC6Gn-0oMZEzWN%D1BY+rvp1J z+4C<4J$sU_#T7Vmnm|A}y`fShoQX0X>qz7hmnkgHbm6lkdeJD&uHs83nphnpra^dwE?iR< zbg%qtQa}&?jn8l!%1w{9Ei+OLjS43%ln=18ZUxv0b;@wi{qiY>aw3GvoX4XW@~EW$ z{C*kM#NJa!i(8}2(Hdkmn3~Iei>e-+4_`C$=%hw~-JOcinb>%1^gb{u)G7239vrZB zt3m|kY6W%!d1zov^G|+!ja(6zI69$D7A%H8tPaWWD;(NhAU+3Nx?cyVeflXURp0J#)T_Br&r~`LlHjodW?Ya-(9flv~b6Eb|q^sCWG}AwNW+ zbz5;ovlxvX;(j43glqaq_r;exjV8bV91{X z5jSeY)z}HX2;L~3k-C|6{k%C6b;8ortWC2lKnZ`eV1OU!tl z%V|ps@GyJ5E3kYZ4od+WfCtP+Kd14}VApn(xCAmmK4ivcNaUivf51aA`A_071vYW+ z1BYW$E;grx@I7Ah_GzQqZWmT3BRzXTTPk^+0Iw6>W{+{^lO}RCr#P0gax{O&;Mmkvv5Obqw-AdPexVIkYm zOwxspCyQ-NN|Pat;~)kX#}Q*SJ2>rrOjKL3fo5F#J154+n5tZJy$Y@TTmy-Po&Q(< zNJi~`o1BJpOf#nmX(4M$`j>Ce-(0h5Xku_>{nqm#ppwE((BCYPj0Du=Ev4tm@5SUY zP4Z)2=cemV`J)r^L$+ap>K1m$-<`qwm}}}&J$2*m`wEm{#IHj}d(0?($>f#a*juBKpvaLf&LLRLNLQ#LFjeTJF2YERUNB# zQRsV)#9&~y)tvEhtguq@Op!@jJ!kikHQ#qyK|J&Xd>r7=hNG+(GzRSotYZr_{tl$( zf|CRct-Kxx-<){Hfmzf6J<-YFSj!FBw5^wZetbBKXyN2*({(d}8A#8Lec;MXRoPmd zs7wJ5>^f=hEnN*EkO<=uTUF4v`#Rx{7?0B2_Fo=jWLsVa9$RO%{MsPYp9HC!572O- z>VC)jDy_Q^52xJDmeO4C7$P66!?*Ifq@=rOG%FR`&E8Sw>32EWY4{Ou_#sW6H;r6} zj`Q=CtR2{zZQG`zf_sD~Wh_6|(gLk|-r#Jq_AUQ<>W${mfrzEHAh8$)1}qghQ!|;Y$ZDE}yxGciZd*?3 zdY?^Q7f=d1hPzD~6f}q|p$yScw+aJ=uV1q;9hqvaXCzP%z{VgQC22%8rAvceQC8== zfKV!PKO3hobA;mKRn7*~!S}XXsgSlC(5SPZJbzW@TA$4yl?YCCWdo30De9@eQL0dK z23oInYYq*u*6ZnmU27@oDuUUM(&pF^O!4MB^8DFp->d2=oF<)c8^VL7CeV2nY2UNd zP~GbZSM|yq2bRVVlv}IDU+n!ED+3$ZR0Y2=s4Lcq{thT4?mwWbyfS!rtgZB(o@QC1 zi9u4Q+mzI|M)z-#s`k~sN(Rs!O}VQ|2*aep@-3wc4>wqhM_W5pt5w#NYTo3m@2fFB znCjf1sW5Nal_`xc-O6n1-EBuCyAEB?jP$s~A5YSA92uQ@s{~rU$I%vOlMH??&T#7@ z*nN!X?PCg)4XN{tMeLUAhNVUn@^7to?+M=uEUgw%wFq7Jsh|u|kA_PK z`eEmN+sAn4b<3d9#}jX)5|>WveaXfVdHi`$jnGP~KNOvbvkOVeZ*XZ*3U~l5RrnL= zK6B`w5YA@qCtRH#5wX^siM73V-F{a&wA6*J%6^Di z7tPE*?Oz zVb%5=9&*I67g+1Qka7n)y_&y0@V!#X+P=}NzRXz<>15y&{mdu9rXCkM5o3Ljf=-6C zrCeesb*pR84xO3iEEL`0S49l!%Z@#}hGNuX$=%t=- z=x%#u^}t1}pr)ZJupVdbmsazF)&Iq(BH_mQFPd~Jx(@_|pyy)p=r-wI($keijHz(SV+)aI>8(xS-~S0E>?m-hubeY6NT-~W)a27+9SPli=^Fm z$%*Fl3Dk#Q`vy}v*Bs5KMy+L2;9;F#ihe3v8BB+5{omdKu-gYRgRRshg2sYj0&8KWY>bY`z2C2FT-LZeyvwgvgR-$E4dqS9x-+w>e`&ljSAMdO?Dod zZFjixu`%ktZ5ONE?Wh&C%&gs^hRF&&6+m2p>f^{c*v-?Ki#q8!|P=0N$Vm2khY^Q&N3c}2ukX@UbPIVGgpm2uMF-ls%1wOYv z6U5x8K8hA_fU9X_6x_ls99ZAu;pK!#nVk!oP?}XJx=PZ2r5DwJbLS*Sz?X`Rm0(Go zdSYk{i#wS%!;O)8=9~pO@yfhOg_JA8g-mEs(36*`ihKN~lu^uKes$*B9Rk!wAZ3lQ zw=5cJ7Ry2uBXI%Q4}<;$2)AH5eyYmfDB2Tl#ck(-t@kc*XJR;Ozlor<>T;R8kf_JF zL?Ky?JSe;1(A1ck0_BVtQ^6yzD6X?by*-0Z;1H~O zwVU%QyXfS?i{C=L0-`v2&H9-lu0VAJDYAIe@Zw$_jlT_~J}39Kq5|fT&WcOc{K!%KSoU&Z61AbYnDu zrdUBKWUAWJH(J##&2TWK9mA(mE;EAjrDDTtmQic3R2aU9yeRv(#reoU-4Fw`fj;pe z-%;MuPENk0n>+Y9SDlP|sX!{!hU}WY5Z?Q*R7mqE`tXBX|GD1HihPCu@f_ThoT+!i z{%0khPrWTay5rUubKFm5lc{O9 z`udMqQp&``dGr-F$COeu z@`)Fl3hM#{NQ&oW_<2=1a#U2m&ftDRQu;Go{dj4wrBb?@_|}K)7b}*8T~2F)bs7t8 z*eH7Oa$-s^p%Ww$jwhX{3Eml3d?G6HC}V7|OyXu)p3dD9mkfrWmEA1p`II2~pH+P# zz(b-Orr>)p>R(ha&@*k^=~xQB#4a6+n3kCMOyk$rJX-~xW1=&CX^Q(P{u3H^@!?QB zck75{#izZI3XS)+isN>mnpvBLt0eu zQ>yugvo#e-=SpF!zfK9qOhZemFtwyZWzQ%+;KD z@0HCmg|C~Y2UpI%ox^Ya_qB8D4k#4jb&57E^Px6S$1dBb^iOci`-T`LH%w8tGWY-H zNa=|z4o5uReu&_?0cz#!NJbX>BPtcXuIRe6+wZLIt)1n|3)t^|)l4tv&xTT+64bm` z8+}Lr>)5vlp#3k&We6zcY}ta~0YuEN5z_h0Pk3=c~fp3&6^1O4=SCFu-PU{ zUq4S1y^ThOABP|3-36-<{--j_^p6Sw4EmYrUk73{6+`h8CzjM%1+A$C%_@9S{h4golWD&uE5{(YuJ z!pJnhla%vM)AAqv17kix%Lv+cvmp?Ol~hsQbf9Cu06>ljS)dhf9z*P_{KFRrdas=l z+d2V2Nwd%((rkBd>$gBUpfv2wTWfn~zNr@r9Yq>3IoLRb&I&Sl~;f&A@JJ?M9w!+Z9P5li&?r~QC}MtcfS zwz}-c+nqhJX!4lF0k6ZqW~lW4PE97S@Egv)h4`QYv~t4(B*7U%27NT>h9{B92yND61s%bsZcbJZ=_ z8%52Q_<%TcJ(#-W=Rgfx=QM0+{I@_1{A+Qb%Jd8v<8yn#r7BY`JMa#GH#H7vPX@_c zIEg;AHHAFrtGe<*x0|MWvvpm7Zm97;g=91T1}1;@BL&*Sctv`VmQ}g~-Ejn?zEW2A zC*Bp`-w+@z9wZ42F5v@~V@uguG7CCwN69kpv)kCg!c779e)s2op%0wJ z>;B%@riZk7_oI7&Y2izSk^Cf^|K8mOlqZRHrYkC5jjjr0iTeRLxe~Mw=BEd1iWJY+ z@yM6^>sQ#YjkejmxPk{B2>$b2qmbzn-pYDs>-T4*xLlO3!*!%;?0hFdU<=iu?^jF6+ z17Xp0c;;U_7=@H_S=?Nq1oVe28yh04PWVd_Vh=&7n$1x;I3pi)s;kplC8F$K0StxI ze~OJ-Uin%3!});H3b|3;PZUWobWWq+_fu6|h+-sVn{uJX(Hv}!N4=VC=S{txCUADa z8|fT>{3UgGp#G`07-dPEq#6S7luRE!WZX9bCrWsJ_p=aeyAn6vH+?ED1e6}H-J zu=35pW9|Lf7fu|GkX4${2V|o0Fqp^SV5;ljx0EiFBt6Em$lS^L8Ys+Kfq~MG=sYm4 z6}QKwOjNhf-iPBcMP_-@!>1Pjx?}ml`S9aTVVJ{ydyKz`qiI0k2;Dfm^q6znt5#n9 zPhg(9Ef6aP647|yw)dKtt$YG6L+wejSD${753cwE(_?q);J_Bvku3di_D_qYA# z^ad?bxR*gmXYBF*gLZIv1@OE%4ci*k{0sGSd!EaCts zpdQ4rbDQfCnfj&n8i-!i(VdjBh?g9fxyb2^Z>os_R~7P={N;3PWD>p6eXHjA|Pv+t)@ZF5w?ZJ|)KNN5_vl!UyZ>-2$Ap#lKPhyWj+-fxL`x|VXuD=5t~#1pzIR(G##E0~?Nt<7Jg8$fzE zT`Y&{z-`WO?<$ogxUvUIPX3sY`*;*be8r$o)NhoMuV9*%kj8zPi zmnTcc-gIanf^c$79`LPXHWz6?T`=RSpE2^HT|HQyEF>$E1)#wtv)bUtfavHWl~`cTZdOi2bq#=P5(vmC(> zdO}pdQ)+C#@-j8VUv7xlYYQ|}pgv6v05W)-S5U;4iah4}P3o3y{Yxyx3+47Pva~#o zJvq(ULaD{9XYUc=zJDdBQ?TvzB}BX`eQ&Rp0SIQ^X%(Q_q%NX%(eVflIiSVd=Xnc0 zk?ghk^6Ax~6xjz*>cu{0r%&SX=CkVKX;yy}LaeD*v55ApRd9K>4xSnP{j6`~joye= zFv?rHA$I0bS7K=2h?F?cBZv#Yh3y$UWKq+d0qVHrhdQw;q*T0IujVuOdTz-GXF%1# zB~i4IiZ*D)pYFIa!u^4WYJ?Iv@EQO(i?JJ(_My}RyfDQ9ZSS9YGmQ8!c3R)iTE@ms z*}5rjTNhxrKE@#V006NQ0Ji->4kJ{=>Ee@L3pgR($t4DsaKM;kE8#n9_QjA;T`i28Rkbtm%(7irHxrQqnm;nwni4aYo*!%iL7sj)P)py!=ZTX^ftyk^4iRpv! z?ZLsw^|5(fwNOI54Ds}Q)KGrV7ZU>I|ml)+*-5__fR+c4k{tV%& zEkH0X0}!Hp&%>;Ii|I1;?{_S2?-L%nPtJw5!qCd2D20m(pFLY5JUqO}(T+mOlI{mb zQmCQlg8P5K8t1o|;>u*&4B1J~MehY|R&x{(ecl<-ib8oTz%6&GY&^(%-ah!^@L1#qqB^Mb`7e7 z?E%%%fvifcsboLU2qk4z9O6qW6b(Odr*tI|*~ghk0~IxdRlY~*L!$Oa8aF$^Ci4tC zox_fiJr=RHXf`!Nx`j$ZK{5#983t^H08l0_1KLdYDFKoE^xKyf?<#J(s7IVa2JkLz z9dXX*mhL>J{?J+aGI?|p0s^aP0%HKMD~W#W1h*HXi1spvQMLEUbALdMCwIK@e#LP@ z2$D+pLC^+g)Ju2)Ml_sB@7t{do-Xq`o3@zfAf+5PywL}4>s_RL)Pvsisnqvf-I={q zMD~sW^0)AzHD~qAgMeg;ZL5oK8$iF-K6=~`eeHvy9QxUeuK}A~$<-U-@!teewVHZS zd?}lu4sF0{^DlqgP0iut2_e|&H?`07_c(Qk)n#Hc_g zL+vhz&vK1-(de5u?9zFDSiMT;K>0f;*1}T-cjtu7Qo&XFZiD)Z)%8I%YCXyAW_X9I z!S{j=9ja(cI7_H>J4^yp@r6JtA}6*HKm&C4K!iR}^T-XEo6c1X$L#zbDtEtVTLW_q zkG!s@l45#mJXNo=FYxN|+4Wb&W(d9ij!?BcpcnQulDsXl(nfaLVypNPt3SBBr?kb+ z)Rs1E#V^4Mf)*x2#=v%sRI#z+3>$D(#o?K`n1M_VU(WX^Hgh=N+N!Ij0 zxYQ=`ppANwn-c)dH2gh!;1ym?&AJWCHq8u)5=8Y zOt{0jo~>?vma~54(S;#-`SkFTLb8QUYVE7r%&m|l=VVawp8`t~1C-s0AYSGTo-q)8 zERx+3mQ_+LVD2A#nzfMDFoPjEEZ|hbxSAtDb$t^k?|B!Mm2U3fv!g%l{5i8`d6Uq9 zpTYa9e2G^~MfA{K%y*prF|Y;B4R4Oc;(Pd`oKft{K~5%lip|&i9cVGMW>Htf3xG8eq1 zZ2XB}_)+_)&%P#I7MjKS0*NMuru2Vgk6-|7trB+DVp%6@Ladk|&iQO40m!-n1H_$J zd>+7Jke!d~uta_Nu!~;?TyuDRJtauCzjA~k1)>5$6d>UAwZ61-d-C_;&YFgrxWs9- zKJcDy&Ms_JAPmjQU9nkwYeHy2jHBAiU<}^JP{9|j^s{LubN#a3=d=%m<=R!I_~D9h zLT>cU;Q-o~ogl)PD2(OjhoSV<@5W=u)*+jt8Z=F#44&(`lNk(UZRxK%I`Tb@g|)?> zydC4AMj3QIe`GEd7@ADmDl2El_3GO{18BW|I0YH#R(#J!y#$XeJDuTQnZC|)TnXJv zv1!Ek>)x&B0->ChvNTOtR#&ljdir*K-GhA9&K^Y)BHlX3z!uU~6#*v&vt|AI0s6XE z$*zl?;oCa|;`buv2JEq}MSht#2LkrvE+Rab z_LD(26k5)Ncf?0O{)FClW;rjE{?loMk-3_*v}YBaV6eLI@*#dJEC8KNJWny};9_dz zOZ|Z)1apqf^J4rB{qArpk)K6%HF3c`2K}M`OXbn13ilp*IHNZM-jx;6rhxBE=C@d` z=XO(IoJgr$zq?2DXE2XVe4h)l+^?aXn6cCB0HOZcpRBY?8FWMWwXZMmG4)~Ab~NkF zwe{*7G~+N>-{XZFtgMVit`Sr*B;#8*xFa@DQZSI`7U~69Ej6xV-OCBQ5^?2g2!h-_k)Vh=RMlRtAoZOGxgTy! z{4EXlUX7uJ%3|oVcnZU-M4LtlU#D#r2(^Wm!jnCR5aWyI4_?CL@*zd1+(e(l{I-dj zH}YtyvFnKld7>F=GLr(!J^@&6>t8tQvro2tUXkp zU`N2H_+Tm(&u(DpQy1O}#77w+nn-5k)3{5{@vpMvJEu+4# z;q&nKFEvlcyFrIsq}7Vm0YSz{#o?MZ+aR42)ReVOs4rg>oS>ZxG}}yBOx;!GT)on4 zro1A03(XfY$1(D`R1kHo*kqO|B$KaxLjj>OiXJAvh<2>VMW`EC#!t;h2wg;Jxco~o zxO{T;trs33Rv%7rb2Mo2CiUi<vQRvy31Ga_+R1FupZ8EWlEZfNM%ssNwh0Sym`o)6e`{T52>Dkytt!!8bav4K1bm)XIY` z!BO(GhJSO%hx%ZYA|F3Z2hLK?vg&{%(h{|1nh(6 zYRo!ns=xDdK;>^A+7vR9r;_xX-_VanjBC$ZzmWN7XKHGy9oTy{l#^gGJn(zG6hH3X z-w^w(wDe?Xz+d049gzI#rPFTvIKQjm5190Q8R1$X9Kl>Rs@x&Pfu?t>re-Y;^kZ0A zc1$;|aDKM5bk@%N*vg~yclWnW>vk#2`_n~`Tk_KuJ>2;(hR%?h>TMAqeQj|`baGO0U9>&2Klfa6*7K-YYinDTh-+aZTR5^(G0n9p zK%ZI1OPyxxFSym3W=#gYonvZ9Ge`Q`N_R)!J!*X`=j>5aJ@CoBC}C(&7FH@eHd`dT zuQy}(T>|j;mYs7+iYIsYX={(ZG$M$|FY}<3fa+_bQ=S&6I*-)=n`F--KP60#wgeUM zD-Njp6v1s#QgFbR3`^5I?O<%7tC~puD(!0ywdR;B3=$W$9`wT?v{P@pG zG{Mf4D9WoJ6G^1+N8dRa^`c4j5G6CXYX`>9vRB9Azk8%(AH^Mwdl$mPE7@PLJ2Lz( zm*ns%Qv2d`qS<#F#2k~yb95ozQ{b$Ez4m0SY zP&U!=c}&YXo|dQiLT0FRH}~W1bi`3H3x7?uvY`!UfM;C|smTVal#}h_Gi4|3ohW&8 zj<%ro7cx%kS>@!bQJ@0X9`TO*+B0dQ-GE!F=?y+t0%LWn3%vFdtMYDrTnLBC=zvD6 z9&XW2tKe1RkN!H^JmbvvSNw#Vp7_m}$d8{bHHtfrdcmd>1IwWxBcz3M0J@O=ifdAJ z*Lx(ZtLHnt_didLrZy<*#|M&C-VqyQtU}>3p(so7W9N(a7fR*?3$saC`=Zk;8feN< zP*@VmVA!~_bqF`Uq<*8gx~3{Da({p+PVu9$dB(7T?EQwvnIh3mTqpJ*_iQOA%cQs8 z1;MH^+T1)0r|?4U{w7_P8}{{vtnKoPhiZ0&b01G+IJx<^QS*w33;ey6=RH(bNY<*K z=XS~M2&)f6xG*^yPkHUVkKXl$9%)}xEK7fAsK^neH`HztOM@K~vTLs*J%L9AQj z5N(g)&+FD3xR5HZ^!Bcx&W7{}@*|e!A)W0Z(2Yp5=78*!3M8dd$E%!&2G21&BlW^( zRdxA(*mHBOq5qY~j+atyiG5^$6qos|0r}iyeJ1%QTIH*xX@l*X9Wg0egN8pUdiX#2 z!&;AAFVtH)6nX+r3M$D~IjYK?t=sH-^_3pkMqyB7U#!E?sdfYWc$Bi|^z=G@n)CAu z(UW;97|k6;+>>#emwm52>6Urh{NbOAmR){1V%>|cv+09bT>*YkQ>P8<@Rv}t(5@$m zTn?kB4Wz}oSzJLy<>K&BoY~xqhp6}Jg*PldYGjyoyJPi>MR-6?ssv1fr=I*= z#qh`(=qM|3+#HTDaIQ_x?tld)({s3JN6zbSjVbDv%)~_NadC|N+z%9eY5E%1Xm5-@ z9WfDMj!V8Bk(n&+Zju6}sdcYo?bU-;1-BQKw(0rkmHV$>gf5VjAkG_D>n zVWo-J7FqMV#J*?iqmuEaYpK#06*x%gPgr z4@|Cge|?0l<9-6XfRm^tJ7QoO8j+We{zz<{oyce1d$l0cKM-)Q5tn6hfLw>nd zwS;~Cl7G}~)D9}LoLuo;6uYQt&5J9jPTOt{=ZPk#IBJTzuM5qu@@Nzt;AnJ# zu_L4xS{eSS_f?&=-cFcFM$32vP<3nwwVxJx#25Vw=6ljct@~%!vA31pS1#Y|i4WNlO@6L|HZrzO1K9>x=XWgbL{|aHDBWQC^NsBoUxQ zAYXC86!y<5wEy)JmPuN#r!K$x93`b#xEZ!bqv^p+>+|76CTQxO{5at*TZHrzdcnJj z%P_F&2OboM^7{iwg3y-h^M5$S#QeNa78aY{B^rlV8cs}!@dwg*xCE|sJkct zS6UIHPKeGjY(Cw<1_|~k7UV{@L}Aqzg7@KuPwx7@&vz8nytd4iZI(TMr^H*b+i?;l zWm&SOxCFz|wH6X80%MnFF@0@_G=t=0lscmY@tB&oWh6pI=YPkxSwuOQkGe{#T$3+d z@-?q}yhDvTuBe()LLN0SFTpF!-P0-iSv~CVkF0JqB1BPPX0(T15E(Oietv}^+wjEN zV1bjS?|yy3(IRU7+N4%COB8#6{sj_>N;$JACzyFYn;W@TUSRD=_zEYU1kCYbSPkK{t6>tuHLph{{I!KviY+VZ$E|m8Z?f565!-AbemLkrW37AOidc zGc>8=y_9t&H!jOU*;r{pg_>7r5SrbEutYk?HlbDYijqR0hK6Syh~~u2<_(*oz;-<3 zWg20Z&!LL3jaqWuBd8U28C89Ro$Ei~naE@iuxdy_xX7S9KGLC(nj6WyDqNe4>HLFD zH;7X5)%K_c-Ua$xvj%zwMZMMLD-GQ8iGR7rBUt&K2GSj?&qJj#_8#~kDF@PLfEr&H zJ;i)r?R)c7yN&sBu{YzTGHP~~D+)aJbqT~ z1S_Z^+gah^lQvrQK^CF#E*U6)dxr&{LYD8nT^5q z=;vAPzc{0fUL-roqK}d}8i2n)FwDGI9V_*Han(119#CNH%;S%{_(|{1-?YliM`Zh@ za6$p@7Z#RH-p`Y+%h7>o~e2SEy9U;kye9;w+91LP7 zd|PZ1>pa}7Zp^>R0mq|ButQ8aE4qZYzhTcnan=T8Apcvp@=NldF`w{Tz;w zlP;}GLBihZ^-mWA9XhXG`V-WG_##_+z+l^E%)Y+2bjBH~*&1&Vf+OZM8QgYuJ$X!_ zHWyk>o0rm;?tMXVF^9tQz^h4KCeLxIyI}Fz8GxiXqe-Tt`ne3i^&dqf(dV?uGasW| z*0k$O>2;%P^m~5^5Gri?Fk{EDn2V55IVHTrQ1XO z6_lo=_hFG{vdmfXEaKwct-iOf$Ge^5GZoVm{?QA^>AnctpP~UJaJbM?+OuLTddCet zc52nh?NCnQSrlD{4&8_`35L*Ort3j|knd`N>*Zr^51E^zT={IGW0izTStAa9N;0PiDs!_raB{ zPFukeV|kv|wLzK-;#um;plMXJn{s==u4eu`7zKO+~sny zdUR(C)pvjHz3qR>?ONiuZjtKGRl#$df>4vPhipFebIS*B7_GeX9enB%G-!aS{luG@ z9gV~25;-i#mV>XRv6&BRc-`9hl-X|lhqMQ6?-_OjXsog$vaMw< zN8Z|%<4>9^IlqQgRIF*)w}0?7KUa&WdMqyc@aQ`Cj8(BF=my6o!(GZ)qVUY+T5K0j3G+aK@icEd=03Fd zatD1|@m=5~HDSp7VhnHqgFAc-&w`o9G*^CE?TZU|iE9 zi`oygyJ&Mrg>$OaA z=lYYUgkUx`qe)0lIXg_V&0d$BK1}!4ad-CBp4=jku#Sg#ZOyiJF2B_71Uh?fw~VA- zCE$v^HiEe!W|#u~;jsdbb6IJ&SuxN>R|nH_BWYF1C!bsN2)4r2cs!U8g&EEPa~4(x zSBw~f)-K0-n&?|c4hD2O+j3UrT%7eWIepf@hqdUaHV62$a8vp}InToaG2rMY^?uie z`T!EEX{W^tY`k#hWQ|Xjvu~S(P&2m$l>A+TP1Q^Tqg2NxG{vy;ETvaZd^_^xzNUbW z%q4H)m>pi2f7Q(4_)v2?$4HF%f8Vq{Hom8L||M-to6Q^um4(&pN z`7c57eRYf16(h2#&RdPNH0&Jxx&C^n5)X@=UQW1x_bSf}G5n-o3YY2RHN{v8YHa0{ zE(d0JP2Ip~3(cCqNsu=GM#vIdZefO!_Rru?@xBNd73PCw(j#>GhC6>;J|2BC_l9ig z+ftt^y@AgK4kST-7aDUC0>==}n>t6V+iuqNHB(>3P~?XDaiTnpdFJqsNY#vnx;(i<_){^e zL9E?hqNf|o&f%yXU2Z9be`@*{8bpFW#)$6Mv=Whd-uMmcdhNfE+nRsrW5IcjP0Krn z1B=Vdl!9{Hu(&hL!XCv@if|Ss-yuRrCyTD!y`?ivP}ASfH{w7SfPKc9_Z*NC-T^|H z-Y|>KY^CwId=}VH6dwycxFZPXXdel!GUp=%!RUt~Pu{f;j+1_$_1HTH{@bh|jCL0O znC4Z^az!-P+!>dO&rA-M_Hl|PnHe{pwHt*#8P42WgnksB;PSRPA&`KqVw3FH{&ysH7-Z}G{4gQ{k^9~~*n;!pdNh;Lc3%Q4l| zmo{zp#)B((pjw0cnb^;e7EhV14zaq}tkDvlk|;R*(APm1R0^0PQKKtcx7>|jKM9Jh z*h$IJrx;334c{S55Jf}l4Py%kprb-V6GfT!s8r%p_^LD_loc@7uSAP%PKl#qQsRm+ zwf(t%uJZ0_vBslZ7q0^QspBOs)83;VHQ;svZGN`uhtAk0>n);2b%XveD=}2FIy9$$ zT1*Lu2O!5)ahvPpMfmjfb~|>$^6vs&q`ac&U0lRf685PA?hn{=I}+&#dae60hEED} ziG24^Jda7-Tm-G(BZE9EH!0ecIlaws7@hDOVd-vM*8B43}Z-VCRz|ktR98I%l zvp*OG3F|-Fwx9I8NGSI2MGrg(c!mh*aRRX|46RRbB|3S4<>)%`-1N zmNB{Mo8Xm2tBCX2+#9q=-(7=Bzq9r15tL=a5*2Y~oX8biO8>-A3h=H}nRG~@QtkQ4 z4a!YAXDHm`IcQwI2ZE~M{_6EZlA8h1`Ua~QShavC(D4za!||hZ;RGvuDl!VvBButo zp=&~JnjgxMFE*JqUHH}C`rdr_vKGbGXrKvq-ktl%;J{hNbxZ;WAJcRFxoj&MG90h) z>M{5kH$he2bYcqHpy&_x*buryBii>F6Rbd>NuWg9GT0BTGl5(`=K5c)@{dj_c%F(Q zh6VP>FHplq5?w;+KZzV?dk7&q9kP)gF!J_pwP+%Uzs%Va;~wpqA4o942yn>b?6t>+ zYW9_{T_z~nuU>4PD~;>W2tRG(Xj0GmDpls#G8P_q_F6v2SI6IY6{lPL=BJmOOoMb9 z!Dc8Wq*6bebDoRKnszP_>2x_Jmfc|@wK>rnuer3$o6?O96EJSC-+Z%_LRtGK)`$sX zI_a#rN!w}@smR5hrxcK3KNpTQ#{)a6aTEL)I45v29#5vLCK-mo$yo`FLUm9bLu1e* zIU%3YuND^@ny_7O#H-(ej_Iz~K;n^{GY!M|cORVVx>z@Ojo<$5`EEw#N-ViKvJ(8~ zdkY-bcP1DUU+tv$g3wA|sK|4^_ZA*9pRdn%|E_eVx=P0vv-oIvvlff!2rGZ8{UBaU zcY~>AjB<9E^5p4#Cn4v4c*TY0YTylgozD`b3t=~63$_1vH^_sedi#Q$v^S;6cR?7J zS2rZV(-+eSzk8cfj>T4CqsJ?#`%B%Ak>9T*gjaI8R%;SEoLD}Km=zJwYG>h#mYm?L zk6H1;5WTeUUlZ68>90!1a)(*M1hMP_>O4iq94&PbjA73xZ+;jueWp}DWa<#GMmW_S z1&nm8(5{M~)V<_Pqx|U-g6^DHBe-TBST$fK?F^$0fm*Rr8w63SVI7zxNL^uhTpX3h zBd8#w{ZsE4|7;J6Y?ly1YM;;kuK7T&dkS^-J5=XJ!xUL;AWoYz zRiwPuD%z#zR{X_^9w;h92#-L?6M-wgTUEa^BcQk`zc5K;^VK(f7IklR!1-wR<%r-7D~+e;Ii{?v z6VbkdpYw~qA4-!bIM#UV$C-Na#!RH%XRbV(mz5CHY8jW)l`7^&zI=|4` z?1kg7--?Wa91mqSA7?v$Cba1zTVCHA<+~!DZUf2rLt{k~iFl4S8vG??yf58l{rm;$ zp%HGG>;$%H;NlU?nCkw`(;T=X>@}sj_pb| zuT*Gkdc@8({d8LW3#eFSd&m)Ex&kRLj_mQ$MTx!9AD09hT$TlEIHUU{FRAYqS6n^{ z*_n$S|BQ*fdl=ZRa3Xiq8MC$&r6$90*Z{l#+<_HSDKq((=yG`FYV|Ywo;vNo za@G<(_=Be}PODUh$PB3QU!&xQIkx_9{8hn{Dk3c=s&tz*wd(v}(8-eK70=$*UVPNf zX%Y8ivh6opjJZG9m03#)R2n>2XyTW{y@Y>{8-m}(*wy{73t%TeIH{*@XI?lqYwZAO z<9T$>_a-3+ls`IAd|`jO^ts&gN<9HXh|7lKiYAr~;p*ZvszUAX&RHPT<9G?Y`$NK} zViE;xkxm17PFEWIziJTKEuZ)K<^Lp_rk!SPzsI~1zI1hsmCim<*Ck%k6XUq}dd~_f zC%fWbKNdrG9N0L(_OtpUX`@LMM>&5v2NVb;4lpHijfn>D_lWm!coqx#8ZFDQqf?+uFok)PF?*K z8dpzmh|SiZOS-$u&pNTUhYx6cGCB@8yv95)qPxk42_!EP2{(q`AcWE~^ll_Ok`SdA z(SekHQYKHl|7x_&by?Zv8aRr9GCp`76^PGhePt#~zVKP?r@RrarTj493f*F*l> z#_62nk(AM!cm%fq{i7ukA;V`YUrjoKLnEXp@Xaprin!?$_7Lu8t}WMIlI8+Sxc%ZQ z0R(`rb8==UexGXm&soAvE9c_zPD8d%wIACc(p40;RetP-5>{Af@m&-g2|3y#Tl94h zSZAA?nOL8e^p)%n7k|7q>LvRUw5y!Tra*ZAo*sj^g#kari(SeQZ{^Q$i+8KI&yjde z>#$PJP}izC5u@I@n?Y;anoRaDFRv_-?6re+@K-5Ch-C4`Dhls--;&LX;#9X9l#oJc zkhr%|4_+L}6eSVr=bqXBwe?{p&O`GyKs;;}NL~@;5wnp(iZ_=1vb?%n^Y%SG6Hl-N z|LGU?7C+())8D>y^J2ZupIf&qyjzR|_5Nh`Gd|Qsxd0ikmwOO#6|A)$RTJT_hd0Yx z_S;A>&rhSRa*`O<3Ig?Ls zF|ovrVdqW|7;*Vk+?el0ntAtOEOOm`rPu4cy0t=7XT<(D{YcM_%rmJCcSb*SyS@zZ zMAR*nGx1Dumd7Vr*WIU|$Kpa7hlpjhR$q9;be|*KbJgQRJc%6Cn`|Ql-|oq+0V6iy z?en(F>mU)XeyLOsK{kSp#pfmMR@0F2ATpkb)x-*<%pGJy-boC6E*j5M(|~{ty=qq? zCretEBK5a?tS=uf&Gz_Eu9Hp-yM7*RP0`U6?*_hF221Q+&?$0d@k`S5>cCfEP6(w?pYA~{dk~G~&9LeF z$meV49cdYR!E(g z58Rm6wxKD+VO18X&GCl8?jl+Sb7cma5#%|E;ph4CYRP6>xKGm#8AX9FOA8Bmg7(jl73#d!zvxUAx#98}MrW|K6-6wm8$H{-AIfT%D6QHd|L5`NZD2;tmB{CwyEi^R zLAI9$^Da?Mzop14>`hY%WYBqOKFJ7%`9CHPf8^&MA^LMP1+>8QQ7c~gP415+A0CbM zWL?a6(*8@zx1TBSs42&HsHxs7Rw%_Vp$G z-=6|+ddY@{+YvG2Q2puu{U@x|v!10dRoLXi_0w)Xtj76OM@^3QAN;a8y)DLxc8HMk z;^!Q#7V5FoL<(7Y%OEGRlF)v#H5!xvTy62nQnlF-^SUc|u*h z-fyKE``SFT+^k|qkeFOztt`5ZN(fqy<8lC&VHAaSBHXp;lFh%?jpBcc5ly@ z8eB(2dMUU*k%Q>(Bi{y#Bi1SSzmgv-r*Kv&bN}hi_BorWJ$ouxQ5~bW1d{S?$@06q ztTHY#Vy~3GxdmD$DyNOSx$GAJO`qsB(0%fBH2&`H`k(;_Bj>4lIkFD)4k%~Tm+oYg zi(if4@wAw<11%$~1m8PrrJSFT@fdo|xZyARo=)lXp5^a-*y;A1&9Ob$DhAZ+f?f1+@I2i@MMx-WVeg7zyIDnjm|8907| zHh)>tzTHf+kd4k$Cp0OpWrl<340Y5tzVUB7RNYC6wSt6Wr+@_3-kgfzFnRkq8>8Yl8C%H zE?OaXYs+=b_A_0Q;GqYrWRNtTEqWp8nt@13q3^#s>dseVrC`DZn#5=%53hg-S!tp` zo}Lu5_ZeLd6c=1AbgN-w;Qh+j552P z$hors0#KLwBr60^k4G{`vxS_+O?aR?qn^8mh+7=OUwb>wz^UOi) z;G2bn5};MeF4d1}Tn0H(9(BPI)g?t@Gmu#-JM~{LlVrq&y^NI9`m4i~jZVZGxMmKN z)ZeD+RMV;}ixfz0<_7H|f`~?%Lq4n~@HmUam?*H&`Q4!YRa*|WNbWfQr1S~$UAI>d zMXzseb$a_RQTUhp@$pFfLAI2arotDef4(sX1;}QBUhTiGgrl#KU{u!3lde#r24Lb4 zDq{9qIc6dry#(K`;NCL-cUseX3rxOA&w`;?7-&Trt+!`$sAFKvtsWA^SF+qAEWgkO zRRhtu|BSwFvC;@T<&s#vJv?bS8sHeet~Be308{#wl+{OI#Qt$b&OY=l$`heuyzC|m zdWAi>b_!&m>jPKN%K1Ie&X#K1MgNVw~ zq=LwV7$1ioY*LFxgcGEL`G~%GvDLe``4%wy8l7-D6O!~kia9xGyLK4L5$^{%GFu8r zGx4TXWD;<>CR>%e5FkYVcCV9*DQpt4xWtG+bRP>H0-xAt@O_Cq=5>XMfQ zKDVdSRg!?~w1y6l-)RGlc`Y(VU$PV21JUOTgc2uX&nbRNX}-20nvE7G)>QFZa2e#vZ#Z60HLjmf z6XSw0O5P#?-Zo+gMdh3=BiV9?^tAkpKlfG0f!Zo(OME>E^DxJ=cL%OhMMnmo z7&L+G1xjNG-z@rpefV!(#{^ngv9y0z!@b^;#Uoz zm1-i%6o*Laz+?Sft-Xh$+|W*$AhX!dxhC|iSD7dWaDaESseZSJrV*-NjS=I%6KuRd ziC1RPaNV%++9%&N0n3Fv3MOKtv~eQ!jc&;FmHlijoPNpg-ls+FY{n`_SIF*SEm7Uf zxJYW;sw%ubL)^<{xB2>@-H&r;SZFD#cPpb|-bHyR!#31cX(?CrQiWzr=K+e^uy`nnSJgLMJ^BgLv8#b%WUqTcY^E*UF2u*MOni+_Cjk zyL39;Dx2C7tYM7xpV(iLHrZby^6+(H+41!_b6- zaIiPR0`(nRUlZ0+y@K3bB7p^+VdrmQB%SLJpEDpdAgPl14t(&@5JI{y7(u!$BdS6R zN3RbAoD(fAa??_UZf- zQeY(nN^6@g9NTCXL0g|d8Ki`bD0STi*mK%VXS=(b%keV{0W%IurX#9KCS7VR^(@iX zgm=Gi>RB;@EU@)Thi#^Qf24iPkF~QB$rDo_&yMkWSMeLeWaW8DMcY-dlhVeZ)!)uj zAVxdNEHZ$}-hHob8iq!Hs@UE2)RFVY6Ah>{5m3XQ4M77V5vK+ARJ3MdLFTjA%Yi}x zX9f0$zZq-O96}B+fhi(iqClop;3Al8uUl-rBk~ykQxzQ)_9*-84w$4@8K$Oh;&B8! z$h}Sbv;?oHPYd?$fHqIq$yz^x{s0GK4(NgUCyFKWrkQveXJ9hDg~~!;@8DN)c0`cx zO65L53Or>^?iuD4a6i&dPch2=?4|RZk8ju@(-3@qQVamLJ!ge}RU4v)Q6FGKXGrnyV_ax_XR>-!9x*^KkooW_HX?rBvg0r?fzZEEAMp+EbFn}4NCuVX9MzRSJ;aIS@t_wy%mhbkD=N=*y2Gr|Yl zzStFJ^U5Dq8GZLv%p&eiQp#bKS~;YfyHH(Mz!b8nOr}V1b;d7%_h^fGFlo(d%>rOh)#si#I#4)I_P1d6QnVeEHRe7l9ohh^YeZqv% zgV_GAxSN_kMqiZ1{*A9oO-_WHJy>qo472%=si^g^Uv=JQa(qjs zhDK*uh>c0^i6S91Y6y_QWKJN_2D?yI@|nP{r4dwKg5dTMzNa1Seztt&8mEcjUfeVxdQ#EDGY&qD1-LD&%V5 z?3(4yy*ReKL%G;qCxlUJIRgzX|3QgM>jMG^k8HE4U_B^GNNtyi{E4; z*3E)pO(pPCagxrtn`E5gfHEoC#~6Z=7mqnCG$y8ExL*a`hutPsCk90O>I%r`J1Fx-}&S{_S7@NzI8HE_9iVAk`p#qr!GWuW1=RnY)TSD_R$EiwNI#uW(7`Hi26 zNi5|PLIa$Bg#8d-`P1^(0|j&>JjIzV@8)a6j>A~QEgZ;tpFCOHn@;ErVickvhSuU_ zbA7{Fl-`y5Iqm?De5{a*__a=j0;4W#5w8o>ndotyik6)1P1uK)I|o|cxZSNg z<;_3fvRd&x?dEov^2Yf@h%G?Ia~pG~E(P0xzc9eTEGIcCHk$8SX_&A_3WY4G`szHV zq%pztyrMzRSt9Y>A@+T&=qn(sc+Yr9qb&M&OcMUKJU+GcQU8N z#1P&_Lbe0?@5TFE4xQA$@KJ^gH9a-9>4L`b;9E)7NA^9VrvQrWw;J}$nquya1p^(u9;VipxiPgQxo6^8{iD3* zPU_N=2*x5h#L%P&=CehdqjNMaceM!IoPTfcAPR7=Y$ck@UBNMjNnGzu5Iu;8`LZkH ztcL;)rtZH<>r|!(yUgnc;=}}q|G?nO#D`dYVmJB@>;-w$Zq0Mm@0sajBLgIioVO$C zYU!9*sh^YOMS`U_$F#$+iguk_$Fbr5lxdHI1{5w__85QmwdX!!H-f_m2<>LR?v$C6 z9_2fZarY3cDKD+CkxUTUcyIUMIX?C2z7~&YV zu1LQ#^Tx(lxJf4?LpV%uI|bj#pUcLvaXAEB)3ItsgBB|s&=zRl(l{dnk^wL4t%~-r z8DXw8Dn@@zq@0jLm6C>yo;Y+1+k}lqlcMjbX0QlN1Q2DMjT!$8#rexPa(F=!xFjD0{fIZ+`I!|00(DT;L-*;5~uTl((<3kLm%FQ6Zkd5Kr{`7pSM zARD^B8BkCxBuW&ahj4z3|CZQ$K0twm|H8!EtB9exJVr+R$MY8h&pF&5CDuaiQJ&bc z;P$kT9xZ-s6F4%67IunF*~NU&70EPgW^F$tzHO3=>#v$*sf86^w-x<0MF1?Km+bc(u=hIGc(D?L>+ zYHMfTlQv;kOv?_p3Xr+)K-eo$3Z-+OLPW1WtQHrcj=|iB@ZQIcWb}%dyqdn6b8)`t zo;hlLR^5_&{wDrP{0y9FDgC)LYARiF-w<0&+*>3EDjaJ9Xcf!`H)6dPQ5X4!TMY&R za)Ia6+D^z><}u&?4w3e=zo!Ro^TGHovtUq(gwy=}lS9g-v6!=QvnO9(@k z$WWpP(hX9g#Lx|cqyhqx)*u4XNH-D+NOyNhBki;K$M3$y`~C8+^?ZBQQr5T-u6^x1 z&*M1GV_Q2*n==dvOmbKIBa~ZC5PHKg^ydHHZi%+l5d#z@Cd-_)>!sn4RlRq$EF z8d9^{#%X6fgKr;Ae^AU6uuM6Si8RAMi?Mx;Opw%M7R9UHAn!rm;|eNI6B(!6KCEuK zLdLYVdYA{}6~!&M-9ECmWEgj^19uzR^F0 z;WWKBzhWjN5t7ejmNa#Lm`3CpfCcbUM5s6n#hEDnlM43&Mx z&%?6g^V6~3!At+qLWB-IB9nyzln+Z*_I=J zW~+XvdmDbu9+~|+(*CSlSa7=2xi?C1MvwUyh0^z!-L7H#q+$wnCp4?hVM4+ZukeY9 zc=-il1}3F21gn@Hvol0auPB^OYnf|y#os4uKiD@vC$$cyfRX1%JCV+&)1u{Oao_ut zJ5Sp^RMB86jPdvB@2t_?o(}a}<{mc*keF1#I+_XR{-_q`<1VkmP~)67H(_~v{isl% zcSsUZ5iaW?m6sUY{7b}LR<_c#DYT_D1i{u)tUQgz_Zc~1`d+~$UoS!O%<*MC9oem_ zj;kM8`V5lqb>9DKk@=Z`vsn@|hUaMdY(f0jv`?H=J-sBl?itfn@+j1URUOT(V^+-- zhJGf~yO|_MhQSca!uGhKl-&{{XYH|228vUVY$l&>UE4`%bP}oqYEyXA5w3k;-4I_L zGP|`f$ZbdbL;BEvLm5soEZpDMdC4Mf$R?lmEb-pzi>6I-9!i$@Zhz32NzX@wjb+5E zL~ForVqQbR44oAaGbfM8@<$ST)B0~QnTFHgzNwGr`)MXr05e)OVe|O@?HaFpMBP~s*c>b;L1S-bUU%I;Q^Bi;eO()81t z!ak4Fv)!_MIizM=9Ww0|s9dv{k|)3IiFK%wj?a7*=WeE6(@8G7Be^Xy^8ltX8rdMG zO#z`HE`sFfJ(<_RcO)C*I3y+Ez`cs^KEH+l1 zZ~BJq2Kz{m+p{ogb%VWiM&@@zP}Gk=X}huEQsEJITcHeQL{-mJxT?<#k^HLCqtSRf z!o_$@(RGQX)MsYK=AZbsZ!d`|j9U$x<2zTh%Stg1g-AP-ujO7}V07z6$9HcMGujYU zd-juz_3bNm?A4;e5;wmipe()_%hJR3PSk3yGI34vn2$qcOFs zeL?L(=cq&L=Q&@rdPzr7$32t7^};WHj=p0&tT-D_lF?-AjoVQ2z4C7a8K#+z|H_89j_)*V^Uvl5?(5Xq@?9Me8k*!lo?Ppf=DytZ=24Lk`3vU3&Z4Ik>vll?ds3iq@la(Ofod6ai0w;zY)8p$1P71zMvO9&(Otz2$^;Szt z3N%JHlS~-`#OA|0`@`!!aHuzwp}b}T*sf;vn;!fH>?CsZG2CbI48#N$M*FAeumCZdw zi`c4!z5Pft$u!EmN!L135>!6CD$(DV+zKtYO~x?1MSzPod%9uzOfZ((Z65!b2quga z7F8y9jU(s_CPTp!lD>0f4Ju4IS;lt{_wed6HUUjDF)ZB8`W@8K4SFlQyR?P=q5Urm zAq65>GFNx=2huEDvfE}T20{2awnVb<^mki|j(Q#VX97#3RBN8MPM@%ZmI7L%s(sAq zXJ?tdvaied_eJ|+hfcwh1QaPx$a@g@%5Yd^g@=svJK`PK7AePO#Gcczsf4Hx!Ek*P z1glQ3L?ET$$Krd%^#Uh!dvSsC~I#0p5`+}Byt$7VK~1biuyk2^|50aw5Y zEu9|{3S<=M+WaykK>ykAKO{_MD&c)X?btz~>79BOiStbE_*4vRQM~uL z3xU~W2XxH>P!s;<%m7)!fU3Fm+BPV$2CgmL{W(He9M;n<$5_%B;v<1Ora!)81P9=| zPtnEmi_Oqtl3lTcPI5AAVqinjEzZ%?&K}kU%HBVbLN=CLp=U@0c|P$El(O+mk{2;; z$qpkQ-h!h6M`x*}1y~p#!Q3oode3Z*T2CUw8C1!9GIc?F3!l2q{;J z@5R4O8q{C!+`+E#nM2{$_8I+jq*9rUB%RK^o-5#3hyZ#RZk&>K5Cv9i zG$gd6BLvn%_!czDc0TZYBszOl`6Lzjp3U!=&UJ}ttbJ%qj6Ur!%zInup2qA8X%ADO zA?(yM+U>Tfjm)T(;$ei>&dF?D&K7(5S$~{VAX9(Z6z9&p|FQ@6(0ajG<7k7T1|SK- z#)h45RmqtBn8h~7qi<)Lr%mOTQTT|s(pN&uDR5|N@^z03?>@1ATN8e-w^eL|GnPjY zI>f!p!^Gsfd$drjDMpY3aX)S9d%W2lGk;~h<`$`+J6=-nx6Qq+{6o_a17^>;_pP?% zeK_$MxTdf(wDC)cKaGpbYVj^m#B`^pZxWXsrZcW@C(EXieY&k7ZZFF@U3iD%ygD~< z^f{(@0dBr^q48gqM1qCtS}^iXZvNqIYN4Y9W%hYCT2GczViG1u3&qZ?~>)! zn0k-(U$Rn5ar?iy1~q;^jb1?_QhjXd2?LLZe<)6Qoby^nk zIyN@g_ayzeM_SVNu~d6`ylTx$AHGu3VMXIN8ds;rzV32J)1;(wAPrC5qr`lpUE-5Z zeLC~&Vrf?HoxqN^Q<`o|Q6RG>+lxLD4PiUXI@fex?|V^~k@dO1r>^z;ehdMie8C?J zOiT}q;!Mam4&F$vqUTOK$DH9)JZfTbx|5y_+|r1kXO-%U*cNGp25 z)(#2iIyo9~)|3*wlx*um+)MSp3F93Oe|$|Wem|*&r=ggl+RTufQW&j7{}6<8bTx*wPAOIObu&{TKqE;h0= z=s^DG_d`_)Y_8b6x~0%d4AT9Y9oOrr@&p!R(-OADeqB~$-{cXu6B}#U%Df%!d5ows zz_60f*I4yWSl-4%x0iVS0mQ(P7`}y>hdF-honTCZe^oV(j0(5@3kN>i385EK3}{`Q zc#+(XLwDGZ!*~9(d@lI6*{eUEVMM5^ul!Gg{!NN!20~&na!p9spIxTUGs|(V{eM65 z|9+2s@@NEf;ED3L_V)IZT(bW}bpKKY{QEOihD^eZ2RAh^c0#5%sp>pG1^j2>{CoNQ z>nnxKn(dX3eLt!$f9w8JfI%fx2RxAc4=DA|q<8|pp)R+1`FWb{g!6fr-_f3*i}yvG zTCSuNdspy0s_9tF)pd+`m{&CJx|aKB;L~8Hm71MVRkZN<)+AsXY;ny+2IR{ASBw7D z5E(Z}8$D)Hm2cWvfz6AkB%yC#0k@(}&G&rP$${Kwt&#KtO72{F`GO!d@*ons z!6H%;`cZ1Nlo6dyecdd)sM(73#+!D3&D&GL_$my83b#BTVY|>8A=jcPqDH5Y*TOHZ zV8s|dMm`|Q@As!RUCOYj%l-T@7pFrJql?t2D(h=ZAz7As;e(0_t-S|oa|_%zT$)Yxw;CnH z%)0Cur2T3)2Hw15>U;M^IPXE##}TgDzee*0mL)HSa%Y+YnXgb|t#LW@N?oJp>2o=z z7?z{YhKY9h-qH=VB$jEVmVRpk?`^e;v<&`g&wZRLqEmwM55CU1&n0GzyjLWeJBGvE zKG3ygxcXC2fB&*pci1eIB9(V5LVNP^^oSk6>G+O)I2jYSe*VIEtYf^?`Bzub`#{kr zJ)aWXT8HGD9yPg^sg503>Q#4K{Ws+C&)0xls3tpeeYX{&7+;AbW;SL*@*CSc4y4?B zgTBgro}WR)OUWZC^Fnm=$9QKN5XM(xobn>-s{Q7-ptVoGogI0jy07Ov9IIgq9ecl( zD*Ioi?4Rj9oJtV?3B#b$s+5FTsADnD;KhNS?*iuQu-4u2`Y`2~Tj{bN|JNe>_j{%g z1>B?Z=TkfFs!f*2`O$sA&ApsU$SCE7uGwh{F1m4V;jbB9LBT1Ec_=@;vfHUL6=s|9 zK}R8cx{z{vfQ5ZDMwL2|ljS<~KOOMz4?qo(dB1<8a(^wT{I$vM!@L3ne7feA2@J6m zir-tutZ;)Y`r$t>@}Ca@9qO6)ai-@B8akGQvcg>bKD_k#IvT8IDwQTMfaXh};WyVz zc=6|ikr}+oNfmO?zL#D0WTE4#$L}rJ=Z;RblSjq8Lt@6g{_anWwy=D~eLPi)?|ELH zCvBcsbbjQgy<15kU?s%so(kt{w;qc+(FGgbXxg87^D}3?DrDC|mOOcVRqv%{%ctSQ zV)PiRVWnQ)W4czYt9|i0e~zLIj^JJB-1D%N453#%rJt^oTUD$LjZ89$k2y*INN&G?nY$wX^M&nZ6Pkxdtw zY?dGG3f{o5?n<@&GU=Hk>^MHmTfHThV_4<(qBBL5=`raqq>Yy$`Az8|~7=Rod($UNie&=fe+T-eio- zw-Dvol0PtH5WdoEy`aZWmWc=lU z>1a;ip9AYp5Fad~ggd=cYS-w}alIi+2kvB0`Mf}jXi~v%pd;JrTTtffFJ_jJMqeI9 zxwJobA+}4d>_7`8UwIHXA#68TLNEMSx76%24`NZP#&aK$jQ0Lx!O7r=Xk*c(VO7Ei z>MiYb5^b=P`rQs~MSqlfbg#~NH9>P94d&U|4dz8OlDxV9QsyZ-z|61ns+!^12ddl< zMj@-nn*BljvRg+b+nwJv@n%YI+~{a-6WL!I>zVKz6_(hr8uHuDj!G8IQiutxeN4qlz z=SD=F+JD`PV&*RN9DSruClWX#K(6evW@^6F-OlInCT1yk3DauS?TR%I3oN?cMO`Gs zRJmK&w3Ag^%I94oE$eBMM>G#%$_n|``So2BNvQMIwU=Rktt84<;Hf52(;sqb8?+zl zVvA747|P_}_cX07#hUkt5WHV3T?>xV{}fUhR-wgt#+bF0v2~gdx}h8E?TwvMf77UX znEsZHiE9(P2=V}_(0qEdyBoRR4KzVft>k_v$zW)JYtX!wG*Y%*UiR{ictsWZ4y(Ye zbX7+Jz5|WZ=7LB`=XIaxVHBCF#yygj)b-IDu87+t_%MstQUCMNz}=m#<0w@QjcQj4 z4)sW~U~u>1#GGRW1v>(j@&$^8Ps(WHKIxHl2JX&|e*5HLedM5#h_5+216;Ez#Ehd+ zqf6E%zW_vfPF`SP&P8Y1!}@?;F^XtF^smL{f6*-yTe+2ku4^IIn7md$%`sc3&+nhn z3+HJEDVau1bsqskoI1A3U#MhnxWlJB678EF>L$+Efu#;LpxO&hhzf~kk|yr@wt(UG z)AtwRp|Yn7ch~x|V$!^RRum@XeVH7^BcZnt9e)y(<~pe7{K+OuJ#F2|??m5#LdwEEND=J%)vl(^rronzLw;!|ig*i3cqANa zwvVo_v1$65>fW>m&h_*<$0$MWk4fhvHD15q#qOiFS=HUyEV`RZcIPjblFpOA2Tdsa zk-=W<;UH}jW#S#tD;9h6XzScZ%=PZ@*|SI9aQVl~+|P!TOm!s@ zC(UcRFY8Iuco_P>@J7)mTx0k^bvazg<=EQoxsk$Ek`;wlWDW}} zPk->Dl4yz#UJDG^ZxY&fbL`_?h^&47FKFO@qAi&PHq9GY1rO)YgI_!%rYoB2UeyG& zsuB~Ve0L%2)A_p_iYPd#Fm-tlJ@EIc?l}}S{>k1qOcr0yacml^Nt8ePRc)v!lvwrj zuMl44k~mHfBo41&C9!~=Fk+jBy8Mob4=7(?oe}t+sEB4~&L1z{FP`(-FKnth zKuxwG7W1nwg%t{!Fmg@`OacOC5@&<%Nz(~K9xwZK(XuB49b!zyC9HXbv-!5VYwmY5 zYDM_hkF)=NZSDX;3V)2xSB*~G*6laeL<^K#qlcRt0n*YA}|1@1Da~wDGi$c?DVTaa1hdI0U)dAB- z%U4s1%@p^pbQu*RHoy3W*1GZ2%#>L#45sdRpAy*pZ(r&e>U!v`NX{Oc%FfcjzS zv0=oq&o->-z02OWPlbQ#zC7KbSl3xK*?|;1Jm4<(y|=JITABLhT;Jf%0hy(R zvG37>(#VT+bJXMjsE!Q~)FPKr2APCm|_KIgC#i_P%e0*G23RY&@ zhjwegrO^d}hPmf@%}gl6dXjl`VE{J%A`>W}cHZceKdcLWWZZU zEPjDa^>apsZfdOMer=7?80qt2prH;+Y6WqyQ)i0k7ZAAcIi#-*<`s_=TaOiYOW9o% zvAna9INR%cmuW_C6GqDP)&|zy|Kpu9nZ)jQp$#DO6b)v1BO8Plih|A0&(j|5-r$5E zP6rKEy4qFEzf?6d+Fk1I78S?ZZ0E=*0l{;B?eQwN-FW%GiZ?`DmVYGqfAMR#97Fgd@T?sG^VI%&ATo&C-=7A$lbkj46l4l8@B{ok#4$ zICiz2=o5UtAg&OC6nhfwEcH^LBpqW7V>FwnbS=Iph6yE?t7moD>XqE@*LL>2{+5UC zBZJ*0xyeP@M{$?a#X%wS1xFwcT>%yAx8u60(?!t>eLJU1 zp-Gm$MH_g(406To&W~#QvRN8ihyVP!|tk&nmk6u*--!aN9{(5_m;`I9PtOh@AVw<)eh{G3r3gzhF{G7)rHRRw zN74zgJKpRqa~QMO$vs9eL5#fC3zhfgS<}gdk;T!V0%N(eYW6wzI=CxbJC#%gj%5VO zaZb9X3CYl2vr%i~6~%?G$X+1`EqNwDfs?~*b9he<-ZNSu*Owv$vr>x@qNI=kP(&jZ z73aPq1t@=C)_Zw`Q;RP_Ex2^Jeo@I7d1y+02o7 z2dB1jVMg|6P@_<^mR~33$t%gUiSj`E_fDd!L6y-cXbZ#Gt7lttw38hR-x6EP2_84+ zUrJ77dI&7;W55uPN^Brm>oYTV@dhhqY!xD*t43fVa=Ikp-S*sD<~-2t`&uHcV7NJ1 z^UmhZVuN)v2^Jx1OG#6I{q4aogY&zXD_l7P^5z;g|*SP$?@U!9Tp~| z9ClkjLE_E%{v2GlGxm+Ijxj>!);671kf!-FnrMNLt17n z4e{oy*?W0W%CIdY;pGdY^;#x<)cn?J7l;W>pSF_%``T_B+rS#i6p07%1LbBO^G&a% za2Ny0^4U^BIaeiTZ#O8Cu+eA=C7E;4P`JL(7+*#7egcT=?Joq;+9t_mSAJX6W8Y1g zV|t&)TYEUmscx%3`xHB%G!QALr++7HI`dL+py`4|N98|eU#!NMITv!51X%~49x*lP z_D}cMl_7v(K)8u_wjY1Mt41JyC%r@^$v%0HSFN7Qz((2_uy2DR>`=CWoswrFSw|>w zW3J6UPZLglISRDD{Wl}d(!?){7p}~{ix$7(*-oyC>?Pgw4()s!B%;DFhY#LY_&_G@ic<37BzbeN|7Rn z$)r!WIOA>0V!-fjWc;Rt%@Ajk2UyyTb}6kc`T?{U&{U?A34? z#qY57>W#u*K;bSrc&a5bsiIJT-+b+@Xk1^sq*G1A|BjvO7nP=$UijcWj()<%Kitj7 zA4OgmlC-_=g}_zhC%@7WWE%nwiEtxM9D_=k5d!h4!k5`G2$~JB!$w9Im}*0+?;t;J z1-{@&m)~)jiR0aT%Pem{`^IZ7O7v}kENXV6DM$H?5Cubv!cfRyEV(Xh*9M`l;^kq? zo}7c>D+#%57EM&qZ0y=uJulOXvL&nBJgnXLf)fZ!-M>qS2N^F+_OIpoZ!VVtba*k z8y1}1OrR1h*qB<`tB!gwc{w3?t_~9IC)zD@NAfPL^jj_-chabrTjMr!A@Uf4R1kpx zo0*W0cP0?*D}{2?9&jtNn!ev?{!jvOd z(v?|!N|=Blav?HW5*+0{`eBD}C)ZQMt%*-Sy2eqp+a)pQ&b$`O8yj7on_wQr+9$qE z3CrASV})wmUWl)L-#?-Fbh3ejKxbkF2UXbZ!fZ*Wuu$FR!ymy*{j7-6Y`N%L z>Cw;HyS-bvgZWL4VQ-^O$#GFCIBJ5>LpV8^X&|k0;nLFGleEf7fF~64#&`ZqBY?qm zQGDxf)4`1gQ=+fCo=?jf%v=wqstR+v(q|4)$3Zt-4P&}z8X4Qnuwo)<{Kk?XYIfn>U40n{?mX0gpO znO)&DBMoT6$iV*~be)2XE~T1Z@kMsbygW04)k2$gwi0Dt41rtnidR<>(8GgsUD}sE(j{r)ZS|OK+d#oN zQf3l`)1e#-yWvyAlu`$kA&d`0?%9T=8^e>|`00AJlDjBvTs}Tg>Vgr)!lldERX`&}+eeJ%Z_V6>cRtgoN@jR7Vj6vVpLIj{v!62j z0}+9Y0+t+?Yo6}YC&9gxO{R~_}Cbl4FDEwq%dyL!s64=uh(qS`z#dys@6gvKcDGqGJ-q7YiKMZeFf2O z;b4cy)3RGgypa_OEh21qAWpS69IvTGto0*zOr;44lv%-Ek`Xe1*xieog>)!QgEJMxU_V8uP!aBYTxOyP>=a9oZGzfHR6im< z3;suRlvzYU+o*n$koOWHyg?~~XkUy~*;bZ}fSsda3T{Lm?v`8foO6n_DY(*-1OuXv3YcrPb?dh#+@{C2P9LjQXH%GFqmn})R= zhJQxUpv|G6lzs442=QMCQ}1=4t*-P=xJ2m!jR^@z6LW2^(7`T0|AQ(>`&^E0{FU9e z(Su}Eua8nt;qvqECW7a$>^%5+xnp95LB|xMxhznZ0it+b@ciC|obgMWngY}gi2tqB zwA;x4%!e=`oQB}}AHF@T_~SSSWAp#tz-aQ=-Bb~ z9y0H7!Y7@aN5VM|YbpUG(fT^5oGfIEE%9XCqa$|K2;?9;)~FjgP<1PJ)Nq7|+* z-YbxB<_m2@zQ$`C7WI>!GKZeg=;gttN|DFwS^IxY*A(G^rx5>dfx}+DjYuyxlyEh2 zps})cl}@)ck)qi|ojfLI=yNGRfy!PyvmTL6s~Ryi^wQlZaMb+ktwlfy#^>kkJVrKT z(e#k!k#*?cC*A!|DF+8sr2$DIk0c2iE*Nf#Ii)c`RIW$zoamh#lRST&FnjqeWj?TK zqOqPFuIst|ZZj3}8R>+g*4n;huw8W?8(LfZo0j`D?0O3Fv)j z%8+~XbkU{C2h=URi)=Q&ie>g}^<|5j9cKoU(&cbU!Q|OBO?Ip&^Qj|l-6+$tC;NAE zbnw#aTu4+Zb{p9R+(}vJvx3nP@60c+5zXWaEQf#oWdy_4eO|qq(XZVZ565plPxuLNq2dd_fKq_1 zP;yI#8+j~HRUM4lIUMh;2*jwwv5!1>XlN-PN*q4*j|TCd!{-{I#W=Rubz~p%yPOJ^ z-T7*;Z3f4|Xz%L%9LJcbv#;q#Zn+DNy1zuPp&B-+yT0t6IN4n=1IC@NwO-DejkY)Z zEFuPafwvPNk{)~`j}NAf@r#Ji9&HUtOF)$tZ2(MV5Us{=veiVkQMvZH_`=?xJ^(u+ zHS6j>t@!tc`;He0E%)GO0kr?C{=9M#_`7`fr0Dj_IwzX%5#4L%^2v`Gn7JA%CEYd+ z%AZ;$)m9WVr*uLeCgW9#+w*JdtYJKB^lSpar+(?<7N@{f*=upi10)8d;>%DkfX6wk z<$d`)UZYcb2_NUA4w%h}xz)l$#!%0HCRC2mm&()e=Eh)-O0ig!3nJ-W1Gxvg zcs%JR1tUUvk2rJQGb{J<=@oYj*R%IwZ2=Ua1o$Ix!jZl;8=%s)tcg@&enhomt#NPQ z0@eemf;NEhQkb69W9k5t4+3scf?g0LPKA;%SfMWbWxCwPom=4ziFe}#L1lSW&(Q(FpI-{*dYI$uO+)e7% zrzWZ>8``=Fki;)Uyyf-2ARSVMpnh$FY`>6-TW5HEqz6#*9=FBU^j8fj^X}-rRZtd3-hZ7ze*+$UtMQ=}e@2c7T0IvJQ%zXeA z`SveS!Ud;G->Ax0bL-WEe4`rVPcT)!FgTX8dQ$f#yt%xiQYFer^?pK9Jz{`s+6^U~ z9(l(b<$Ffi;4oBM$Z0x$QW{Q6VIAWl>ZT98;P4NoexHka&XB^lWc17IlV?WQa!CR! z9^~i%dNyW+LIZBf^QZ*1z1W?>qR6IwJHLGP`7-N;oj@Ldcn<(f&9Jn)>nX_8->t=) z4_sa*!8N9m3~*$D>-uodt6;-NG`t41LqxMw7LqXLwhNnz&)m&ym$!7yBLa(l>?9R< z&d}Mlmx9Sl=$(#8m$U5n7i`L-AeCB=6zVESm-PA?WPd!nwx9I5J%TG4h%>_Znn!{O zDc;J+uV0`~J$A}c!jJn4E&$p7Z0)%t;12OVFFHb6y@-ogTNxN~%yaTWk`Sy5w0-I$ z!Y;s;q6n9WdKe({mOT1pk(2e^(!a+qoHk01)-o?mmzL{by0j_rR0~3=!<2={he>WN zzc0bWOrV6@vQh1rusdYkf!m`b2j@MWY%fiDSU>)Vj}D$?vM_}5p~JxCaY1F`1=Eaj zv1-O2-Ax%@p`^jS(q_H4-1GImw%AUZD=Pjny?4<)_c4mt^Of)h7g!a1 zbxZhre`?kRe29}+lS_RhOPSYUIl-*4%k2yOO@I4A6ZV3TC>ymC6O0tH7?7P9e7wJQ z@}|U$)QP2W{k7fa!VABf&voa=Nq8~S@m$(FZlu@!6x;`~O>k2~_o6u|^Be$$v6`l3#j zOboXokdcE@=W0BG8Jk5g*zhrDy;5?C;lS*cQ|TTO#soQ&6iUB|d-yqeiCe`u*cNMp+Y zgTRL#lD-s>fLBt&HL01dc&zuu=uwQJR>4B>3aT%Ww{VDE?bc59lX|zoDwW+s6drTm zk3w0tDo)0=o^sp0vpm9XlA9_ryrraWb6Vf;n@CQ9V}(l8{`M@2Ez)`qEMoWVG}{cs z{@zNu5n2PH2;I38dI6G?JFE|@7_I*ZpKdZ&76a2{?koQVRkTMtn5!pr#Ug{c>i7AH za!-5HEF`GzWMcDek_8jgNNOh+m@-``C$_gK!KLCXZ|hyWE{zXNvjSwEFq5;=A)}PS zN0Ne8?NV$MI*rt_m?n=Gg zNA5@<ShQ2V zXOjB!EJs(}-(OwSbWRJY3+AQ1PFoUJ0JgrDzW?Kj;_BZ)?Dc?1BALP24SKZwU|YRo zrphcq!#uag=kg3aYh8}@^NMG>=DMdU?^}2b|9ZVldqN8X3^OU_`bo9dEh{OO`e|`y*m(dEJ7^Z(piK|)RyaF4T{R45s!jmCN!YEy4~2!*D6x~YE&uTX$WVdeMpyV(5rq5iW^RH~L$ zA07Ca$gD)C^s(FBf*iF8&ks88mVasTMwBx9CycAUY?y?(ZZOyQFKpny_cl^?-L#Yl zS;_uy9pyjoRd5@^>5AEA&I;Psda%}i9bWHaNNKWwiS*&TEgJ9lB+=3B{+D!}>%$-Z z`I-NINTCRLY^kNkJnFmn>a5AL>c+wG`^`C_U*w^dR7nRaZ^GQyOgZ*{<`_zUyHee8 z3Bzau3U3%jwsR*GtD68;pprLwu@Ch5`CB`VbOYH#!z95PR$bEVPHJ4%Hr}*cDvP*Rjk#AN zCA=KB4j^^Td5^X+_fFi`uPU-za$VX8z(11`fGpy0f?l}H@$5@sQ=-e4H^tVYTY5qp zVJ$=sK>uGRE-I>|N)^O=tce=PS=0q`wYmGww}t8_FZoR?N}OOMR@9uS8-8sYZc@ z6b|mM>aA^Qy2Hc%dVeU^3ZX?5hRg2LMl}CM5czz|BU!TCjky-_21G8^p7;NOFAxRf zQurSQByN3+iH!a$lba#9&tq__<+5rC6Y_BxKu_GBL><0=CHL0=^duK}uVjaTjPhH}Dw9}{%~aEvMA43?;9xjtrqHmzXTX}8 z?y*RxUu2;Yd6MsU_Klj)s9MV?+t>;SuL1i$wY&wwce@Oq13kEi_u&(-;u%wk z7YV`V^4Tl;Rc;QxS|Z}UKp;H~q@$wZZWFFUBS0~{8~x$AEDp>;N7H`_(23fblxctD zG68}dnx-NFT+amQZf^1U9YtKo3~w0Em5g2nH&CtB**Mq)9^ z+*fP$LP-I%48=eZaBFk?B6(A0bPjf1>Ak+G%Pw7gyqXg_J!YGgwo{}Sd!cM}1MK+{ zlj<)_)8{?_^-WZJeXAXiDTaWCp6gZYl(?`>+audw21O0T+)}X}=e#z(nDeZZn;4RPsSGzwi+y_)nXoO9GF z#o_JJMhAC(Bv!QBg-0DoBYBH21mNWEAe=f>)Ixkvkh$1mW_!w><08F zqabf%@|c+jvb*Q)C64ZyeQ*khIY!*WKwKjG3*$wf13)jwz|v|#Sf;-=QT2;&c-BWT z3GnvrKkbnW+1Nged{*MJo0e;TMl2mc7(#o3;<%LCt$U*ALDr{9SW9n%F~$3s$G$kR z;7?LUTXS=8Sih@e6G?)Fb6mj;&$=!Q`bP9?B|qLGLAE_{fr%kQ84L@;k62Q$sSi<8 zbF5rJi#^!!$0j{gq#}&8J|dv2og-|LKd72I>jH_8CvZGXzHnIKbB8}BkQ&9QA~y!Eh=l-n zaJ=xr)r|q>wbEB~qFG^(t%8P#_WP>nO4wFXbolc1TVQ3WH`oL17p3LOs8?e!p&Q`B zM-^!dDNiWYeszJ-r~<_g?`j?zx+Z{KmAhM~LvGy&cv^!u2c!PP?9>Zc z#CpC5VH3W+Qhc9A@+(yd^Udc}+;3?W`YCLTS6jA&NX6U4-PV)b8S{A~Pe)N4d;cw( z{rAZjk9k22iqzI~7XgGtp)j!ocsOAD!EX>B1DN8sl0dURgFu^qQiEhVOv-(=e}yLH zSu9JVzpxQ7hP+cHbS=nna6rKD#mY;hl{XKq_Km_AwFd=RK(H^sn+_fH-R`)bB<}j@ zLe=qA!nMIsfw+$eLPmfyh-1d#W-1y8NgEli3GK@sM%>*-7LH000w)^OMRvfr47GXR%K+Jfe*Wj(lQ| z5BJH{fCKYbb7LifUZ<6m5+P?>`D6E9>~R0Oyq0TENx`t48$GI(kmKZP1O#Yn82V$(GlZp9|0c@Zz9k5ct(nX$dEdi+Juvpo&0sQS zVH{o~M6{_i7Euh1906;XGNhYzHs%=i6H*&5KYa$a>Zpv7`yreCapE(I?0mEZR^$S| zdz4ue1@0d0L8>zle3HLUcX!bSW<)EJCKRGf1yrVO;!PF|b2H8C#{@@nBo?3Y@R3z# z{g|6Pky1eZU_+y=14&}mJ!z#Jm`6+cM!R>VJ?-G?IiGj-`J)Cdpa=?j5CvuB&+Ovk z4~Z@{B<_HSduA!)8im;b1DewQ>1H%LZ)cpjLva ztwt?;g{usSTOR}K&NiI1s_%6paGFUQ@ichl2nsuu*pc6pp1-EwI%tS4Gx6l;Qz}eY ziAEV#R}-*L)#IABMStuMI^ckQB8czwXckx_pZO}l+*fbiyc5e`lXNW987~zob_y=? zra1bF*g3-QhKiCD5PA`g6F+$oj#RWalZZ*ljCrE%KZlGfOf)pGrP$2iZ=lAqg}g(I zETFNDo~9onz2i23zAvpj8-e)vUn7nG6lzBjS}5J7?6tw$Lz>d=aftG4%X-L;Ow*ZF zGS7y2lY62tCi}uUSX|g@$*G#(8L*T*OUJAtBA1GDV#jX8-y8v+vP_ct$e)~34)r1( z3O4TtF{^Zcqm*O0J|S9NOgq`HBZ^Ot*Ye*bHLtx6uYX5WK=mR>4Mig|)({tFWA*$7 zvntwZRqL!X(=Q~q$G6xBSRpuFOn{sWwIMYTKpRIrqX6zCvjO>pC_YH zEkLfRn9nIn-WGSdbpXz(8ZAvv@jqovG9DL6Dr!r%zP$WxAOVJ(Q8V{dH|B|D#`Fpc zgW0MfR5wnwB;v@zlfyZgJ{)Pb)m74CA;X9?c@kX?lkS*F;C)}rYFPc2u}M9BGfY=L z7)JaOvmr=V*1ke>XP1Cf7ZOG@Wq9pgA}}Qte`=DYIhbq-o2I%hjY9^!%F1SX-gc&P z&yqML6Up$6DKfgY49*K-r%B8vFsWBS{Fi{?K|8(jXp(I*@-#_l#ixDlsH%8tv)!GK z+vDcG(4?*rPKQEnAum~ByE*GHQ%R;^L1GFXk_wWDz_p~Hr)k=I=q9M4t>zTGKuG9ACy%h&fN z#pa%@NO8k1>I?80KXX{;L)}I|wdx$xh{CF)-?JL8c2Le+Wcn2|++<+= zGdYye19}mBaz2(SquEkfPN+ILseN0ce>$e%+t7L_y#NZ?FcΝ@`?EOokpSB;~&S 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%0A|)tGTmQ4FMu=svz1gux%h|tiZku`erwy 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 iy9jL%-2uhHFFMd^fP>_R?RpIKPw|;DxJ=gQ +OpenClaw Bot(chalice)在 Robrix 中成功回复消息 > **重要:** 如果你在 OpenClaw 加密设备创建之前发送过消息,那些历史消息**永远无法解密**(这是 Matrix E2EE 的正常行为)。必须发送**新消息**才能触发回复。 diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index defab4fdc..f6beffc0e 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -293,7 +293,7 @@ matrix: device is verified by its owner and ready for encrypted rooms ← Encry 3. **Start a DM**: Select the bot to enter a conversation 4. **Send a message** and wait for a reply - +OpenClaw Bot (chalice) successfully replying in Robrix > **Important:** If you sent messages before OpenClaw's encryption device was created, those historical messages **can never be decrypted** (this is normal Matrix E2EE behavior). You must send a **new message** to trigger a reply. diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md index 4df23956e..b4d757ac6 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md @@ -45,7 +45,7 @@ 2. 输入消息(例如 "你好"),按回车 3. 等待 1-3 秒,Bot 应该回复 - +OpenClaw Bot(chalice)成功回复消息 > **注意:** 如果 Bot 刚刚部署完成,你之前发送的消息可能无法被解密(因为那些消息的加密密钥没有分发给 Bot 的设备)。这是正常的 Matrix E2EE 行为——发送**新消息**即可。 @@ -65,14 +65,16 @@ OpenClaw 会保持对话上下文。你可以连续提问,Bot 会记住之前 2. 邀请 Bot(输入 Bot 的 Matrix ID) 3. Bot 会自动加入(因为配置了 `autoJoin: "always"`) - +OpenClaw Bot(chalice)接受邀请加入房间,和普通用户一样 + +> 可以看到 chalice 以普通用户身份接受了邀请加入房间——这就是客户端模式的特点,Bot 和普通用户没有任何区别。 ### 3.2 在房间中对话 - **默认行为:** Bot 会回复房间内的所有消息 - **如果配置了 `requireMention: true`:** 需要在消息中 @Bot 才会触发回复 - +在房间中与 OpenClaw Bot 对话 --- diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md index 8e1b67052..121d46c03 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md @@ -45,7 +45,7 @@ Confirm the following: 2. Type a message (e.g., "Hello"), press Enter 3. Wait 1-3 seconds -- the bot should reply - +OpenClaw Bot (chalice) successfully replying > **Note:** If the bot was just deployed, messages you sent earlier may not be decryptable (because those messages' encryption keys were not distributed to the bot's device). This is normal Matrix E2EE behavior -- send a **new message** instead. @@ -65,14 +65,16 @@ In addition to DMs, you can invite the bot to group chat rooms. 2. Invite the bot (type the bot's Matrix ID) 3. The bot joins automatically (because `autoJoin: "always"` is configured) - +OpenClaw Bot (chalice) accepts invitation and joins the room, just like a regular user + +> Notice that chalice accepts the invitation and joins the room as a regular user -- this is a key characteristic of the client mode. The bot is indistinguishable from any other user. ### 3.2 Chat in the Room - **Default behavior:** The bot responds to all messages in the room - **If `requireMention: true` is configured:** You need to @mention the bot to trigger a reply - +Chatting with OpenClaw Bot in a room --- From c837b1be14d46e85d4fcfbedb25d10ef71510656 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 13:40:06 +0800 Subject: [PATCH 132/283] docs: mention Access Token auth as alternative to password --- .../01-deploying-openclaw-with-matrix-zh.md | 2 +- docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 4123b7b70..1ebb686ba 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -229,7 +229,7 @@ openclaw config | `homeserver` | `"http://127.0.0.1:8128"` | **本地 Palpo 必须用 `http`**,不是 `https`(Palpo 默认没有 TLS)。matrix.org 用 `https`。 | | `network.dangerouslyAllowPrivateNetwork` | `true` | **仅本地/内网部署需要。** OpenClaw 默认阻止连接私有 IP(127.0.0.1、10.x、192.168.x),这是防 SSRF 的安全措施。连公共服务器(matrix.org)不需要此项。 | | `userId` | `"@chalice:127.0.0.1:8128"` | **必须是完整 Matrix ID 格式** `@用户名:服务器`。 | -| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。 | +| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。也支持 Access Token 认证(将 `password` 替换为 `accessToken`),详见 [OpenClaw Matrix 插件文档](https://docs.openclaw.ai/channels/matrix)。 | | `encryption` | `true` | **强烈建议开启。** Matrix DM 默认启用 E2EE。如果不开,Bot 收到加密消息无法解密,表现为"发了消息但没回复"。 | | `autoJoin` | `"always"` | 测试阶段接受所有邀请。生产环境改为 `"allowlist"`。 | | `dm.policy` | `"open"` | 测试阶段允许所有私聊。生产环境改为 `"allowlist"`。 | diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index f6beffc0e..aed5ff061 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -229,7 +229,7 @@ Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below | `homeserver` | `"http://127.0.0.1:8128"` | **Local Palpo must use `http`**, not `https` (Palpo has no TLS by default). matrix.org uses `https`. | | `network.dangerouslyAllowPrivateNetwork` | `true` | **Only needed for local/LAN deployments.** OpenClaw blocks private IPs (127.0.0.1, 10.x, 192.168.x) by default as an anti-SSRF security measure. Not needed when connecting to public servers like matrix.org. | | `userId` | `"@chalice:127.0.0.1:8128"` | **Must be the full Matrix ID format** `@username:server`. | -| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. | +| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. Access Token authentication is also supported (replace `password` with `accessToken`) -- see [OpenClaw Matrix Plugin Docs](https://docs.openclaw.ai/channels/matrix). | | `encryption` | `true` | **Strongly recommended.** Matrix DMs enable E2EE by default. Without this, the bot receives encrypted messages it cannot decrypt, resulting in "message sent but no reply". | | `autoJoin` | `"always"` | Accept all invites during testing. Change to `"allowlist"` in production. | | `dm.policy` | `"open"` | Allow all DMs during testing. Change to `"allowlist"` in production. | From 6f1a5eb923b64802f2208647960952845b9fe0f0 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 13:45:50 +0800 Subject: [PATCH 133/283] docs: remove Palpo cross-signing bug references --- .../01-deploying-openclaw-with-matrix-zh.md | 124 +++++++++--------- .../01-deploying-openclaw-with-matrix.md | 1 - ...ow-robrix-and-openclaw-work-together-zh.md | 1 - ...3-how-robrix-and-openclaw-work-together.md | 1 - 4 files changed, 62 insertions(+), 65 deletions(-) diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 1ebb686ba..39811a13c 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -30,13 +30,13 @@ ## 1. 前置条件 -| 条件 | 说明 | -|------|------| -| **两个 Matrix 账号** | 一个作为你自己使用的账号,另一个作为 OpenClaw Bot 使用的账号 | -| **Node.js** | v22.16+ 或 v24+(推荐) | -| **LLM API Key** | 例如 [DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等 | -| **Matrix 服务器** | 本地 Palpo(推荐,参见 [Palpo 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md))或公共服务器 matrix.org | -| **Robrix** | 参见 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) | +| 条件 | 说明 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **两个 Matrix 账号** | 一个作为你自己使用的账号,另一个作为 OpenClaw Bot 使用的账号 | +| **Node.js** | v22.16+ 或 v24+(推荐) | +| **LLM API Key** | 例如[DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等 | +| **Matrix 服务器** | 本地 Palpo(推荐,参见[Palpo 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md))或公共服务器 matrix.org | +| **Robrix** | 参见[Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) | --- @@ -57,13 +57,14 @@ Bot 账号就是一个**普通的 Matrix 账号**。OpenClaw 会用它的用户名和密码自动登录,不需要你手动获取 Access Token。 -| 服务器 | 注册方式 | 说明 | -|--------|---------|------| -| **本地 Palpo**(推荐) | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | -| **matrix.org** | 在 Robrix 或 [Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | -| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | +| 服务器 | 注册方式 | 说明 | +| ---------------------------- | ----------------------------------------------------- | ---------------------------------------------- | +| **本地 Palpo**(推荐) | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | +| **matrix.org** | 在 Robrix 或[Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | +| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | 注册时记住: + - **用户名**(例如 `chalice`) - **密码** @@ -205,50 +206,50 @@ openclaw config #### `gateway` 配置 -| 字段 | 值 | 重点说明 | -|------|-----|---------| +| 字段 | 值 | 重点说明 | +| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `"local"` | **必填。** 没有这个字段 gateway 会拒绝启动,报 "missing gateway.mode" 错误。`"local"` 指的是 OpenClaw gateway 本身运行在本地(只监听 127.0.0.1),与 LLM 是否远程无关——DeepSeek API 调用仍然走公网。 | #### `models.providers` 配置 -| 字段 | 值 | 重点说明 | -|------|-----|---------| -| `baseUrl` | `"https://api.deepseek.com/v1"` | **必须带 `/v1` 后缀。** DeepSeek 使用 OpenAI 兼容 API。 | -| `apiKey` | `"sk-xxx"` | **直接写明文密钥。** 不要用 `${ENV_VAR}` 格式——macOS LaunchAgent 服务读不到终端的环境变量。写完后 `chmod 600 ~/.openclaw/openclaw.json` 保护文件权限。 | -| `api` | `"openai-completions"` | **不是 `type`。** 网上很多教程写 `"type"` 是错的,正确字段名是 `"api"`。 | -| `contextWindow` | `164000` | **必须设大。** OpenClaw 系统提示词占 16K+ token,默认 4096 会直接报错。DeepSeek Chat 支持 164K。 | -| `maxTokens` | `8192` | 单次回复最大 token 数。 | +| 字段 | 值 | 重点说明 | +| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `baseUrl` | `"https://api.deepseek.com/v1"` | **必须带 `/v1` 后缀。** DeepSeek 使用 OpenAI 兼容 API。 | +| `apiKey` | `"sk-xxx"` | **直接写明文密钥。** 不要用 `${ENV_VAR}` 格式——macOS LaunchAgent 服务读不到终端的环境变量。写完后 `chmod 600 ~/.openclaw/openclaw.json` 保护文件权限。 | +| `api` | `"openai-completions"` | **不是 `type`。** 网上很多教程写 `"type"` 是错的,正确字段名是 `"api"`。 | +| `contextWindow` | `164000` | **必须设大。** OpenClaw 系统提示词占 16K+ token,默认 4096 会直接报错。DeepSeek Chat 支持 164K。 | +| `maxTokens` | `8192` | 单次回复最大 token 数。 | > **注意 `providers` 的格式:** `providers` 是一个对象(provider 名称作为 key),不是数组。`models` 是数组。 #### `channels.matrix` 配置 -| 字段 | 值 | 重点说明 | -|------|-----|---------| -| `enabled` | `true` | 启用 Matrix 频道。 | -| `homeserver` | `"http://127.0.0.1:8128"` | **本地 Palpo 必须用 `http`**,不是 `https`(Palpo 默认没有 TLS)。matrix.org 用 `https`。 | -| `network.dangerouslyAllowPrivateNetwork` | `true` | **仅本地/内网部署需要。** OpenClaw 默认阻止连接私有 IP(127.0.0.1、10.x、192.168.x),这是防 SSRF 的安全措施。连公共服务器(matrix.org)不需要此项。 | -| `userId` | `"@chalice:127.0.0.1:8128"` | **必须是完整 Matrix ID 格式** `@用户名:服务器`。 | -| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。也支持 Access Token 认证(将 `password` 替换为 `accessToken`),详见 [OpenClaw Matrix 插件文档](https://docs.openclaw.ai/channels/matrix)。 | -| `encryption` | `true` | **强烈建议开启。** Matrix DM 默认启用 E2EE。如果不开,Bot 收到加密消息无法解密,表现为"发了消息但没回复"。 | -| `autoJoin` | `"always"` | 测试阶段接受所有邀请。生产环境改为 `"allowlist"`。 | -| `dm.policy` | `"open"` | 测试阶段允许所有私聊。生产环境改为 `"allowlist"`。 | +| 字段 | 值 | 重点说明 | +| ------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `enabled` | `true` | 启用 Matrix 频道。 | +| `homeserver` | `"http://127.0.0.1:8128"` | **本地 Palpo 必须用 `http`**,不是 `https`(Palpo 默认没有 TLS)。matrix.org 用 `https`。 | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **仅本地/内网部署需要。** OpenClaw 默认阻止连接私有 IP(127.0.0.1、10.x、192.168.x),这是防 SSRF 的安全措施。连公共服务器(matrix.org)不需要此项。 | +| `userId` | `"@chalice:127.0.0.1:8128"` | **必须是完整 Matrix ID 格式** `@用户名:服务器`。 | +| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。也支持 Access Token 认证(将 `password` 替换为 `accessToken`),详见 [OpenClaw Matrix 插件文档](https://docs.openclaw.ai/channels/matrix)。 | +| `encryption` | `true` | **强烈建议开启。** Matrix DM 默认启用 E2EE。如果不开,Bot 收到加密消息无法解密,表现为"发了消息但没回复"。 | +| `autoJoin` | `"always"` | 测试阶段接受所有邀请。生产环境改为 `"allowlist"`。 | +| `dm.policy` | `"open"` | 测试阶段允许所有私聊。生产环境改为 `"allowlist"`。 | #### `plugins` 配置 -| 字段 | 值 | 重点说明 | -|------|-----|---------| +| 字段 | 值 | 重点说明 | +| ---------------------------------- | -------- | ------------------------ | | `plugins.entries.matrix.enabled` | `true` | 确保 Matrix 插件已启用。 | ### 5.4 本地 Palpo vs 公共 matrix.org 的差异 -| 配置项 | 本地 Palpo | 公共 matrix.org | -|--------|-----------|----------------| -| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | -| `network.dangerouslyAllowPrivateNetwork` | **需要** `true` | **不需要**(删除整个 `network` 块) | -| `userId` 格式 | `@用户名:127.0.0.1:8128` | `@用户名:matrix.org` | -| TLS | 无(`http`) | 有(`https`) | -| 注册方式 | Robrix 连接 Palpo 注册 | Element Web 或 Robrix 注册 | +| 配置项 | 本地 Palpo | 公共 matrix.org | +| ------------------------------------------ | -------------------------- | ------------------------------------------- | +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **需要** `true` | **不需要**(删除整个 `network` 块) | +| `userId` 格式 | `@用户名:127.0.0.1:8128` | `@用户名:matrix.org` | +| TLS | 无(`http`) | 有(`https`) | +| 注册方式 | Robrix 连接 Palpo 注册 | Element Web 或 Robrix 注册 | > **从本地 Palpo 切换到 matrix.org:** 本指南以 Palpo 为例,但同样的配置可以直接用于 matrix.org 或任何标准 Matrix 服务器。只需修改 `openclaw.json` 中的 3 处: > @@ -301,22 +302,21 @@ matrix: device is verified by its owner and ready for encrypted rooms ← 加 ## 7. 故障排查 -| 现象 | 原因 | 解决方案 | -|------|------|---------| -| `channels add` 向导崩溃报 ENOENT | v2026.4.7 Telegram 插件路径 bug | 跳过向导,直接编辑 `~/.openclaw/openclaw.json` | -| Gateway 拒绝启动:"missing gateway.mode" | 配置文件缺少 `gateway` 配置节 | 添加 `"gateway": {"mode": "local"}` | -| "Blocked hostname or private/internal/special-use IP address" | OpenClaw 默认阻止连接私有 IP | 添加 `"network": {"dangerouslyAllowPrivateNetwork": true}` | -| Matrix 连接失败,反复重试 | `homeserver` 使用了 `https` 但本地 Palpo 没有 TLS | 改为 `http://127.0.0.1:8128` | -| 启动报 "Invalid input: expected record, received array" | `providers` 格式写成了数组 | `providers` 是对象(key-value),不是数组 | -| 启动报 "Unrecognized key: type" | 字段名写错 | 用 `"api"` 而不是 `"type"` | -| "missing env var DEEPSEEK_API_KEY" | 环境变量对 LaunchAgent 不可见 | API key 直接写进配置文件 | -| 消息发出但 Bot 不回复(无错误) | DM 默认加密,但 OpenClaw 没开 | 添加 `"encryption": true` | -| "encrypted event received without encryption enabled" | 同上 | 添加 `"encryption": true` | -| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | -| Cross-signing bootstrap 报 "unknown db error" | Palpo 的 `keys/signatures/upload` 接口 bug | 不影响基本加密功能,可忽略 | -| Bot 回复为空或报错 | LLM API Key 无效或余额不足 | 检查 DeepSeek API Key 和账户余额 | -| Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | -| 其他 OpenClaw 问题 | — | 查阅 [OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | +| 现象 | 原因 | 解决方案 | +| ------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `channels add` 向导崩溃报 ENOENT | v2026.4.7 Telegram 插件路径 bug | 跳过向导,直接编辑 `~/.openclaw/openclaw.json` | +| Gateway 拒绝启动:"missing gateway.mode" | 配置文件缺少 `gateway` 配置节 | 添加 `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw 默认阻止连接私有 IP | 添加 `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix 连接失败,反复重试 | `homeserver` 使用了 `https` 但本地 Palpo 没有 TLS | 改为 `http://127.0.0.1:8128` | +| 启动报 "Invalid input: expected record, received array" | `providers` 格式写成了数组 | `providers` 是对象(key-value),不是数组 | +| 启动报 "Unrecognized key: type" | 字段名写错 | 用 `"api"` 而不是 `"type"` | +| "missing env var DEEPSEEK_API_KEY" | 环境变量对 LaunchAgent 不可见 | API key 直接写进配置文件 | +| 消息发出但 Bot 不回复(无错误) | DM 默认加密,但 OpenClaw 没开 | 添加 `"encryption": true` | +| "encrypted event received without encryption enabled" | 同上 | 添加 `"encryption": true` | +| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | +| Bot 回复为空或报错 | LLM API Key 无效或余额不足 | 检查 DeepSeek API Key 和账户余额 | +| Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | +| 其他 OpenClaw 问题 | — | 查阅[OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | > **重要说明:** 本指南仅覆盖我们实测验证过的配置流程(OpenClaw v2026.4.7)。OpenClaw 本身仍在快速迭代中,其 CLI、插件系统、Gateway 行为可能在后续版本中发生变化。如果你遇到本指南中未列出的 OpenClaw 问题(如 CLI 报错、插件加载失败、Gateway 行为异常等),这些属于 OpenClaw 自身的问题,请参考以下资源: > @@ -351,12 +351,12 @@ matrix: device is verified by its owner and ready for encrypted rooms ← 加 } ``` -| 字段 | 测试值 | 生产值 | 说明 | -|------|--------|--------|------| -| `autoJoin` | `"always"` | `"allowlist"` | 只加入白名单中的房间 | -| `dm.policy` | `"open"` | `"allowlist"` | 只接受白名单用户的私聊 | -| `groupPolicy` | — | `"allowlist"` | 群聊中限制谁可以触发 Bot | -| `requireMention` | — | `true` | 群聊中必须 @Bot 才响应 | +| 字段 | 测试值 | 生产值 | 说明 | +| ------------------ | ------------ | --------------- | ------------------------ | +| `autoJoin` | `"always"` | `"allowlist"` | 只加入白名单中的房间 | +| `dm.policy` | `"open"` | `"allowlist"` | 只接受白名单用户的私聊 | +| `groupPolicy` | — | `"allowlist"` | 群聊中限制谁可以触发 Bot | +| `requireMention` | — | `true` | 群聊中必须 @Bot 才响应 | --- diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index aed5ff061..a797c7192 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -313,7 +313,6 @@ matrix: device is verified by its owner and ready for encrypted rooms ← Encry | Message sent but bot does not reply (no error) | DM is encrypted but OpenClaw has encryption disabled | Add `"encryption": true` | | "encrypted event received without encryption enabled" | Same as above | Add `"encryption": true` | | "This message was sent before this device logged in" | Historical messages cannot be decrypted | Normal behavior. Send a **new message** | -| Cross-signing bootstrap reports "unknown db error" | Palpo's `keys/signatures/upload` API bug | Does not affect basic encryption, can be ignored | | Bot replies are empty or error | LLM API key invalid or insufficient balance | Check DeepSeek API key and account balance | | Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | | Other OpenClaw issues | — | Consult [OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md index 26ac77ebb..5a6979a74 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md @@ -195,7 +195,6 @@ OpenClaw 的 Matrix 插件使用 matrix-js-sdk 的 **Rust crypto 路径**,实 ### 注意事项 - **历史消息不可解密** —— 在 OpenClaw 设备创建之前发送的消息,其 Megolm 会话密钥未分发给 OpenClaw,永远无法解密。 -- **Palpo 的 cross-signing bug** —— `keys/signatures/upload` 可能返回 "unknown db error",但不影响基本加密功能。 - **vs Octos** —— Octos 作为 AppService 接收的是**服务器端解密后的明文事件**,不需要处理 E2EE。OpenClaw 作为客户端必须自己处理加密。 --- diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md index f9d51d268..980638a97 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md @@ -195,7 +195,6 @@ When `"encryption": true` is configured: ### Important Notes - **Historical messages cannot be decrypted** -- Messages sent before OpenClaw's device was created did not have their Megolm session keys distributed to OpenClaw. They can never be decrypted. -- **Palpo cross-signing bug** -- `keys/signatures/upload` may return "unknown db error", but this does not affect basic encryption functionality. - **vs Octos** -- Octos, as an AppService, receives **server-side decrypted plaintext events**. It does not need to handle E2EE at all. OpenClaw, as a client, must handle encryption itself. --- From 8c86e1ef2166d0ca826c00b6612e967391ca0442 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 13:55:00 +0800 Subject: [PATCH 134/283] docs: add brief OpenClaw introduction to deployment guides --- .../01-deploying-openclaw-with-matrix-zh.md | 4 ++++ .../robrix-with-openclaw/01-deploying-openclaw-with-matrix.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 39811a13c..8f93c1397 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -4,6 +4,10 @@ > **目标:** 完成本指南后,你将拥有一个连接到 Matrix 服务器的 OpenClaw AI 代理。之后你可以使用 Robrix(或任何 Matrix 客户端)与 OpenClaw 驱动的 AI 代理对话。 +## 什么是 OpenClaw? + +[OpenClaw](https://github.com/openclaw/openclaw) 是一个开源的自托管 AI 助手平台(前身为 MoltBot,2026 年初更名)。它支持多种 LLM(DeepSeek、OpenAI、Anthropic 等),并通过频道插件接入 Matrix、Telegram、Discord 等聊天平台。在本指南中,OpenClaw 通过 **Matrix 频道插件**以**普通用户身份**登录服务器,不需要任何服务器端配置。与 Octos 的 Application Service 模式不同,OpenClaw 的接入方式更简单——详见[架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + 本指南将逐步引导你完成 OpenClaw 与 Matrix 的部署:从创建 Matrix Bot 账号,到配置 OpenClaw Matrix 频道插件,再到端到端验证连接。 > **想快速体验?** 跳转到 [快速开始](#2-快速开始)。 diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index a797c7192..11a3cdcb4 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -4,6 +4,10 @@ > **Goal:** After following this guide, you will have OpenClaw running as an AI agent connected to a Matrix homeserver. You can then use Robrix (or any Matrix client) to chat with OpenClaw-powered AI agents. +## What is OpenClaw? + +[OpenClaw](https://github.com/openclaw/openclaw) is an open-source, self-hosted AI assistant platform (formerly MoltBot, renamed in early 2026). It supports multiple LLMs (DeepSeek, OpenAI, Anthropic, etc.) and connects to Matrix, Telegram, Discord, and other chat platforms via channel plugins. In this guide, OpenClaw logs in to the Matrix server as a **regular user** through its **Matrix channel plugin**, requiring no server-side configuration. This is simpler than Octos's Application Service approach -- see [Architecture Guide](03-how-robrix-and-openclaw-work-together.md) for details. + This guide walks you through deploying OpenClaw with Matrix step by step: from creating a Matrix bot account, to configuring the OpenClaw Matrix channel plugin, to verifying the connection end-to-end. > **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start). From 221726758284b64d025c827e801a659bd4b7644c Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 8 Apr 2026 14:48:08 +0800 Subject: [PATCH 135/283] =?UTF-8?q?docs:=20polish=20OpenClaw=20guides=20?= =?UTF-8?q?=E2=80=94=20remove=20unsupported=20items,=20add=20wait=20tips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove partially supported / unsupported feature entries from comparison tables (tracked in #67 for future enhancement) - Add startup wait tip: OpenClaw needs time to complete Matrix login - Add LLM response wait tip: replies may take a few seconds - Minor formatting fixes from linter --- .../01-deploying-openclaw-with-matrix-zh.md | 16 ++- .../01-deploying-openclaw-with-matrix.md | 127 +++++++++--------- .../02-using-robrix-with-openclaw-zh.md | 7 +- .../02-using-robrix-with-openclaw.md | 7 +- ...ow-robrix-and-openclaw-work-together-zh.md | 1 - ...3-how-robrix-and-openclaw-work-together.md | 1 - 6 files changed, 80 insertions(+), 79 deletions(-) diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md index 8f93c1397..2fab781ce 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -61,11 +61,11 @@ Bot 账号就是一个**普通的 Matrix 账号**。OpenClaw 会用它的用户名和密码自动登录,不需要你手动获取 Access Token。 -| 服务器 | 注册方式 | 说明 | -| ---------------------------- | ----------------------------------------------------- | ---------------------------------------------- | -| **本地 Palpo**(推荐) | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | -| **matrix.org** | 在 Robrix 或[Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | -| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | +| 服务器 | 注册方式 | 说明 | +| ---------------------- | ----------------------------------------------------- | ---------------------------------------------- | +| **本地 Palpo** | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | +| **matrix.org** | 在 Robrix 或[Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | +| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | 注册时记住: @@ -291,12 +291,14 @@ matrix: logged in as @chalice:127.0.0.1:8128 ← 登录成功 matrix: device is verified by its owner and ready for encrypted rooms ← 加密就绪 ``` +> **提示:** OpenClaw 启动后需要一些时间完成 Matrix 登录和加密设备初始化。如果日志中只看到 `starting provider` 而没有 `logged in`,请耐心等待几秒到十几秒,登录完成后才能接收消息。 + ### 6.3 在 Robrix 中测试 1. **启动 Robrix**,用你的**个人账号**登录 2. **搜索 Bot**:点搜索图标,输入 Bot 的 Matrix ID(如 `@chalice:127.0.0.1:8128`),切到 **People** 标签(Bot 是普通用户,必须在 People 中搜索) 3. **发起私聊**:选择 Bot,进入对话 -4. **发送消息**,等待回复 +4. **发送消息**,耐心等待回复(LLM 生成回复需要几秒到十几秒) OpenClaw Bot(chalice)在 Robrix 中成功回复消息 @@ -317,7 +319,7 @@ matrix: device is verified by its owner and ready for encrypted rooms ← 加 | "missing env var DEEPSEEK_API_KEY" | 环境变量对 LaunchAgent 不可见 | API key 直接写进配置文件 | | 消息发出但 Bot 不回复(无错误) | DM 默认加密,但 OpenClaw 没开 | 添加 `"encryption": true` | | "encrypted event received without encryption enabled" | 同上 | 添加 `"encryption": true` | -| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | +| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | | Bot 回复为空或报错 | LLM API Key 无效或余额不足 | 检查 DeepSeek API Key 和账户余额 | | Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | | 其他 OpenClaw 问题 | — | 查阅[OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md index 11a3cdcb4..bad16f4f2 100644 --- a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -34,13 +34,13 @@ This guide walks you through deploying OpenClaw with Matrix step by step: from c ## 1. Prerequisites -| Requirement | Notes | -|-------------|-------| -| **Two Matrix accounts** | One for yourself, one for the OpenClaw bot | -| **Node.js** | v22.16+ or v24+ (recommended) | -| **LLM API Key** | e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | -| **Matrix homeserver** | Local Palpo (recommended, see [Palpo Deployment Guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md)) or public server matrix.org | -| **Robrix** | See [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) | +| Requirement | Notes | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **Two Matrix accounts** | One for yourself, one for the OpenClaw bot | +| **Node.js** | v22.16+ or v24+ (recommended) | +| **LLM API Key** | e.g.,[DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | +| **Matrix homeserver** | Local Palpo (recommended, see[Palpo Deployment Guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md)) or public server matrix.org | +| **Robrix** | See[Getting Started with Robrix](../robrix/getting-started-with-robrix.md) | --- @@ -61,13 +61,14 @@ See below for detailed steps. A bot account is just a **regular Matrix account**. OpenClaw logs in automatically using the username and password -- you do not need to manually obtain an Access Token. -| Server | How to register | Notes | -|--------|----------------|-------| -| **Local Palpo** (recommended) | Register in Robrix | Connect to `http://127.0.0.1:8128` and register a new account | -| **matrix.org** | Register in Robrix or [Element Web](https://app.element.io) | Public server, free, instant | -| **Self-hosted Synapse** | Via Admin API or web registration | Recommended for production | +| Server | How to register | Notes | +| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------- | +| **Local Palpo** | Register in Robrix | Connect to `http://127.0.0.1:8128` and register a new account | +| **matrix.org** | Register in Robrix or[Element Web](https://app.element.io) | Public server, free, instant | +| **Self-hosted Synapse** | Via Admin API or web registration | Recommended for production | When registering, remember: + - **Username** (e.g., `chalice`) - **Password** @@ -209,50 +210,50 @@ Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below #### `gateway` Configuration -| Field | Value | Key Notes | -|-------|-------|-----------| +| Field | Value | Key Notes | +| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mode` | `"local"` | **Required.** Without this field, gateway refuses to start with "missing gateway.mode" error. `"local"` means the OpenClaw gateway itself runs locally (listens on 127.0.0.1 only) -- this is unrelated to whether the LLM is remote. DeepSeek API calls still go over the internet. | #### `models.providers` Configuration -| Field | Value | Key Notes | -|-------|-------|-----------| -| `baseUrl` | `"https://api.deepseek.com/v1"` | **Must include the `/v1` suffix.** DeepSeek uses an OpenAI-compatible API. | -| `apiKey` | `"sk-xxx"` | **Write the key directly as plaintext.** Do not use `${ENV_VAR}` format -- macOS LaunchAgent services cannot read terminal environment variables. After writing, run `chmod 600 ~/.openclaw/openclaw.json` to protect file permissions. | -| `api` | `"openai-completions"` | **Not `type`.** Many online tutorials incorrectly use `"type"` -- the correct field name is `"api"`. | -| `contextWindow` | `164000` | **Must be set high.** OpenClaw's system prompt alone takes 16K+ tokens; the default 4096 will cause errors. DeepSeek Chat supports 164K. | -| `maxTokens` | `8192` | Maximum tokens per reply. | +| Field | Value | Key Notes | +| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `baseUrl` | `"https://api.deepseek.com/v1"` | **Must include the `/v1` suffix.** DeepSeek uses an OpenAI-compatible API. | +| `apiKey` | `"sk-xxx"` | **Write the key directly as plaintext.** Do not use `${ENV_VAR}` format -- macOS LaunchAgent services cannot read terminal environment variables. After writing, run `chmod 600 ~/.openclaw/openclaw.json` to protect file permissions. | +| `api` | `"openai-completions"` | **Not `type`.** Many online tutorials incorrectly use `"type"` -- the correct field name is `"api"`. | +| `contextWindow` | `164000` | **Must be set high.** OpenClaw's system prompt alone takes 16K+ tokens; the default 4096 will cause errors. DeepSeek Chat supports 164K. | +| `maxTokens` | `8192` | Maximum tokens per reply. | > **Note on `providers` format:** `providers` is an object (provider name as key), not an array. `models` inside a provider IS an array. #### `channels.matrix` Configuration -| Field | Value | Key Notes | -|-------|-------|-----------| -| `enabled` | `true` | Enable the Matrix channel. | -| `homeserver` | `"http://127.0.0.1:8128"` | **Local Palpo must use `http`**, not `https` (Palpo has no TLS by default). matrix.org uses `https`. | -| `network.dangerouslyAllowPrivateNetwork` | `true` | **Only needed for local/LAN deployments.** OpenClaw blocks private IPs (127.0.0.1, 10.x, 192.168.x) by default as an anti-SSRF security measure. Not needed when connecting to public servers like matrix.org. | -| `userId` | `"@chalice:127.0.0.1:8128"` | **Must be the full Matrix ID format** `@username:server`. | -| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. Access Token authentication is also supported (replace `password` with `accessToken`) -- see [OpenClaw Matrix Plugin Docs](https://docs.openclaw.ai/channels/matrix). | -| `encryption` | `true` | **Strongly recommended.** Matrix DMs enable E2EE by default. Without this, the bot receives encrypted messages it cannot decrypt, resulting in "message sent but no reply". | -| `autoJoin` | `"always"` | Accept all invites during testing. Change to `"allowlist"` in production. | -| `dm.policy` | `"open"` | Allow all DMs during testing. Change to `"allowlist"` in production. | +| Field | Value | Key Notes | +| ------------------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `true` | Enable the Matrix channel. | +| `homeserver` | `"http://127.0.0.1:8128"` | **Local Palpo must use `http`**, not `https` (Palpo has no TLS by default). matrix.org uses `https`. | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **Only needed for local/LAN deployments.** OpenClaw blocks private IPs (127.0.0.1, 10.x, 192.168.x) by default as an anti-SSRF security measure. Not needed when connecting to public servers like matrix.org. | +| `userId` | `"@chalice:127.0.0.1:8128"` | **Must be the full Matrix ID format** `@username:server`. | +| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. Access Token authentication is also supported (replace `password` with `accessToken`) -- see [OpenClaw Matrix Plugin Docs](https://docs.openclaw.ai/channels/matrix). | +| `encryption` | `true` | **Strongly recommended.** Matrix DMs enable E2EE by default. Without this, the bot receives encrypted messages it cannot decrypt, resulting in "message sent but no reply". | +| `autoJoin` | `"always"` | Accept all invites during testing. Change to `"allowlist"` in production. | +| `dm.policy` | `"open"` | Allow all DMs during testing. Change to `"allowlist"` in production. | #### `plugins` Configuration -| Field | Value | Key Notes | -|-------|-------|-----------| +| Field | Value | Key Notes | +| ---------------------------------- | -------- | ------------------------------------ | | `plugins.entries.matrix.enabled` | `true` | Ensure the Matrix plugin is enabled. | ### 5.4 Local Palpo vs Public matrix.org Differences -| Setting | Local Palpo | Public matrix.org | -|---------|------------|-------------------| -| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | -| `network.dangerouslyAllowPrivateNetwork` | **Required** `true` | **Not needed** (remove the entire `network` block) | -| `userId` format | `@username:127.0.0.1:8128` | `@username:matrix.org` | -| TLS | None (`http`) | Yes (`https`) | -| Registration | Register in Robrix connected to Palpo | Register via Element Web or Robrix | +| Setting | Local Palpo | Public matrix.org | +| ------------------------------------------ | ------------------------------------- | ---------------------------------------------------------- | +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **Required** `true` | **Not needed** (remove the entire `network` block) | +| `userId` format | `@username:127.0.0.1:8128` | `@username:matrix.org` | +| TLS | None (`http`) | Yes (`https`) | +| Registration | Register in Robrix connected to Palpo | Register via Element Web or Robrix | > **Switching from local Palpo to matrix.org:** This guide uses Palpo as the example, but the same configuration works on matrix.org or any standard Matrix server. You only need to change 3 things in `openclaw.json`: > @@ -290,12 +291,14 @@ matrix: logged in as @chalice:127.0.0.1:8128 ← Login successful matrix: device is verified by its owner and ready for encrypted rooms ← Encryption ready ``` +> **Tip:** OpenClaw needs some time to complete the Matrix login and encryption device initialization after startup. If you only see `starting provider` but not `logged in` in the logs, wait a few seconds -- messages can only be received after login completes. + ### 6.3 Test in Robrix 1. **Launch Robrix** and log in with your **personal account** 2. **Search for the bot**: Click the search icon, type the bot's Matrix ID (e.g., `@chalice:127.0.0.1:8128`), switch to the **People** tab (the bot is a regular user, so you must search under People) 3. **Start a DM**: Select the bot to enter a conversation -4. **Send a message** and wait for a reply +4. **Send a message** and wait patiently for a reply (LLM responses take a few seconds to tens of seconds) OpenClaw Bot (chalice) successfully replying in Robrix @@ -305,21 +308,21 @@ matrix: device is verified by its owner and ready for encrypted rooms ← Encry ## 7. Troubleshooting -| Symptom | Cause | Solution | -|---------|-------|---------| -| `channels add` wizard crashes with ENOENT | v2026.4.7 Telegram plugin path bug | Skip the wizard, edit `~/.openclaw/openclaw.json` directly | -| Gateway refuses to start: "missing gateway.mode" | Config file missing `gateway` section | Add `"gateway": {"mode": "local"}` | -| "Blocked hostname or private/internal/special-use IP address" | OpenClaw blocks private IPs by default | Add `"network": {"dangerouslyAllowPrivateNetwork": true}` | -| Matrix connection fails, keeps retrying | `homeserver` uses `https` but local Palpo has no TLS | Change to `http://127.0.0.1:8128` | -| "Invalid input: expected record, received array" | `providers` format is wrong | `providers` must be an object (key-value), not an array | -| "Unrecognized key: type" | Wrong field name | Use `"api"` instead of `"type"` | -| "missing env var DEEPSEEK_API_KEY" | Environment variable not visible to LaunchAgent | Write API key directly in the config file | -| Message sent but bot does not reply (no error) | DM is encrypted but OpenClaw has encryption disabled | Add `"encryption": true` | -| "encrypted event received without encryption enabled" | Same as above | Add `"encryption": true` | -| "This message was sent before this device logged in" | Historical messages cannot be decrypted | Normal behavior. Send a **new message** | -| Bot replies are empty or error | LLM API key invalid or insufficient balance | Check DeepSeek API key and account balance | -| Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | -| Other OpenClaw issues | — | Consult [OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | +| Symptom | Cause | Solution | +| ------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `channels add` wizard crashes with ENOENT | v2026.4.7 Telegram plugin path bug | Skip the wizard, edit `~/.openclaw/openclaw.json` directly | +| Gateway refuses to start: "missing gateway.mode" | Config file missing `gateway` section | Add `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw blocks private IPs by default | Add `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix connection fails, keeps retrying | `homeserver` uses `https` but local Palpo has no TLS | Change to `http://127.0.0.1:8128` | +| "Invalid input: expected record, received array" | `providers` format is wrong | `providers` must be an object (key-value), not an array | +| "Unrecognized key: type" | Wrong field name | Use `"api"` instead of `"type"` | +| "missing env var DEEPSEEK_API_KEY" | Environment variable not visible to LaunchAgent | Write API key directly in the config file | +| Message sent but bot does not reply (no error) | DM is encrypted but OpenClaw has encryption disabled | Add `"encryption": true` | +| "encrypted event received without encryption enabled" | Same as above | Add `"encryption": true` | +| "This message was sent before this device logged in" | Historical messages cannot be decrypted | Normal behavior. Send a**new message** | +| Bot replies are empty or error | LLM API key invalid or insufficient balance | Check DeepSeek API key and account balance | +| Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | +| Other OpenClaw issues | — | Consult[OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | > **Important note:** This guide only covers the configuration workflow we have tested and verified (OpenClaw v2026.4.7). OpenClaw is still under rapid development -- its CLI, plugin system, and gateway behavior may change in future versions. If you encounter OpenClaw issues not listed above (CLI errors, plugin loading failures, gateway behavior anomalies, etc.), these are OpenClaw-side issues. Please refer to: > @@ -354,12 +357,12 @@ After testing, tighten permissions. Modify these fields in `channels.matrix`: } ``` -| Field | Testing | Production | Purpose | -|-------|---------|------------|---------| -| `autoJoin` | `"always"` | `"allowlist"` | Only join allowlisted rooms | -| `dm.policy` | `"open"` | `"allowlist"` | Only accept DMs from allowlisted users | -| `groupPolicy` | — | `"allowlist"` | Restrict who can trigger the bot in groups | -| `requireMention` | — | `true` | In group chats, require @mention to respond | +| Field | Testing | Production | Purpose | +| ------------------ | ------------ | --------------- | ------------------------------------------- | +| `autoJoin` | `"always"` | `"allowlist"` | Only join allowlisted rooms | +| `dm.policy` | `"open"` | `"allowlist"` | Only accept DMs from allowlisted users | +| `groupPolicy` | — | `"allowlist"` | Restrict who can trigger the bot in groups | +| `requireMention` | — | `true` | In group chats, require @mention to respond | --- diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md index b4d757ac6..82a9f9ac4 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md @@ -43,7 +43,9 @@ 1. 选择 Bot,进入对话 2. 输入消息(例如 "你好"),按回车 -3. 等待 1-3 秒,Bot 应该回复 +3. 等待 Bot 回复 + +> **提示:** LLM 生成回复需要时间,特别是较长的回答可能需要等待几秒到十几秒。请耐心等待,不要重复发送消息。 OpenClaw Bot(chalice)成功回复消息 @@ -83,9 +85,6 @@ OpenClaw 会保持对话上下文。你可以连续提问,Bot 会记住之前 | OpenClaw 功能 | Robrix 表现 | 说明 | |--------------|-------------|------| | **文字消息** | 完全支持 | 标准 Matrix 消息,无兼容性问题 | -| **流式回复** | 部分支持 | OpenClaw 可能分段发送,Robrix 逐段显示 | -| **语音气泡** | 降级显示 | OpenClaw v2026.4.5+ 的语音回复在 Robrix 中显示为附件 | -| **Exec Approval Prompts** | 降级显示 | OpenClaw 的执行审批提示在 Robrix 中显示为普通文本 | | **多轮上下文** | 完全支持 | OpenClaw 自动维护对话历史 | | **E2EE 加密** | 完全支持 | 消息在传输中全程加密 | diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md index 121d46c03..b2ac11d5c 100644 --- a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md @@ -43,7 +43,9 @@ Confirm the following: 1. Select the bot to enter the conversation 2. Type a message (e.g., "Hello"), press Enter -3. Wait 1-3 seconds -- the bot should reply +3. Wait for the bot to reply + +> **Tip:** LLM responses take time to generate, especially for longer answers (a few seconds to tens of seconds). Please be patient and avoid sending duplicate messages. OpenClaw Bot (chalice) successfully replying @@ -83,9 +85,6 @@ In addition to DMs, you can invite the bot to group chat rooms. | OpenClaw Feature | Robrix Support | Notes | |------------------|---------------|-------| | **Text messages** | Fully supported | Standard Matrix messages, no compatibility issues | -| **Streaming replies** | Partially supported | OpenClaw may send in segments; Robrix displays them incrementally | -| **Voice bubbles** | Fallback display | OpenClaw v2026.4.5+ voice replies appear as attachments in Robrix | -| **Exec Approval Prompts** | Fallback display | OpenClaw's execution approval prompts appear as plain text in Robrix | | **Multi-turn context** | Fully supported | OpenClaw automatically maintains conversation history | | **E2EE encryption** | Fully supported | Messages are encrypted end-to-end | diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md index 5a6979a74..b22091904 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md @@ -142,7 +142,6 @@ Robrix 生态中有两种接入 AI Bot 的方式:OpenClaw 使用的**客户端 | 基本对话 | 支持 | 支持 | | 多模型切换 | 支持(14+ LLM provider) | 支持 | | E2EE 加密 | 支持(Rust crypto SDK) | 不需要(AppService 绕过加密) | -| 动态创建子 Bot | 不支持(一个实例 = 一个 Bot) | 支持(BotFather 模式) | | 服务器端管理 | 不需要 | 需要管理员权限注册 AppService | | 多频道(Telegram、Discord 等) | 支持 | 仅 Matrix | | 对 homeserver 的要求 | 任何标准 Matrix 服务器 | 需要支持 AppService API | diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md index 980638a97..0a9d8560d 100644 --- a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md @@ -142,7 +142,6 @@ The Robrix ecosystem offers two ways to integrate AI bots: OpenClaw's **client m | Basic conversation | Yes | Yes | | Multiple LLM providers | Yes (14+) | Yes | | E2EE encryption | Yes (Rust crypto SDK) | Not needed (AppService bypasses encryption) | -| Dynamic child bots | No (one instance = one bot) | Yes (BotFather pattern) | | Server-side administration | Not needed | Requires admin access to register AppService | | Multi-channel (Telegram, Discord, etc.) | Yes | Matrix only | | Homeserver requirements | Any standard Matrix server | Must support Application Service API | From ba248185d52172043fbedf436cf08aadddffdf32 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Thu, 9 Apr 2026 03:15:39 +0800 Subject: [PATCH 136/283] Add issue docs for release CI failures (iOS, macOS, Android) Document three distinct release CI failures from run #24117713131: - #003: iOS missing signing secrets (ios_profile/ios_cert) - #004: macOS certificate import failure (invalid .p12) - #005: Android APK path mismatch (upload can't find built artifact) Co-Authored-By: Claude Opus 4.6 (1M context) --- ...-release-ci-ios-missing-signing-secrets.md | 39 +++++++++++++++ ...ase-ci-macos-certificate-import-failure.md | 48 +++++++++++++++++++ ...05-release-ci-android-apk-path-mismatch.md | 46 ++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 issues/003-release-ci-ios-missing-signing-secrets.md create mode 100644 issues/004-release-ci-macos-certificate-import-failure.md create mode 100644 issues/005-release-ci-android-apk-path-mismatch.md diff --git a/issues/003-release-ci-ios-missing-signing-secrets.md b/issues/003-release-ci-ios-missing-signing-secrets.md new file mode 100644 index 000000000..477f3266b --- /dev/null +++ b/issues/003-release-ci-ios-missing-signing-secrets.md @@ -0,0 +1,39 @@ +# Issue #003: Release CI — iOS build fails due to missing signing secrets + +**Date:** 2026-04-09 +**Severity:** High +**Status:** Open +**Affected component:** `.github/workflows/` (Release CI) + +## Summary +The iOS release job fails immediately because `ios_profile` and `ios_cert` secrets are not configured in the `Project-Robius-China/robrix2` fork repository. + +## Symptoms +- iOS job fails in ~57s without compiling any code +- Error: `ios_profile and ios_cert are required for iOS device builds.` + +## Root Cause +The `makepad-packaging-action` requires iOS provisioning profile and signing certificate secrets for device builds. These secrets exist in the upstream `project-robius/robrix` repo but are not available in the fork `Project-Robius-China/robrix2`. + +## Reproduction +1. Push a release tag (e.g., `v0.0.1-pre-alpha-4`) to `Project-Robius-China/robrix2` +2. Observe the "Release Robrix for iOS" job failure + +## Fix Applied +None yet. + +## Remaining Issues +1. Add `ios_profile` and `ios_cert` secrets to the fork repo Settings > Secrets +2. Alternatively, skip iOS builds in the fork's release workflow if signing certs are unavailable + +## Files Changed +None + +## Test Verification +| Before | After | +|--------|-------| +| iOS job fails: "ios_profile and ios_cert are required" | Pending fix | + +## Reference +- CI run: https://github.com/Project-Robius-China/robrix2/actions/runs/24117713131 +- Job ID: 70365022917 diff --git a/issues/004-release-ci-macos-certificate-import-failure.md b/issues/004-release-ci-macos-certificate-import-failure.md new file mode 100644 index 000000000..822ba6a24 --- /dev/null +++ b/issues/004-release-ci-macos-certificate-import-failure.md @@ -0,0 +1,48 @@ +# Issue #004: Release CI — macOS builds fail on code signing certificate import + +**Date:** 2026-04-09 +**Severity:** High +**Status:** Open +**Affected component:** `.github/workflows/` (Release CI) + +## Summary +Both macOS release jobs (aarch64 on macos-14, x86_64 on macos-15-intel) fail after successful compilation when `cargo-packager` attempts to import the `.p12` signing certificate into the macOS keychain. + +## Symptoms +- Compilation succeeds (~15-30 min) +- Packaging step fails at certificate import +- Error: `Failed to import certificate: security: SecKeychainItemImport: One or more parameters passed to a function were not valid.` +- Affects both macOS architectures identically + +## Root Cause +`cargo-packager` calls `security import cert.p12 -k cargo-packager.keychain -P "" ...` but the certificate data (from GitHub secrets) is either: +1. Missing or empty in the fork repo +2. Corrupted / incorrectly base64-encoded +3. Has a non-empty password that isn't being passed + +The `-P ""` (empty password) suggests the secret for the certificate password may also be missing. + +## Reproduction +1. Push a release tag to `Project-Robius-China/robrix2` +2. Observe both macOS jobs fail at the "Package (macos)" step after compilation completes + +## Fix Applied +None yet. + +## Remaining Issues +1. Verify macOS signing certificate secrets are properly configured (certificate .p12 + password) +2. If code signing is not needed for the fork, configure the workflow to skip signing or use ad-hoc signing +3. Consider making signing optional via a workflow input flag + +## Files Changed +None + +## Test Verification +| Before | After | +|--------|-------| +| macOS aarch64: fails at certificate import after 15min build | Pending fix | +| macOS x86_64: fails at certificate import after 30min build | Pending fix | + +## Reference +- CI run: https://github.com/Project-Robius-China/robrix2/actions/runs/24117713131 +- Jobs: 70365022919 (aarch64), 70365022955 (x86_64) diff --git a/issues/005-release-ci-android-apk-path-mismatch.md b/issues/005-release-ci-android-apk-path-mismatch.md new file mode 100644 index 000000000..6033f618c --- /dev/null +++ b/issues/005-release-ci-android-apk-path-mismatch.md @@ -0,0 +1,46 @@ +# Issue #005: Release CI — Android build succeeds but APK upload fails due to path mismatch + +**Date:** 2026-04-09 +**Severity:** High +**Status:** Open +**Affected component:** `.github/workflows/` (Release CI), `makepad-packaging-action` + +## Summary +The Android release job compiles successfully and builds the APK, but fails at the upload step because `makepad-packaging-action` looks for the APK at a path that doesn't match where `cargo-makepad` actually outputs it. + +## Symptoms +- Compilation succeeds (~13 min) +- "APK Build completed" message appears +- Upload fails with: `Missing artifacts on disk: .../target/makepad-android-apk/robrix/apk/robrix_v0.0.1-pre-alpha-4_aarch64.apk` +- The actual APK was built under `target/android/makepad-android-apk/...` (note the extra `android/` path segment) + +## Root Cause +Path mismatch between `cargo-makepad` APK output directory and `makepad-packaging-action`'s expected artifact location: +- **Expected by action:** `target/makepad-android-apk/robrix/apk/robrix_v0.0.1-pre-alpha-4_aarch64.apk` +- **Actual output:** `target/android/makepad-android-apk/robrix/apk/...` (likely) + +This suggests `cargo-makepad` changed its output directory structure, or `makepad-packaging-action@v1` has a hardcoded path that doesn't account for the `android/` subdirectory. + +## Reproduction +1. Push a release tag to `Project-Robius-China/robrix2` +2. Observe the "Release Robrix for Android (aarch64)" job — compilation and APK build succeed, but upload fails + +## Fix Applied +None yet. + +## Remaining Issues +1. Verify the actual APK output path on the CI runner (add an `ls -R target/` debug step) +2. Update `makepad-packaging-action` to use the correct path, or pin a compatible version of `cargo-makepad` +3. Report upstream if this is a bug in `makepad-packaging-action@v1` + +## Files Changed +None + +## Test Verification +| Before | After | +|--------|-------| +| Android: APK built but upload fails — path mismatch | Pending fix | + +## Reference +- CI run: https://github.com/Project-Robius-China/robrix2/actions/runs/24117713131 +- Job ID: 70365022944 From d7067fb9e67036bcf22baf112636cee5c60ada24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 9 Apr 2026 10:54:45 +0800 Subject: [PATCH 137/283] ci(release): remove Android APK path workaround --- .github/workflows/release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f10723af4..ffa294210 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -360,12 +360,6 @@ jobs: - name: Install Rust Stable uses: dtolnay/rust-toolchain@stable - - name: Workaround Android APK output path - shell: bash - run: | - mkdir -p target/android - ln -sfn android/makepad-android-apk target/makepad-android-apk - - name: Package (android) uses: project-robius/makepad-packaging-action@v1 env: From 0eafbf6cbbc7167e2c9666ab32eae1f4d8fae41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:29:50 +0800 Subject: [PATCH 138/283] feat(ios): auto patch app bundle with asset catalog, plist metadata, and re-signing (#70) --- Cargo.toml | 3 +- README.md | 12 ++- packaging/Info-iOS.plist | 37 +++++++++ packaging/ios/README.md | 32 ++++++++ packaging/ios/apply_ios_app_icons.sh | 73 ++++++++++++++++++ .../AppIcon.appiconset/AppIcon1024x1024.png | Bin 0 -> 61598 bytes .../AppIcon.appiconset/AppIcon120x120.png | Bin 0 -> 6387 bytes .../AppIcon.appiconset/AppIcon152x152.png | Bin 0 -> 7685 bytes .../AppIcon.appiconset/AppIcon167x167.png | Bin 0 -> 8412 bytes .../AppIcon.appiconset/AppIcon180x180.png | Bin 0 -> 8996 bytes .../AppIcon.appiconset/Contents.json | 38 +++++++++ 11 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 packaging/Info-iOS.plist create mode 100644 packaging/ios/README.md create mode 100755 packaging/ios/apply_ios_app_icons.sh create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon120x120.png create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon152x152.png create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon167x167.png create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon180x180.png create mode 100644 packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Cargo.toml b/Cargo.toml index 662adcd15..4e189e971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -264,7 +264,8 @@ minimum_system_version = "11.0" frameworks = [] info_plist_path = "./packaging/Info.plist" entitlements = "./packaging/Entitlements.plist" -signing_identity = "Developer ID Application: GOSIM FOUNDATION LTD. (HMX6XGJZ3R)" +# signing_identity = "Developer ID Application: GOSIM FOUNDATION LTD. (HMX6XGJZ3R)" +signing_identity = "=" ## Configuration for `cargo packager`'s generation of a macOS `.dmg`. diff --git a/README.md b/README.md index cb1c8d6ca..4ef750101 100644 --- a/README.md +++ b/README.md @@ -134,11 +134,21 @@ The following table shows which host systems can currently be used to build Robr   --profile= \   --cert= \ --device= \ -   --org=rs.robius \ +   --org=rs.robius \ --app=robrix \ run-device -p robrix –release ``` +#### Add iOS AppIcon assets to the built `.app` bundle +5. After building the iOS app bundle, compile and apply the AppIcon asset catalog: + ```sh + ./packaging/ios/apply_ios_app_icons.sh \ + ./target/makepad-apple-app/aarch64-apple-ios/release/robrix.app \ + 1 + ``` + * This step adds `Assets.car` and required icon metadata into `Info.plist`. + * If you already signed the app before this step, you must re-sign it afterwards. + # Feature status tracker These are generally sorted in order of priority. If you're interested in helping out with anything here, please reach out via a GitHub issue or on our Robius matrix channel. diff --git a/packaging/Info-iOS.plist b/packaging/Info-iOS.plist new file mode 100644 index 000000000..85966d208 --- /dev/null +++ b/packaging/Info-iOS.plist @@ -0,0 +1,37 @@ + + + + + + + CFBundlePackageType + APPL + + + CFBundleIconName + AppIcon + + + UILaunchScreen + + UIImageName + AppIcon60x60 + UIColorName + LaunchScreenBackground + + + + NSLocationAlwaysAndWhenInUseUsageDescription + Robrix needs permission to share your current location to a Matrix room. + NSLocationWhenInUseUsageDescription + Robrix needs permission to share your current location to a Matrix room. + NSLocationUsageDescription + Robrix needs permission to share your current location to a Matrix room. + NSLocationDefaultAccuracyReduced + + + + ITSAppUsesNonExemptEncryption + + + diff --git a/packaging/ios/README.md b/packaging/ios/README.md new file mode 100644 index 000000000..d1e6078b6 --- /dev/null +++ b/packaging/ios/README.md @@ -0,0 +1,32 @@ +# Robrix iOS Icon Packaging + +This directory contains iOS app icon assets and a helper script for patching a built `.app` bundle. + +## Files + +- `icons/Assets.xcassets/AppIcon.appiconset/` — iOS AppIcon asset catalog files +- `../Info-iOS.plist` — iOS-specific Info.plist keys +- `apply_ios_app_icons.sh` — compiles assets and patches Info.plist + +## Usage + +1. Build Robrix for iOS first: + +```bash +cargo makepad apple ios \ + --org=rs.robius \ + --app=robrix \ + run-device -p robrix --release +``` + +2. Patch the built app bundle to add AppIcon metadata and iOS plist keys: + +```bash +./packaging/ios/apply_ios_app_icons.sh \ + ./target/makepad-apple-app/aarch64-apple-ios/release/robrix.app \ + 1 +``` + +After this, the bundle contains compiled icon assets (`Assets.car`) and required icon plist entries (`CFBundleIcons`, `CFBundleIconName`, etc.). + +If the app was already code-signed, re-sign after this patch step. diff --git a/packaging/ios/apply_ios_app_icons.sh b/packaging/ios/apply_ios_app_icons.sh new file mode 100755 index 000000000..e4f4e280d --- /dev/null +++ b/packaging/ios/apply_ios_app_icons.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 [build-number]" + echo "Example: $0 ./target/makepad-apple-app/aarch64-apple-ios/release/robrix.app 42" + exit 1 +fi + +APP_BUNDLE_PATH="$1" +BUILD_NUMBER="${2:-1}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ASSET_CATALOG_PATH="$REPO_ROOT/packaging/ios/icons/Assets.xcassets" +IOS_INFO_PLIST_PATCH="$REPO_ROOT/packaging/Info-iOS.plist" +TARGET_INFO_PLIST="$APP_BUNDLE_PATH/Info.plist" +ASSET_INFO_PLIST="/tmp/robrix-AssetInfo.plist" + +if [[ ! -d "$APP_BUNDLE_PATH" ]]; then + echo "Error: app bundle not found: $APP_BUNDLE_PATH" + exit 1 +fi + +if [[ ! -f "$TARGET_INFO_PLIST" ]]; then + echo "Error: Info.plist not found: $TARGET_INFO_PLIST" + exit 1 +fi + +if [[ ! -d "$ASSET_CATALOG_PATH" ]]; then + echo "Error: asset catalog not found: $ASSET_CATALOG_PATH" + exit 1 +fi + +if [[ ! -f "$IOS_INFO_PLIST_PATCH" ]]; then + echo "Error: iOS plist patch file not found: $IOS_INFO_PLIST_PATCH" + exit 1 +fi + +echo "Compiling iOS asset catalog..." +xcrun actool "$ASSET_CATALOG_PATH" \ + --compile "$APP_BUNDLE_PATH" \ + --platform iphoneos \ + --minimum-deployment-target 14.0 \ + --app-icon AppIcon \ + --output-partial-info-plist "$ASSET_INFO_PLIST" + +echo "Merging asset metadata into Info.plist..." +/usr/libexec/PlistBuddy -c "Merge $ASSET_INFO_PLIST" "$TARGET_INFO_PLIST" + +echo "Applying iOS-specific plist keys..." +/usr/libexec/PlistBuddy -c "Merge $IOS_INFO_PLIST_PATCH" "$TARGET_INFO_PLIST" + +APP_VERSION="$(awk ' + /^\[package\]$/ { in_package=1; next } + /^\[/ { in_package=0 } + in_package && $1 == "version" { + gsub(/"/, "", $3); + print $3; + exit; + } +' "$REPO_ROOT/Cargo.toml" | sed 's/-.*$//')" + +if [[ -z "$APP_VERSION" ]]; then + APP_VERSION="0.0.1" +fi + +echo "Setting version keys: CFBundleShortVersionString=$APP_VERSION, CFBundleVersion=$BUILD_NUMBER" +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $APP_VERSION" "$TARGET_INFO_PLIST" +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$TARGET_INFO_PLIST" + +echo "Done. App icon assets were compiled into $APP_BUNDLE_PATH" +echo "Note: if the app was already signed, re-sign it after this step." diff --git a/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png b/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..598abaa0ee3d251ae7a4afe1650c57ba6ba5c3e9 GIT binary patch literal 61598 zcmdpehdYxg zY~TCz`TZ5YcaQhOdFb(ao#(ip_kG>hbv+BarzU@v{31DmAZHa7Fq#NL3?GRRQbPE# z=3mf#sR7{y)ZOzL6ytVO7ZuWlBtXaRN?hD4lu}cH*KdJMS%V{lsO?+UoWaMfyd&}p- z`3dPObj93{@4d^6`F1-ZE!gRO1;5MMDe{OZ4i^g@&6-I*wYhumEl#Bb4sVKmvLbe3 zK&8}RQl2<*l6~gqFLC1zs`*1^bz73YgmQ8vqdg&};7XeXVY857+JCY{&99L8&y688 zT`1Xfawfg&6{4s9k}Hi>2#b9%ek65Arf}`>kEgv;=#c3&laTB9&KddAwE=c|w`Vmx z9~R_{?zoWOE6{&0pW3HvrpEX{LoJo7o=S5kBud=%m20#&g<;~8eX502R}!U$?EMFK zEFw&39A1&m(EK-b!8$U_Wyn`~G%2;8)B9q?llyLq?6HFruZ@K6#gt!hDtKj9K;t^dXYSMDxIKhX;sdZ$ejE?1X$KbMX=lp7%{|+3O6U8Sf zgt3tj%Ao)KjTy=9|9vQX%JRPt|G&Sc$J!JoDkXgB#O!#*o{fWp zZFbeKxi=QWu7HX>(fcu7su;TJM}{Eiv#?f;f5=+gNOeeasI1nkNUhe~NHd3Nbei;7 z^{u-*)XN?+KPP{O(o&++q>f=*;OW=lL2D|fC>tmkD9};Bfk)6(_?IN^96dPMO}e(( zSR=felws`t=x{UZgY@bd7pBkQ9#^~iS;c18-ki{Ny^%W4S(B}gl9OF}F-4>(Slkz{ zU9M3f89tlsc|BZ<^%4o)^n2vBAcF8~;CI`>PG`-&izbdgSI};I|ukO)?cbcSpxREECyZ()XE2wz4@G zA7u5k=>8KXUFzS2sa;pkbZ2N3TQ5~W#h_?oLWvbw*<;vV2mjEd)m+x?X<`>gwUSK# zxfRP88t3aI#r)-&w#9TqCX^Md)IY| z8VBR<8V+KMOQrq(Yo+?8b?BoKon4&8>Eq$~+hhVceVJUD(NlY3sm%WG#+hhUv9#p5 zI`N8OkZaBCW#Q#zSHxyD)^HJ3<|9T}mvXFQ@=lZ;$+(eqyF>Cdl6I+;C8~z97JuFL zl~=x^5rxEIsC5sS2iIq==j?o4@4>w4`#UToo%zOR`qR~ZYkw^QWYUHOUOOaYMS5rT zV0^oF)$8M*r91Yk7hSq5aK%;j9hYsyiM^$KN#xXyKIANH=|K7@YZBx3}#4)W<)#U^BQSxlyhs&#HUC^@4 zuPI+!SzFUgs80MM_Z2>D<|B?_3xB4Nqmt7Ryu>A3-1mf*IXo`TupxHFrq{uP*1VJ- zRyTnA7J~38u#^x{)iIxv+3@>bWt3YrUO6~Adw2Agi1nA@Na;ol1Bqi;v8eY+UQa0J zM#l%-pI^JLY`H!1=Wj7ieAtgE$3+Y?x0Y)N*Vng70X4fp2z*c z#AYh>_DV>|(finigMkRyGS(k= zQ%%)2f_7^{t)|s`1V4`BYB@#NZSTrKa8*89HQ4+%B4>lcIC0GEvJ))9N%1uQH%JrS z%>s{)Dn9x7d?MQkxqfELKMr2wp29$XdA~TBbzJtn z@s(XHmyk@#^_hN>8qm)HdNE4r=&qQh=c2mdLxM;Bu+v^e~vT-E& zxUQYSh>?c1T8SwluH0@%DK(>eBOA_iR+P4JjwWj#JN+F5sTBM7Dowi2ckD}qIz?68 z>aZ&Y<+jTO9`;?hI+tnr6%p0Flj`lD#(j@lEwZ$lU2kEYrZ(*i~8kbj@4SjNo`GmK( zE9)#r4hDE7MqTQsZZU{EzH8PzZdj2g3Fl_D&bGZ7j{Ca)TTDD9H!pU}HjC=Z_e`;_ zd-xr2$0sZ)VUCTCs9}DpI)Agy{>r@y9+~1chFpV+k{OnI53_SpVNR|sm|mp}weqCi zrSz_@`K{!EgXuRf1@8(Y$fUt7c*mL#%*_^_t{M>%RdH!euF3U%u$=1A`BlvSwtnHw z{8c()eNu6HxlIkJysSE>Cqg}(6*sfX;|y0L(GHN$cAx$mN{rLM1X^;o9*2vp`YFV$ zo87&*p*worNA%{`7csm3=h$OVK`ENtJ9pO=GwM_59p}DqXDq8vMQJOAhAKR2a*2gN zka8eAjd-#JLG0hLFa6lLU0Ky%2>~`*K~-$LSz_G1pWSmZ{1`(^NF6EVl^aqucNAGE zof~JV=&h93`eB^@3sa8Bff512L4}X{hS0ENF@A&DOdL+d=Uu)Gm%M3zKeo5T(w`kP4a}*SUO%O!=3>_4CQ})mT6y#^@p12He0KxobJKx-_26@|0rV zr6ACE6*pt&#p&dR7|tMT?f8?bFPgcj>kY=xdTdpXUADpm`}dW_v=D zJ%&b>EQZ{PMa`sZLv(YhF3ZI*uSPqbT;Qh1h1XC^qW^=5+``a?4yHqxUF$W$rd?I_ z&A~pZT=umu!m$&-KQ`+07G?zGeYvl2_(<>(Q!rkH*hfK&cl?5Vf^!^N*@n4wxao&6xo(}UK-DVQHwGQ$s} zYbrS@z3!*biNwVx`jIOa%NOoX9Pkh~jH^e{uDAFcTD%DUmb0YPuk+BKu_1M4^-sp& z=3zybg#+d3fDCf{IH-9yxpsBtE*U^<(5LM?&aAZ;cuS3#k`|GCjBfQ$h^ksOo)D*T z?S31GyS%Db=VQCvB$lGl6R#;IdmeGW2(e{$u-PHK>XS$dj?OBHF+m3_qdGF z*I19L=z_O8gMmNqQRT;0|1W``=8U@!Y6gA@br!hjtX=KO==)nQw!)J@k)jaRO@ui3 z;8&xPE_9V)Q_k4eB*|2}f4kDaxn$LtDaexJcF@b+F@sCYe_xUc6ay%^nLdv%#os9Z+b*eLmP7>rx&MK7Kq}p+lOO63R|i^lMTtLWXIY zw!y~4-Mznt0qas#ylPaweFE)OSZ8~nhNOzjV(}<5i|!%GFKuv z5a)qCxGlavYP*Xy9{X5%CQ~F;0yz$K>elCdApiZ2(}w%FSKfSW;tXfSio%s=p=E`{ z2*)|W3kbit%H+YoefzW*sz+EYQ~OlOTqfp4Wehv<-H(mT^r6Ya??f3H%gz1%EG6XL zOtZFe_10dL)<|cC^!+^;CWj}V5A5 zX{)^YP+ojLw8LGW9 zbYmXjQ?iMx(Q(?}NONHHpYiK(&8D+PGPiEH5}-nrU42QR5w>Ou4ha7kbLHt)hdV5@ zF>8ThsB7XrId*D)x8A5xaeRM7$NfZ`z)bny0SkAUY9RluX>rioE z*+u0bJS}R=i{W^{GgF~C_jBo$q}D3!mnxyOXp7q|!M$$@%b^b?)lp(~b_LQv9a;zJ>Fzh?B2C-U*CaHaH?~~u8h02)8!|`4jcNSSAeSV9 zRm8`%h$%~TzsRe>8F{NcMV)a$>_m-Vun4g_Mt{FKQPeMiJPQ*Vg>M#R)J)XM8(gbP zZT46r$WH|n6XSPMD9Y>n;d?g^tPd>0qk__fh0$u0_36LI?4;6t{1-0y-(#SRq_7{q zCNa8MLT27G5+@-Y%YG2|;I16m%|4`T7{8XCO&I&GF~Q}D_TLSWA)+JJ!&UVj*{G$j zE@Tu}lEtEx31U$BFO9Fj;Zn>n`L-cLKStqovoPzt0Bj@=1jMAAy{!J`O3Vk3$f{oh z%<5l}DUdXL zP-1_LI9sZkbhOr9Ic;nMG7tGRYGwB@Z zzZ4qyY34D7MG_!oWcYc|Hwh*+m5MjaFP-guq91khy8PgXd(UuL%y#Qf=E@7VfeO&* zeC_P_J=N#y8EBUp7Jgl}r@8`hq0aqm^C`ik96mBkf^}9)L;$jHTS;fG+9jqlc~AVe zOyVBkSfemM_@Bv zRPVnpc~qfFH^AV>PR()DSozUpj3y{_AA0!)#burTBCo;VA2`4Msye4%F+6cJkn1MB z?!YKA>01vQQ?IISLTpx-JYv}VmO?idGJX*Gh`GqsERPI>QBjFOO-~b2e)&#@-xSgJ zhskPlS){mAh?E|FDCI4f%&M7tA!TOEfXK6WPU;x_j0S<7*z`=}t7bB5jm1nC69f*B z5RW;GLYwm~m1j2-Cm;LZdN>6)IxK?#S-X#~tlgR{7d<`NE1(?0zSKd|-N_Uv95=AG z=3p($CHQasD`L&2Df>WkC2H!9lk@$>8xqp5W=gHTG_g13nXG*(4PxD za4H8@#z$D8!-A3(gBxo%|LZ52{Ky}}mil4}%4rI{|MLHUXP=G|#RcnnA$X%*5@{mnvoK zCRR(P!n(goVs~0%EJvH?0o$oAs?z~7q}OgCYX`+%9@*HEiq4tb`T-HZ!X*OH=^t28 z)FVd(nFG^kqENAfg~wUZ?B9Q50EX`gNv2spM;n_t@*{@=_>Ji$!(^s}wN3JgcDDA< zGecp1xb?#jJw#1n{oshe@6~F?c^2MHZ14ET*|-9!*!kxfD_q3wPuP1>;$s!qzMc-4 zI0aBziV6Msy^E--PgOp&Z!?vEK0S)_YXSW+E>f(i5VJ7{u@;09#<3YXuZ-s2EbfJE z$=zPz`kKsw>@xizBt{w{MMb!ES$V~LitUcG8IS82$8AS-Rtr%7$`SF*0F= znXe4(`&VoLt3lS{)bYy6Z(TkP@sc`4s_Y-oDOgW%nbiXRcel$S#c0m^&n1H!TlHkMQ)9mNY71Mp(mU=_-5QzKb0lW%3=;*h3d+L?&Pn0W!xS1Ezmn)Jwb~10|4vPWAk?so zWpM&M-wv$i&ZXDcjPeyO$h7^Q$u)2tc;Z+slX#4v&7TwsYS78f9A&xSivIenrX*%a zmuxqQ4byY>D#;C9I#X7Te~U{$E8rRRf{o2;e1*CCY`I4rEWgU>lD+d3-w_!hrx)}el&>pJ5ik;lN=zBjB^6l0*wIbvg_lWJ4o2;R4 z=X@s)7_dI#1%Q2Uezc#tf~7U$mGhsqB(nr8pQVFdrf6m4(fkEj%Y(t0z$GX7W5b%)|bS*JoOniE7aC<+pvJMs=x7Y7FEd z)!y-PYh5|{423&{!cSCnC6U(kvhh zm~=BuGPdU5B?3?fKU7v(jR3lSm(MJ2FU!LAS@|N8X`(M9z#=y56B26MS+$7uG0e+z z^VZChr^%S^;}1{_QEf!f*6F(*9Xh1SU4EJgCzd}BvPxDMda1?*Nq+SsYY_@;S1`Po z>oYx=20d4RjgB)JmSJka=U=fzv0ZL#0pM%)nSVXp1(4qO|7efn%90hsUilgsM9Mz% z+p`}uk+mN?0&V|6f9OAQ|Mp2dtXp-a&O1>8Nu&FaYqLrKQS7Ri%EAgbgxu;A{Md9hXwggN%(%1u;~mc| zDgXlbG)Hzx4KX?o3BpLb-MpuI5xZ%p>*n%@GVrI2g__Yac{6p4-E>X`xZx4KnJhUX^my!q^M#_a09 zZJ77LsT%v6Br)sF%;ft0ap>L?+;zXa82yn0ad3(rM8|R^K$0Efkw~7WFxc_y@x6k_ z+1auLmH3J)auzMjdZyOYeJ9thp{TICaHG7s)-qyPA2+Q-hQv)dWd?<{q1qphM$U%Drt1lQ9%i?5)F0*Bww$K*UTEz)u*jQi?Sms zS*;92Y^)M^SWD#eL^f}(_b?+%EY8}6=W`eVk_P-p0DY%`9&3>o?%N0hcGF4cxFNf# zKQ}+fgJJ9@KAtf?zdp^U4Az>u{IA|rR9ZAFjKyG9!={XR+Hx?A$XXCY)S9e7PvgN= z?2!|`5id=ftrUm`>S{GKR3GJRft2}1DC6_}w>Rm!57lXz6}N_k>R!$`&ShvxM6M4J zdA@_Wcv2HOF?>2>-d}U@^@(l*WLl`@J_8Z8%+WhZ7~4I90IYQ#EekPqKWjXz6hDYV zkm-iS+wY%hkPMg9bc^pJoKX*;hej-b{`Jh`?qp@vBP(NH13giY7*cfR?y?eF(|GU705o)Y00K(eBtZ5dD5Epat@gZ( zcQWD_t~iE`h$2g8EROD>EwbuycUB!gE`Q;uj;U1cx=0!2B@%AI`T;e>c4qAm;&hGi znwU>HmQMuVcVuHBb(aVzcZICSs|Vs3kR?pC=xJ0;qVx4|%9iizm{*GB00=bkQQxRSC-5Dn>~tlZb%GQi5D%>7H1J;*#- zW1W)Clgi9dcGAUJ`k@LhTaLK`Y{&_a#v9hj5J|s;CPeykfaVqXs8VBw6@G+kU0_Bg zeUiEOeXN1QPLXf#^35;omGI%lr@c%uwe=xl&s>1oYiIa1Sawt{uhM7Kjfg-s_S24f zlx+Q79R1n5jsbd;;ZK79<@{oj{j!1Fju zwr;6Q+b*M(%!&B^9FT4~mVpxU$LT2nA|-wMO)v^$`0c5pGi&<(4r*5M4YbKr*Qn zX~|v#)9l9EQ#$vhMs6KCjzrrl#MBhB(N`LKq4Z4t<#(Alknb9QINFXqf-3A#P>3y` z|K*u09JPH4@eGlIj)Tmc_+jAnp&NKkKDoPK zW8&^p$F%*J5BLOJaX!ct-zD>Yx~kL4%ko!WC&)Yl`CGl^ zvHts&C_Bg09q-X6T6(6!+tQY8lSDTlrjTw1^aX45XRcl0)w@jq3Ff+zw>)DnMNE%r zNabh;KSUZG@e>gx&Jqc>n!8!r?$3O-zMzl+*suG|O%CkG>y>bt1-?e#SEQwgQ3AQ^ zxuvg&>O_|$G__Dkd&${~oVQ&&i{N+`X*pGKB?> zsSEW2(yN7&XI+NG-ho9)-CTL3^NAOyNvEGExc4Q^fDH*>B1-3T{@Rq^pggWy_PZ>N7-5;GlY&@vPy%=Cp z-#U(rB)|{E7k1__z|zmQGbeXXUi|P4&uX$e&`>yGl8;X1lfqE7a`>sy$`_gGSv>M> zO&J^FtxIjbJmZx~%Dth%zP@&P2oeuM9b;J{JMV@2F|z*h6;)x6o?G2Qh84%G6VztJ z;dYS1OO`AN;O(x=tr-2&IdoQaYo%l7$9Tr+@@09XItXAv{<|f#wo+*)bG^NLVZ(6r z*A46M!-05w)5!ii^3~<6`vt}^Bj_*Br~b_F`rqIKT5dW_?Vb2}cPVYeO#TLA$W2~x zpTX1@s@qMrTUN$n<+*c5(?v2Sv>u|^zdb)`!5>#Al?{x{ut@^q9GMgmRZ?Yd; ze-L!-bD0)Y1W9_d^!wRDJhrHRTUe-CKB>}u&_>c8NK7U>N(D%U98)2{VXIUy=H$vg zVDl4*_lB1ay44clPT)Fv3l86xtQaYGXp*wUdR_q{ z_fzY(QETneShxpOBos*^`@vcx)lD>rsnfDN*ND3)pE5y-8**wxJ^Ii&sCuL*T-_&x zD>kTbA0Fl)X1ln${ly0W>FD`xQnp-$P*@bD<;ZEvFuyD9rr+Oh+WCp~oDwrU~KM#oYZe#-jsU&)y1-V5gMxfLa2ucD{FC!m5+|oj)|Tf0VitSpK3b zk@d{e35GAdJZ*0x1UApQBlyL&73VeZ_<*f1YQOr=>Vc=Co=%36EyKx^Bt}Q--HeGZ zhjGL;J2`9S@#PGIwWGNA$cg6nGh~zrmIW)IVdmbVLrO6K9*pR=jfwWA{z_mhCZ{9_mcbo!ndf;Vy$Cv+-)CO69=86l_O)79^|W ztU#LFn#S+aR zOt7y-SshEov*o)eD2^ZON-9Axhl&I=RK0NFcx1D2;8{?N*QB8#$-t?Sh71exiF*{= z{Z9r^-2&BqeIWn~-0=|Q*X84B^PBeLzPbtieDcgm*^X)6{gUXRjZc3`fYhK5i&Q17 zdg(I_P1Qu(i;tUKDMDk+txWmBEMc#y`SUK4^c*C~LulcH066Z{gBqTO2IC)^I%7;;Y!#v|vatb$Pd=N90gIL@Ps zB|Ng3J|2idi@I1{Z8f4nYKtR50%Sg;#VK2~pHGIJeV*xobAWcJHE!#1?9z*A*@Dh` zm4FE%IwV;^<$>54-%mdWPP+d)iFS7F6?!qC!zwlv5G@Q}Pocsd8jlJC{u|s2eSg!u z)~!uI86t_pP+VXgCXPD|Z;-NDdoci-Ol0LzW&X|LwszI>KMo9ptczeu=+J0ykX&LD>h$ zJaOhtBOq=8={#!dS&ThB9v#F7x{&@p5zD(;T=p2JIE=U=Jt1uHyT;sHMqk<+1}ZfycRlxW2aHE!Vaj7~waxx%k; z;Ys(N-(#wXy-z$R8j(W|5(N!X@7t+{~ya8^;I13Pilb@k_m6oHj z#Oat7ru53E#_+Kb%}zEFXyyWYn)Y2#lr=)u)2x%Te?I&DVeR$O51l`J#>E1dNDc^* zMq-0*eX`paUog8({8u?zaAh$Sb((eSNi|;-MZPAtW$hoc8vw`C?E0&q&C$^DGf4ur zw_e~y7iFyn&?n%gq`kJvbmB3 zW1xNIqa;K;dl)x~?154%U0g`C1Mh?N=ymZ?LGTjI6PdW@UPaj_f!a`RfdVl}4u3hL<6&zr4fDX-3UWn@96YU9}|$vWW;IX_^q2eW+#nFcqvpImM1??)_e^ z{~8oz4ITLHxmlLeL8<`wFKPWJY+8-Z#H=FIe zwVM4qC)baE{BMm095_%4IK$Xjo%3h2<~_5ZJvpwLPyhLMT&(L2V|2$Ip0mfK_%}R; z_M)msM08{p97+xg?W$lI{QYT$1P>4p^{?nER4O#4?UP2ca`x(rTW+3U55Ej$TxQ{E_nBD)w-xe4NViBhO%1o4e%-xUXM_=+sVs1$Wy~9 z#L{YhvCV%rK|zP?-qB6>G3x|_rzoB#)G?m0O}k>&v*Ar#GaX)j{0xXzuaRlzLLvj#-_8t>ZI zi2pF5m{9H<6Qy$8ixnd;C)Re{T_@gWpN;4}Qu~qFp&{$JF&v&wp_ab^;_sqWd-!rZ^Jn`uoJ?(ZogI#?v5)M@xTAG?Q z%-Zch&Z)5H<9vob-KlBV10Ca|I!2R+slUHgncxdE;#qR3yVpLM!A6g9jNW>_&eCLa zB~!SSrcN~6Lyjj2)bU9@$T$t1=;$Y(21KAmk|}HluCqH;c(o1n>+2jzp+7_f{q*`z zRaB0q+Eroj1pyovI?(C??U5w*(`5l3T0d%74tv8{ow>yHI>$~?w{0+eR0CP5<*klS zTfo+>Hasar@vJ5l!LGf>(?sJL1Al-2k0ufr?_Wd1hLn1lb>`^+gW;>A2ikbkNT$cO z*g7A-=NTviagm~@6@VmD<$JSDz>65*CFAL7ta;@hLz9@EYCG)#KqC&_UqrU!=$Omg1`YZjflAMSz zf{*WtjS(mU|FO#gqk@7Mfco|JS&=^UO-;Uq2TW!^$=Kelt2y5J)KlpZk)qS%RIHbQ zmerQX09imu`3%_z+{{=K3wlfby97@{@+~gF#yF%&S|2&^@ezwaX@yDxIQ;A;x%foM z96@*Puy{tMa(S;^fm365R3KtTvfcsK(J^1k`hvw1fhAx41m!LfRx#^{9sGuTCGPq3 zaqvc|xaih#B0jg*)t*1i_e_kDGmJJoSe9zd{W-kiuh)qxtUw>M7!OD9KU_HY+Q%50 zLag`)9~bTjhco6nadU^tH85Aee$5Egyqu9oOQX$wKXv}*^Gu^)U^>xR(@UL&f(%`N z-l%ntK{MmmY(LZ-oRFpS(duVV01fU&HejKeF7)#y(2Q_`HC| zlD2=8vSZl$WKz|cza1nFXHUZ}AYVC8J2;&2v|WDB09*@ah2K>^ zLMPsqcui`u2=Xy&6yl(sxECV~Gf2w?vqe32!Ctj|Y3viUD*Lbljkr^ICH$eRt#}{n znLD3GL_izD#CF});J9**YV+PHIuY?tgh!I3d#~X`P>FNSATXi_sBAB#EXxV z$w9KL_28E!n)2wQhlwXzc|NRHLF&DpBgl&8{$p**E@c#yB6@If(qjQnBN^q^4QVsS z&%i)_r1B73-|dE82RRJCh3R0&@U~pffd=8-Kl=l(|5ZM<`HGkNJB~(g5tV|(W{^dJ z0)XSD0V+4ssfp8mPb%>%)>ul-b}s8`f0fXIdPe>~_~}W zGggVWGvX4SvtE1JOikOv40@d@)R9=2oWPj)M)7GcCXCAyibIe{IwHBRcjW;#1^Al zWZ?NjS~M9OFqN+pQZ;&hvx7_8pGN|ui93pdR}wy}O)yarH0rjrG*1F)4RRZ3>dLAv z6Ul+N4wX-K_?4TB5IuMg{WVNp*QWHr@KrlM5!GmRZ@}8|m?>sbt$@ml-M{!tC45Fx z{Ubt7iYE@L^FziN4u#?AM!xf?TmzSJbhM-4ua7YLHQ$L32RB-IB=UP*?A))bLbU>}Rw-7xXMfm4 zU#`&-);{=FONSNd;3(+EyN=nvC+;>M3`m*argQN@^GXxC9)&jj&}OF%wQ;Xf5X)Ym z6%DyA#Jse<6!Pj8^nk>(H3LtK^j;0ja9ZtQwfMvq#Zho*r}4RoDq{2~j4RgJgO*8k z3z`Ek&-N~1vcRes@%bW^AN3C6@c!1X(vOiM@ zPL7`HmGHg6cbp@+t(n^SmMcz^EO3WlmoAi2G{v&|pB)dprtQ_$W0i$nUe@gY0`>r3 zALDMKS*fn2O+B&= zUp*<$9>YSd7^74r84lFYNx9^aw=Sbftg(_D;qvj?8%WBOt+fkdUviJ%G9E#m%gsMm zBrKIV{$i9K9v?WxX2HRBrNx1kDSVr(80sRJq+ZQDM@tv?frg}Vo)}4M zz~nX2_WMI+el4fN00c|G*$Gt~J=}4s(|97J2>zD}HT68G$QcS1vOiINeDJ6?`g3eK zuz4q!woV4i{JSr{$iyF#UDn&|VYR5Lc)*zbyeGVf^#oLz_xCmXa1L9f6_M0sQY5qH zmej^?c#!KkSZHPP+l;Vxrf>L(9*n@FW-h)BV>0N6>Wr|)+{N8F!+X3kh2=+1znapS zJ#$frl!p%KN0kE&@%`1O$IoYMzZ(ReJ%9(-q-9CvgqY<)amIyr6&g6nE`nJ32F_xp zI5E;4V^bpg$!-L$O@MR)?RAM=7^&|GJSX|<)7Yy}8g3hHA$qx*Y*4NzIwc+de6h>u zlGA_Fv>DVr;G+HPadQQ8LiUOMkAqJauNlaL^8tZ@Wz4xyAkED#0kr_02^_R0AjFrJ(%y2AG61R}xx{dQ+PCXCi6YxhawQPq5A_eOKr zI9jeoJGft__B=c&-CjGn1laPn@V!IpZ!}Mg5XTjeX7B)~@^AoSAHvkcv+9r4z2Nku&sKp^sl zuQ|mnc;;96>n`uXz40s%mDfabz`P4&pF=&d&1KHyKb-r7Kbm2f$si87AUyW zmOwR_x^MdYDQKM1_IFto{@@dydqgOPT}~cvC`iaX(N?K5a#gdHX8vNRG%`d7o(>jd z_lU6f$DSkj)Nh@Gz@ZQcJo z@C>w7iVo$Rx(Z!?9i1eVBQAX!v31CEW(RSwH2J7jw|!7_)#Y%qiEx8cTj{nwRbyfZ zGD&rTxB5UYRmV|Q^jmr@M95P7`wUZiqrIimLDhBiF$G$^qhB~wj?p$hu z6$j1iuonm7&CNcKW?azPL%!O3;w$Q>;`lGm;=2a-futQ-E>>LY_eHN`d`-gBB=O;z z+!lfTGAgIR_DMqWFs*FsN71-&r*YCLiFb3fr`Vy8Ti&X+-tg&TXG-Gwi9y85m6N`9NnM`3JKJ87NyuhK{F zlGr>k4H2>6e24F3k{yCVgF_XT!uw({4QU?7?R|a9cHkc`i8WL>vlkfl6jtzYV$!8l zqojqLgRg^(A7u@~8DfNJy<)`Pi7=nmGVeRPP0P@!vY7GI^A?QF!8dyJ=ou;>1%l+` zSVq+?JLvA{9cRD5ji8sz$@k$PQr;lDt-74Fk~XThFe5+UbB_8H8#Pz)pF`t!_8OEn zpMUx_xPIefCTr#rYZ8O|X<)62NZQdPy*85`ZqmNlaBhskz)v>MCreCE(|nsM?hHX= z@9##Pzj~KOW}fFZET#TbzVfx3tclhj+$bgzqe)>U_cOeR@@Vg#`yHLf8K)RZPt+XX zwaC!J0rUw5Zj)BI^$FEfbm`x97 zTiSYtB`0W%bB;=4PVpH``yjWdVrIP_tR*d4NqUuYgLS!~BscMazix5Y!OUepH~+Uc zHiADu*=~HX>Dgw(Is*ZlK@UPR{tiv7w3Ae1zl?)DgjYPadE<`HiS?+F)40r73Uf4$ z98;`tF$TxI8s@-NfgagEc6}@ONNY#nxl;=DZ&=?(7kxh*nXeTIP~Skupe#7TyYc|N9~)-A8W^q@%hb`pg9}MQ!hnf$gp%#e^{dxv z#^2i)un-@0Z8zwbp624*pmsZL;jL&*L@g@I@am$#LWqbtS}U|n?mPgzVNa zT3b@YFHP9ot>@Y=;p}>5+?*6qP;w|8wZ_lTPjp&We{%f#`@N<5Tdbfq9V6RDxPgTh zeqDRb*>1l9gMvjq`ITtS{o-0rxU&@J5yc*MPV;yr$^>kWFkJCwWvY%NhGjGCucMQQ zlcOL-Vj7HKsPPLyXAKX7u9d{Py&n?{cYA-$Ptx^#b%YWXU@H0uXOo3#8`F5Alm|yF zM@TXw7w-47M7g9dpOQQeYdcVBJ`-d7X)uvU9n&pk&^T3dhv=u~yrG-!pT5}ig=bdN zdJ)X7`CXoMv;m|r*hxJy@K>@!r_2`!<{T}E-b;K7WWc41%bKRkC>pb^j+d$;&R*N8<|HZY(A&Go>&C1 ze~*uRQ{v@q(B;;=;rH&z4?YqB)a%G$GRmpHQCDF8Ln6c)81@|YtP6B?u~z;()qn(* z?J6b1KhVB=8G&POq+&&Og}Wa64;M?lSN$87>2h$wXRa9-kABB$l|vdokTp7Ye5s*Q z@{F}?Lb+lxaaaJ{bOYCdN=r)%docK{O=+2X_ba+gy574;NWR`a77G*jjh>_j&>%bz zJpSIu7;g9^i`pEiGgt1W>WM2opYgj;33!LamUd^Ut)zmdap_Qt>upAKB!mj z>|w#sl{4k2Ep9cq=Yb=P(^UfgU61&8C}8kCX<(F@9`_>;->#;$88tV>{nC8;G)4Qr zZ{eR_zlm|WO2hYFY0FyHUn0W&*~4&b$Mhvdd@&68{sp4~3{gwG_QBNdy;w%YEznV* z81@cliosojMlAppn?=8yQWs0bM&F|5d>jKK_sM+5a|XfDG3Q-h@qvsyFt1oBn0X1 z?s)e+-|P2Y=ReQId}{AI*1gu+TH?5I*q2>iyHDniK|=!h>TFE^AFkA(^k_Z5hbY&* znv=?w6B96L>8~-gpfYp-pUQpsDDMCnRnKI9n;DGSlG&zjt7 zHIY1+naBpU?&@uM>}S?GtT2A2yCW0zne;^b< zH#8Bh`7H4cs0P0vI?uuFng9K*o}TZ2Ar?~TLq~U{=Vve_rJ13XX(PLui5vC%%~R2I zWq(*-*I+R6OLrO5nQ2HV!d_-i#HfLmskrGRH^W0>Ac9FGae{=qO(O`h*n^nTe8@39 zKs>^#R-$^<|4!q{ncP}7qP0+C{cc;o)q&>ehTr?a6?<9W5CB=cA}_rU(Uy1ThjE&8 zv1_}$$(601r)I2F1s!JF2`G4bnl@^%m57p`(OSW)gub6Oqx6S(zLGI;ju+kuslqS6 z_#0Y5@HfD^QxXp=0OZa?U9r@nc#J6dSau{UXz;u1$?%Rl=<4*cC*snY(7Ct3del0! zdM==Jm|A0`Gk872Mo7-n%=VE!M|0IKLKFLP?XE?Tyq~U&?pWw0*o>(>^Fp%<7^7es zI0+)=jyr(umaC{$P(kB(8??!gZtYIVas#gFI9e_BsH=L!zd!o%D8zz~kmkK+5D z97|V2NET|AH#99vNQf>5?s9QPRk~cgKBf`?9m=5X^hrnraQ(V^R(5BfK7{Kj>Q`#M zZ|IgbW$RX=DCUp{JdlOyGO7q04;1ve8maQ`iwtI|zuf%FBmax=XTbZ7jF+tjKxTqZ zh~-Wb0emSmD{kE~B0qc4je^Qt5=4yepcd&#UQ!)-b~FEhsocJE-{sWpD)P)T>ZT zLjtf3c@FCHrQQWDB5E7~@m-pRh~=3S8W~y?&g!n!=FnBOczP@I1e!;?kuHB*=5qj~ z4qVLMna7I6Z32dq#1PUv&Lf1`iSw3lShRhm!?b5eKME%bQVQ0i=iTLMY)Ce z94Id6<^y`Kd@d*wKsl>)dL&IPA5B{5^YsM;d2cE?*wzHM5l0X zo|~I`cjg9lv0W{3)`T`M(zwXGgRhbrzz{8kL0g3jMh&zh54uhdf48W@o!Lw|9s{rh z+4oapr6wiz5J70WL&8$zxcgr5;+7-m`ur+BD3H($P8!GS^I(GvEO_@f<-H^7XOL_lW}J%`jS#m1u91gtd(rxo(;= z!5*Y+`k0YIiDLD`Hzv0?G>a%Iv-h0UO_1c;yS(U@&d5@EYIJ^GMZZjN=qmrbAZ>_2vk7>9C~(fbtA7JG|oJ!wMkPy~q`Rdk3jp zAixEvkJQfp=q8W9E`TDqr@PTRs{WEOvgdJEG2%n4jKQ$PV!}V~wXeDo+f Y|qS) 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 literal 0 HcmV?d00001 diff --git a/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon120x120.png b/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon120x120.png new file mode 100644 index 0000000000000000000000000000000000000000..510066ea090695080dd987f85398cbe277e4352e GIT binary patch literal 6387 zcmZ`-1yoeux4sPUqmc&5p+it&XhC8ShEj)+lx`Smke2uf(nv{34Jj?npn!A;NDdv+ zAOj+Tl5c$fcq`v``&ZIIY0%QIF}u3~eB(~`=0hpa)}5R>eD75wfzV5h4yl4g`jRD4-mb$FAPjs^c+Hb*6BNJqidC0^7GLtt!=ePE zud$#1^|%GtEW}%%!7U7)yO*v$-WbX^3jpXcU{Ga!zxe}mrwlWL%1v z{$E}S*9llr6v-1<-6gy8g5PkJ=l4{ErL${Y4Nw zpz@Rps5316VRM~zXOEdd&Kv+o*TU6(d#OdDg*SC*mKHGe#m)61ig;0Rp5L)}6;ax7 zFg@*8m5>q1_yRk`Js`ulxWudsjH=olwf+(IsXbf1(EU*yz60|*gqM;i3{MoAq5&g` z3r3eT7Li6J({LBAMom2s7vxo6$-vY7Y$_!XUYdT3 z78F_Hb1@dyzAwKZzwSxb73LUgxUpkv(o$@;Q#DV8kgeJ1GDo5$HvJZU?O+PsLT=v% z^-1=*Vq#d$N2sv%_>Il4vJ<`i^Yy#T%}|rhvu;cn-zN8iLp-4`=sCi_qKG4%KsFYn zQp>C{Q|Zu_ShWTaN|5?PVd=i9`8YNAV5?<)Ekeo~-cr1aIbmVuws3K@21rArVt5u$ zX>s%Ni2PR7p<|$(>(s-2d+wf8k9$XkOAOA}86AJc@GVNw=DfgL(dL^`Uto{^iBH#y zo}0NLUsIg4sOMa)>oKQWYl`1NMtP~cGi@u(4n&hrB8l+`>KtcA?%I-`F@{q#LnNhT zTp187QKJor8_C%abM?T$Ae8OQ$U~z~0Pl#iPDc~xho%A;c%FB$o=Z+I!B;n~Dz@p$ z$&Z+6FD^T+oL)AG0qf6x=&Lfqace4^t;<|y+-pzvEVC~@=i=~SAFmklM0ekwQ9R7i?6+ zU9Z+y0k&N&ckHfNa|j2p^Oi*&(rY<)i%WE*6*1NkVX8=7MhF-cKzYL`m6iWVJ%^7mFRkBsF$Nz)QRatgvK5IeSa#OBYo0jRqT6JfkV2jIb)qdI$ z3iy3*rH;&)(D2MSLlyp%{C-T7(l)BmcNNo1P5)&LCJiF)DM|+Kp6qpQX=vH+@}*sD-7z|D$7x%-4Crkbj<$*%GA@))ihE zDr--*=%x*wL=v5L^)t?0iph&2!c zRXyC-f-gMfjqIXXAF$gb^^No?By--AdT6q}tD8~m!wps&7(bUI!*DG~0{st*l0%7! z{AqcV!c$X1E!QaBu*V?V1axPPgG!3w#VN3=$PyG(Uoc~A`N~q!VDi{o1b6K zpH989Ze2}v(kJs&-ygqs?>^Qw&w@q}{G?2o52o}o{9VhOXMeQMA z+p){@YU6Qg`wdEW&RkAiU2iwKIrPgXig8AC$-GQ9Dw%%GCy*6%-8)#C*?^cJ=o#M6 zMo=_DMqlv1j-umq07Q>=OxaeJ*g_=wJ~D4O2U4O&=r&vHhv+gWMIp{FR9~)~VvTbH zt}8bjyFYb}l&b9B3BJ}-FpyOFkfN<9#_E#zXf{u$oKC|#;$E%ngjZcDsr@^80v`TG z>&wmdoxYszki{{xt|!}Y+7eBTf%1ttGa`UVkZTB|sZH7(8@h@gGLv}SW^?v{*TO6l zvedI41$1_b#!FmEvUFEzE#;nY=$6fhD44SZ$o0ifee-n|-shv>-tfYmd#w#p#Pv-G z9uirU<(3j^eRL`KL5NOq5%CUGDOp2E-22nVquP3{| z6rlQgj5~jtRY#7nARTENX-LGq3bN&?))1Ev_fLW)hBHb-5ZUdgud>~pV~pB3ouALd z6AN36d*9qKzqwYav(f(Lv$nw_G8PN82D`l%a>xXI6jfL$Xgehi3!09~y^ADY*8A3f?RnYs+S$mT^z{A}A zD8#%hx*|JJQoSlxTXN8AS-?J89FJQRV$I6@yp|&Oivc5415e!Y5Zl_w+|>6=y!mX` zpDaWH;uoxrJJrGROHTVzE9*;4PCBIOr9zVJ@G<` z(2yvFuN52QmE!}1J*EC5!YP)$pYNVvf~GiH>Ps)0u#Uo+wklDo#p@nWF3rqHOo2HE z!WctUmuv|Xc*M|i0$wG_br;L&=#WckZ-Gx0J_G{~J*yXVv>JZ;qGLI;4IVOb6(!SU zt_Q7HGXfrO3YBME5Fd|SShB7go3L-HcW#{rRH{j6r!leBB0}>hq?XwcB=V5jF*JqZ z_4EEW=Cm~wq&9$ap40T95dh{|id%C>M*>QM_1~-Q6ZiHd&6IEgjCzNs;LKn;Rp?tqF z`~A`ZhS5)~W<+ZG7rl(33lldGC+;wQ>>h zh&BwT;Y-~)65_ICg(QRoxE@7pKU55|Ixxtd`Kz)!f9?N0t|M>r=s=ao zcWSc(yaPE!#$ z$@z3*K$C)O!`9c;nEI=@&5y}EN|Au*iFP1za`b!ph2M0lPs%2i9ndEpXGrDg`?Sr7 zuh18~60LYKyGocYEo`(mI3kG$=2>QrK`hJu>Nw1|{0&{wj2jyAe!m5K6>0F|d!H8V zt;O+7Ow>alw7LBfdDHD4T;c(#- z`#4RW`EdBmnyn9f&IOy?EDe^O~kfmlO zgF5jz5Q;)cVh1?U$;Oe3(5FFZ(`(@|XXENaDWfV7P^jvi1tJ=7Xe!T0-GjZ$*Yy+p z7l~}lpf~E*RLWUA@!esL1jyRKni6G7)b0MuGntq#VT;+h^mx#%p~ew~gB1`ES}n2G zStsc8yEH0@nIyFHZ3K;**ujbDtH`2+x3IzIRDc)VG_#V_8!wGl!^1g<>Zvu4=gO@- zx-{M-)E+7`<1!zSM%?qgmyIe}{ut&rgogB4ra)Sd68N=#*CBGM4X>`a(1B%qW(%nr z%coObl$cWv26Gh`c_SoiCcrMcE{3~cAoNVci-S@Rp4s{McKKc^l{W7i?Tc9B7xs3V zA3|C&jAed51<0rd^to0m70v{Jf$IqeUS?%MQUW%uM`t>5gA<$L9F9tA4@~Jk$MJ?U zvQ;#nYrs$EWM1vpqR+>N_gwbb-n|r>`x;Tn9YJAdxHt5r{OfTLn8~<OO z12Z~f`^Tc|8Qbu&70#%^B9eEz{6SC|U%4cqVI;JEta4WhM~(4g5)qdO7& zX10A9N}g08o3Uyz|71mey-b9|T30vu*!H%4s?LzC5bW-)i^ayZ;?j#3zq@mH7D$@Y zy~D~sv>HJ}2Ozq=tIT+^DK>(S^?k?Rk6?0lh|w@=u8l$_r_aJ}7@=TZ)Vgk!Nz2ql z&)>n19zaADth-MR&PGV2H7(KZ`pOr$%(EX<=EJ4$0u)b$NB;;SaV zBqppSC*((&s+tOx-*S1n$Ykt~SWUVJSn+c*^A*=|fNusiWmMI|B&m`LSam=SKN;^4 zhqz@HexlHu79+H1JcU2k7?M>z@A~$M;kQKbuz|NMuC&#;@ z+rr%BEZU}%xi`YOSGl${vRB9;CHXDl`KRsurzvG+~D zFgl*awyd*UHpyq$yq^HPKFh)4Zs%=aSmMDE_+dM%ZTouDR~7eCm^s6I4@P%ZZUH9o@#2YG~>Nhoa-9A{0k=p0nF(!`@p?g32CFX}93>}*jVtgf7an z`F;WmRZ=0V4xY-9UVmiGvyzApuN6~DGJNj_C_Bu15@k0G*O28s$1Pda#~LjAB(g;Q zz;kit8JjoBz&R@4QphSxHS%!dvF+7yc}mu5sL!2%mz8vB^_g7?hxk|@-_g12i-Laa z;r^mS#d_`wteoYNNl;aDCGm>@#aZX2W941V)XA>+DE_=%+&BDejgH0ryU1ypHH_8F@CVT2J(Hsb4;S6%%e+oIB>(*p9X(8j- zBOb`i619b&KbT-P?pXYo&k$pSi{T%Go$NAS*l9V*IvVl@B`#vx955NG4(OD3kfn&o zwj>GhV(Qp7#u?lM7}ne0rJF=={#}=Y@K9=rPI$KQ>|#lF?194N>{T$^A zL72qy+f8zf%=a(LJf$oGttS~1RO1;Ja6)ko?j*rQ{@|j3L8tbW8`RC&Eei5mS-C=# zQ7ODbzbnm{LhY95rh_jtj^^zGyy;VWaH1HQ8g7wAN(zh_>wUYlo9}r)-2=TGt&x8P zt_pFE7@@Y_7OGenf31hV(zIJT4x`9Jx^oM!HHrJkEyEOX8+D}D$2-Btc)RQonObKp z?tdXc?&(!8nbi9_>Jb+W*ZpO zv`(laV=@G;sgKyldAkCk>BNWB=3F1tk)<80C7=qDG^Z#5pfB9QE53z89525 zJEEd;qM{Qv59j_v;O1`YWbgm~3knsl?QjBte@8$%x!HLmt#Bpse*}Suh|21ViONA_ z<;2AP5d3LY$*a2dV-yd)N>%BL3ox9IE`}^)bckgrV?0a{l?jyC^#EirM0NjSDL-nvS?Qa9&V$Z4X z>bH);=dl51BZWXt2R|zeAqh3{8LO zexIlV0zrH6gf=e|*(gfWp-Ja=v-^`1$~pU<<93RgX1-Ix>&yk#J3b>R2g#MPh?$K9 z>cj~)h&5*F#3VjGb(K+;ulRkr?Ca!?`em`iEQ**t2CG&59_OI*ifHEjxT#{M>`AO! zZd|LD@l(_C5fl9JqYUmgGQDGDtRx~A5$8{0n)33TY%>FKOQXHu9IpI4<{71PE@*}F z-6DhSn|#k-uQaDpGsf=wGse6$_uAq}#N0^#mKvRod$Oafy;dgY+AI4@yeQ)W`Df$B z!rgO1-3DN#0B30&8yK7y9*^{KhVp$f0Dz$!236Geo!oiqm!&_JEVnj$;%MP-QC{Bs zm>OT79BS_NO*X?`RT~k*wWOuRC4?@0yhLlnQ2^^sgQ;o{#Kd-UaHH3UET~oBbQGKv zMH{ayoc#7?0#@TNPm`c4244JCQPO-WPL0AO7d?I=hX2uC7?F|SCukTsMVDffetr7OR( z$(9@vEM<_r;GyH$_??9+_*AbgnzTp(&KogB6(QBr0C3$s7GkEYh=Sba~)$OXd*x9ik=UPF6>Nw`^tdzl*uS3#vNR_FfO2n*b zD}SDv-Dzs5W$ex_SR5P7=MZiT&iL5nvcn&Dga}E>@{Hwv|K=Nq79B2%bpI?!kLAqN zB;pK?G>#E6Nuc0Cktg7E!DcZ(ye;geO3^}SZj|ado)rLoOofYj#QmUM^jj!tD7kq9Es{wOsef_m53bx!{?8}f=N+5hq6{Rf^9o#ds?B>1| ziy=G@$qRFjM*bw0$mgK!_+1w39sw*k@|ZOOXWLboQGZ(<++U@#G~-ZYQyP^5$TMe_rsP4{-~qZu+f zXa;KdXV|HsZ$RLJA3Ke*KP|b)6JIz{{R-{TH#;9tnEO-tJ&1)FN5vgZ&Af`c)WM?| z#g1!pg1i%VOz`E6=nvw$u(C+x zuC)d-{Zl9_PZmY}+Oh8PRI-?^?!th0LYc?uYR~Y;%I_7g2?vHdkXVxmyjdjJX@xV! z9nxt@9BRPzG`$@s-?)5_J3-*YhdmgX{Re2xz;KSjF>Mv0I2$3N1Jz&!EQ3SsVRlp5 z%$4j--Hjc@%%*S>>}@hddDv)Q#cg%q4Bq4Xn&z~eg>^`ibM?*nI5SIZ4BQ6Ofz&1U zViS(3o_gi7#sbbgoZ$y!2*lKf4yZYLu3jt9){I8y62|J|LPwzA4b zep+Rf$Id0$tkzNV`etzy~~92sVdRwuU0?tkUP zeK1pQ+N0Q&F2?pY$cDKo(Aw;fUG}L$?JwnM=X(YcYv*AnNJct>TD~k3R$;Gua>HYR zW&!=dDcnW>n6l@Cirw2{J4*;$z-9^B2aVJ^FM6Pc45~XE{nHs%#E5lNuYRSG*R(#} z^)s)h@rRL~>5>V(5%kF(*wt{e?Rbm#7pa>&4o+5#%57%w(s6N_I7V$boJ_)29exmP z!F;y`@$H**{$sS(q@JewxX<^4v%NUcY)0<>K5LPC!W)?BpPvo~KV<{+%jUU-ZaV6; zvH1FH+J6=*oVGrs?ZPtE-+oO?T64ZxFKTWUu9zPAXXJ6Ge%7Jx;Bg@BDs8^|g6axpv}dk$N%i z=FDy`lCkZH37+HwpGO6+gU{AHpH0m@)OOOo#$ySV0D>z+#t$a?iWz zh3ztv3?q{g={s+~AoaR(uUp(gw=v>b_YkOGP~%VHS%1<8u}ZbJ9JHw~@ulACUV-=o zymd%3WNX;En35LfiZM0z(R$hWgJ?!m1_>y<)uaOSl%gG;&B-owfA}_JzM6N?Ntv+f zG2P<7{N<~(ISx`v;`uKsioGn#Id2rHx&{-`iKEopGsfJ_Q}?H*8I$o*X+uwj9qgC8 zla)ClDKu1tSdzVJMD89A`WsnzIks4r<1q4`&)PdY5haIjk9BZ-k=8FISpK~Fw0p(ex*AnP{y296Pgv&NZU zj%Lb<76%2ZQc(R5OxXk8-Azsv06!yBw+00< zTltekS#3fNT_KHcBgIJC=sw=vIMG4xfP3P31Vl5)-6iawT0FaC5)_{+TG$nmDkk5(*lEq4 zLi|umis~+I=-C5vy35x$^Ws@u3JpY@mhQGk4StTJ5OY+$sMdtA=P{P=@lyI>ML&5@ z{AY5>Sl7+#-wn9cYiCoc?8QFf@D zDBc*4BO@oS{ZiYXf}3Sz;OSFpHTQD~kgP5$g|tdfp3k4>l*myko1RAbSvK(moO|`V z!_W5yI6ws8g;L4qpef(}%la#TRM7So5a9UPYKE)p{B zw>X{qd;RH+tbFbG8TE~#TbVkxRFjBf@O^0$`XPLGigSN=y+_+c+6cUSEOTg_(G3Z% z>33B231=*)rt$EsiwcJh>RRpV5Spc;cg)(D=}VJcDHFkwSqZEowXJOT<%qdL$e+s* zAJvZDJsEh3soAXaow}A<@9~-ki}AFWpSg@)&v91?2XXe&D$5LP1=!p$1l?V4u5no# z|FQ5MBfIBV#lGHPd6uZYly{e9xjQA16&{?`oA%@Sck!Uw!#j9Hoa^|()Mgqe`=>t+ zW=6AM5dC51pVit#FHT&utE%L9IH@#gu%{*zmZ2XQNovw%A6x`Tb&r4Go$1 z$GM5w$%?NcuJ;=*_*gfgN|;QUoMxprbcLcVeV_O_6Rq=9_CB!G%T^1VKhC9eZXKKSxS324+Zia}O?DOLn;Lm9E$#Z^ zkzP>;tA%d55QZdv1U4%y!8)HN)bhUkIP{>rS;~}U-BE}BT>e;2lSja2uB;53o*I{| z%G9ZEGr2x(`s6b)hoivb^R^J-%oAlbv#7@J1Si#cz_!jfrpiPk#;e8J0VB2xJ`Cu1 zlWI6NJ^kshcXk)vPd@n(kB^qHKPCLm6SHS_l6CRnKu@|BDt4{~v!8hq(Mv(C%x|QXT=8_X)d9emjat8GlkDdBDBSX@_tA^;$qyicLn&HHA%! z2@H^bccrE?(9fmd-V;4ySZbyBsKbjP79pf^Y5z6+=QSDpr*3F0AClHkcbEqDuKd?% z-is;fY|kVM;*5qj+HC0xrMgMPD!pFzKdzQvyU1Ix%n23ZXKM^6 zH>0v^-10`c#LqW=if{Y*+K;1FAa(T5W@c;Q-+xQH3>1_*M9`Y3glq|_hxUK3e-SC` zuk!$a#Ij9YFsAJ&CbB9hL^!yx=bB$&$OL@@;ZIBSc2$$tMpJf?9Txb!oPIC2E9q_# zUc?;Uw4&R3i2x;X>tlQEtkC^o+}?QR_L=GZEp30Fm|NMX^l;hkdfB>_NC>Q_&dqi$MsE&EN;#BCnnfvUNfH27 zPMqK>Ee2*voy(S2E74bW=K8OG5h+5GD9KrGDkEX!!qA9Ft4i#HKReEaqQE~;nWQ3m z=3rOt*(@V4z}jV0j1YBdZ~M6NX6<~asnCDYx$2C4Ak(cloK`kydi;3){MT^^ZaA&U z(6+C~ThSYHavUNLi60xbEH3n|L8I%JRH1gy%%5WB@t;VTFp80H1YMzrpTnQpVG~*8 ztfP%9XRbrW)4jw$yiI(_O~Y>r3Zc`_RPpy(gimLuI=|%@xNnMAyi_EWJPh-uN%dAW zV==}Y9;w0Ai>6`1RSp>(UGcX2ZwY3NXW0k((XBWFxBVB$W-2W1$gEuUEwvkdNp`+a zT>4tXQFi#_WQt|7)JN5C_)FV-gJzE9)MCW|D*;YuJJvs<*C!#z$%5dQEBTwsab&Fd zN#T$h0fw@#MOAywCQY_*2M;OQNA__4yW zmg=8l3ztpbqgO&{x}lFWzJ0AGi;7XF;s~)EAoIT{3VHM3HEp)p+1LwA;Tbg$-exE7 zYSQ%Z_~cqpK!Bf=dOuHvCj|6T5$#mBST?&}4QxGPz+pvM?Tp@;)7qxjtC;@$&etTI z?P)?;d?@{`s$4rt6H2$-Fa%8Xhs7)1t_L1;pu&ObreKa_RkN-{R!T(;;X1u}c57XVWZb*6XxbS z_QQhNcOet55!VRa03yC2{~rOBR5;}^0o3ujxH%)wv^gNqh-9SW_6#&7{%CIpngZsE z7av<)e<-x{li08?Y+dV_ z{UopKR^_<=)-X~(yGJ5r)p>$`z0MKDPaX*Z9?>l^-S-<1;d>~0>j5<5QEY#IgA@!N zs91>qKE=<}Bmn@P$s1|Jsk50y6WD1)^e@^~Iu?q5p1I4#nXK1^%*yL?SoHa}25>|+ zHkz&wR!{Z^=@h5{KwMm^0Z#{aa=$zU02p-3L^0EG;u0;&y=Gs=bJ`%)d@v$IX+Y%- z!t?MG7TT+JVZ=!gU2y(gA$IpLH{kNNQ*kl~C}x^3?%nb|#D%hIpOcp1RGZ$oMyek+ zoYYA^OHH+_@tWpnBgZeTuBFO+e}fcbV*nI7^)X2s0{j9K%PBB{5U`5=$z44vV|qd$ z+9=y(rOOdx zGwH<`WY9Iws3mW-)|3Zj7zQ3t$2&~Rq#=_Iv$e^0N*W4|ghN4DpxuTL8nby@FnW=>tNVgmyIK^srx7vtqz5|c>`#F}l$vhN74 za1(sb*fYC~=J9$nxDeQhbYY4Hl|rKKc<7T^2>DD}m$+AD>=w0f2~$XYsBnZ^V6@5h z0tV7o3ObcAy{@})S0SrP+ixV5+8pAt85?&YfO`KImGVH%x&DKC36T@KOd-_UBNoS9 z9h+6mOM%+a90H2?f(cV=Y{c)WT^PxXH7{<=<^^_ct)Z-Y!&|9JAq?WspT7l5X=(jYWixV5u@IDWXezoQgqdD zvt3#mab{jLKxFc?-n*IFhx%TmI<%g1iJLLoFdCurYtyXqn8!S^azDEoEcyU9TD ziC*=eNG4e7xx#=*fw0F|-b%?Ho~^xAPU&|}kJ;!!nJadJGE$kVj`B_&V!4y%T?0Wf zkaE+jEbeTlgm$;s)RfZGyKQCXWlAy>wY$ z7wNk>n9DXBkKyH&nsLq#)U|TqW%CfdDAmvY5OkVxHb^MXVRpW{#eaZd7B-ML$=+yl z+(`OZMJ9W4fzA$!Zf-HJdDV4x7N(b`Mc*B~n`=SW=p8s;3fU&+KWMVb+Pp+A`OX}=l?&eN<a-4%`V7jcxCWac?yU;z3)p?ZYG$YlFipydAaFqHmv;KEr$%S?mTH< zSx4g#_=H9?W4vAHXxn<``_yv*!6*7D(hD58_?E5}YJ~CZ425J}GNXOUMgtX>n}HB- zKSL|$4cBUw2irgOcd+~FL)=mOvDRL{&hhcd`dxWDg1IF)%GRC&p^8qo2%2zy8<=Pq z{pb)dBX?j+FJB?fc^Nnss$$b0G}n6Y_1ag-|D_+*;1F9Q9$((U`?3Y6)#hWO4z#Ft zUI`z;L$(p9oXi|X%wT^(0NON#nB<8iDE6MjO7 z|B6*Veed>A+s7hMX9=0yIV3mvaj77!zmpofoISS*u#PVfM34L3K})|sp85c|Z$Eo> z@z}m27Mg+m5U5OhZpX3^u%q)zQ2xg#rYPVtt7axbL^}B;sz(9rRCN2DdTZttqjj{Dz7Mn6q8ccS>0|li zoVWEeZ)5Z6YUuVpmaFr+uZLXNfXfp8C z1(V$qz@r^6t0KLozuW&@eU`ytz3y^t7SYtW)JVLJ^nL%p45e>xU{E#ZXQ7{hF}wE=3p>~Yz+nUk z{<{wV8aQ|U-Pgn6{wD@o^?U6A;Qte2ij{wV>ez;5|5M_MPmggLd6&Xw?f!RS$o)E1t1I&e#j3I;TIOshlopwNl1x2d;oz+K_Da6suTY~ zaCrfDu=V@@2?cW3*jHJA``;E24lXvH2rF#S@E=A*`5}_}LJ%oYNhu+re;C1VQ%hnw z6n{B3z7Doh$}gN>csy~lvbF()$TD$ZSS97Zl`2?-j;*by4FZsan8eIxVzu=D*6KR= z+Bh3Su|{1z9bUKsqLPpVY007gjWPKT3|RD6>SF%2DC{<+{9LyJ(007{~$x5liW%1vFjtIX(!3!;LL2V|h zt^@!+uK^$^6aenwRYAJ|;LZ*J`^Equm!f*qsse+6Y@bvfm)>ixlUW4H* ztLp{;ScHEI!sJrG1H2LqET<%mwvUW~$$?d!LA?$D2tGcVI$(7ZPikjZCrcZ93u>^p zvjw%6gEIi|$0ljYx%N_Hw;nwAMwFFU-Qh=CR8KO?&LNJQb$R_hvB!_v!{nno4IT4{}g{$z8*Lp`w7wwNy&=?du8^UAl*s*vo2 z0r8lOP?wq?>~5zhm@(_G-Av!Bx2&=$@2J+8qND}?x!`ZKKz1R=Piv7%^K*HF(ap|Y z<^DFLWfQ|fT!^F!cl9`HC@G=r)u1V25BG+m=Ma1Qvh57vidzk9s>bTFEHNB+IJq-l&Hp?wbe7U8#bGRxRJ zw>f`>g_QK^H$E?7{Pf)q=o4q3giZy2FMifIl2qC$6>hlXG6Ijz0CM^$YE zw(=1+X5bBj;N+^Ti7=MunGOIH!E#dKnqKoq{@$7T@6)cK*>3wY?Ux@6-Wz0vg~kI2 zHDbL$&5J~Y;m$XsKdE=Adx9QRX_#3FkQmClcOwEm(g~^=qiSMDyBNoO6Qjn$zzAi* z$XYI+y;NUlyJ@}nc~nM(pV&NI=P4Q(zrt2fe$u3pbt5q5yL|DIo;J0c9*OA+Nii2W z)zqKU^wZTZbhUjnhL`m5^kP7GWW>Wz8JZT%RvA&2HUXvh1%YHH_C$5M?i2b-Mt=Si z7iX)xJ=l|zAc+t-9I(`>#G)A0Fv$Lr$%;h_>`DQhj4)QpmyML45y8ficpLA{wSzIor>tZdHQov*&U@Y~x+W%heIz)B>w zG>jhGCrn_|j2lx~QQE>YF1&IK0I`wULxLUax?^8YpLS0SmZoQ3#3JrsnibtXqVwm` zPr6eIbkwxFxPKF{R->lN)Z}JNy;8&ow6HgS4{c8gj|j@`zLmNi4HcV;EU5B+elYU5 z%J(3Sk^q>8v0XAIxwDu>J!Xy88(m5V6?FHRTrFH;A+AQsy%pb=5!AN}S&O#Y|JL2t zey)=uCe=ysY8=nE+41CfE=SRsl{C-rJ0!-f`atZG%xMHqf<+;dwL}L}O+UjWd0z`f zbK0M4a0BJ5B z*&gJ&_O@5(j)5&YUhYD*cG~?R46Y{P%*wK&+?;)nMAavw#zVhKE0ois{nKi)->y~1 z##ZfH`?~~Y8|b#PvtBo&4z%mcI_t47=IP|N_O(;!s&rIG7F8YFL!a6})P=O?Cxhpa zKjws324f#r83V^h2el7}qOXRts?cZ+?3(A%F#iP@bLMx#3TG>=j5uS-JST*@nt-?p zMEP0+-^(0aPnei(6j0C$`xev)3d#!p*=XaMa{V-fWbyjiqRU6Hpd%-8WfF1{6)eJ# zz%)b>L)ltTg|Pn0J+=X@=l77@p~A3_;C)|VYi;pH)$wR!5ymJ)LgGF*f!H@f)zKPr zaT8BqQ1Vcr-=~NPI@a~*$$Uqcc_>mVQ`U>8-F_qx{=l_P)H0V{0@(V zUcMQh`wtOOZuH3Kl#_U|iV&mqvq$Q=b287jeeH7<2c@es)iw|QVe%WKHf9TF)@8-uh{-AOdtJ=Fh98#N&YgGBG zmrv*#G`WHWVD4;XS~KBt9`F#iw0VYq7hp?9BloIzZO>y{dZ8 z_DQWkVgH`q#o|t)XpZgd_83D+Z{HOu2zhXWk1xcJr#$)68=cn~R_hM%`j=+Gx_ z`Qj;OALbcRG>u7fG3!8$SD^qPiv@wAdg~l~b0`nd`d`<=o1UK*EwWsWl{rwvb-9#P zr9CpodN$;e=*h+DM(L=MLy1e`eu^lEI%YGe%r2G}s`NPS*Yb?oi6%h8ZRwkEUTKQ| z>EnG@z^X?1r7(5v<9!+G(@n_QI}-$e5p55+G!=92nbh{{k-g>g zmUKO0Axzg9TA3M~!KXGhSLp*sprr9r3a8X|_~B7kav-yLsXJ>0`PV1*Nq7H)mz(;E zTl23`<7AQ3tKLd1uHpsMzTGsh_%_0+SnFtgM+fsyIrNTjivu(ag{>$`O3-w4^#Zlk zCLzYj?qJ%IMEGNI3%s8+;+gR12pIAZGcpuuPZmWQEO>H=<0V93)%gC ze2H25j1Um(7H2RyyQu%OkThb|@5wAD&vZuGt=WkN&$A-Y<&>Uf27-tll{IRG>Rb%F zH}JQ$+$v9cMe@)X5CBtU*W;hHFWrWhDq3rp+nus8UqsT&4;v9fCgaxzt)-QgKqJ3n zs*=J(Q?7}m>MOY7fbrkSn}};>3J9;oNxtcp*i`G>am@;0eW}i-q{7}Km8nvB2jSCC ztFn_3Fz`ev&N?X%{u-Ln6IEQH!zTqwi$`i>B=H)*q9UXYwxy#^g{E_EC=JzXJkwKA zpchV*Ca|fA^q)^^Vi|0Z_+52_hMb3t$WXw4&rWrLpP zZeQU`49jJU)Z15@E!z$DTb#uSFw(1)yeB=yYveT0TR&TRZY#jEap{`N@P^tT&lrfe zCRYn#-Td>IcStJ?Akv$v|BkeP)tM4~Aqd zYT0=V^f-UHv?dLE5D4O6BjFzZN3&`THOB8<+E=5ZAu7@xBP{M}T<%$CgfNj7`Y7}m z8%kZM*6hax(-XWV;(NIyYZKjML-SvP0h=lsvMoXi6e`}6B39Ao`vPY-aoPxSy?8l; zl(P!7@vaZ#5d)-5YY0ul#~MSnwkMcM3O7lt--gv3W z-o#(M`m@}Li2z5#YbQlSmq>xq`e@iv&uE)WIU8HFjHPV^F;ugdWQ38~EwhSPnkEGF ztG7x=b6;?$^_Imh<{Zu7y_Jx`3SJo7d2B-xO9>O50j(8-PM$--M*9ftU>OZj%?yn; zZQMd1$pG=xJ|ceU1R0Q(Dy$-;RUd7Gp_2v|1%-3NloT#}O?QeDGe%=RPsKm+zIt znc`AJiQ=8)(1S96g)YOK7Fq(^CCMY*s-Gjsro5B!C7YGy691I8sGm*fU;~lCz~a@h za%!*_;ex<6Lz8K)Db$h(BdhR2XOVkwc^}=`ODux0C`V?^Xq@X@!oxYaS*o6{y91)jGz)a$V0M z40D01Ww4NoX3Y886VfwaBum*?9G<1gWbafW07;Q_H8Hl%bY(f)*R8@ksdY$gFr)G!`8kOqhzk9OLfJYT~ zx8?i{+ncPaShj^48uw4*D5)xH*NaZvRqN4p{CQ=Te#$5%_8|aVap}nySM2&6F^H~# zk;_BZ(C~$x|5Z#Ot7oFDz~&H7<80(%OeHRNU}=?!yEc>!N_&5MC%Ca|z&gMz-%<1y zY)7rn_Gy36hswX@;*H)jDr~pv_g+Mk7i69CW6cO=D_ZUvVAbUkG}hB;>%^~zoD%*1 z27+oj5E0=cbjd@$&H1%mF|}r8t9#pHTqV6#X9>mvlCGp!`$O9f&))<6tlRi`qsr;S zY+J^k+&9`=LP9jZxBQtLvO@^U&=sT&660=8%i09Pysul|gx_~8OkO2Rs#@h87zcDO z($hrD2bvW+>>f+EH)FNd-N-Ztj4yGdDzXQpBGtcB^HKk8FHvEe?V+GCF#2hGFkbTy z?1FL$&q3SQ?5(!tIVi)s|4>H;0Lo+iCwc7xf*5!Nx1~q`e1dJ;XTMFI#PzrE0wDD! zN}f;^U*E(W=M*tZwfiu49C)^4X+c~_vYdninw<|=DS}Y?KTuaOXUv9wow`y>YDr>N z=}C(^Rh>guNhBh-%eLSO=j>8Zh;Caky$f6;)1KiAXvsg?%SE_#W-9>S^|-K(8RRC9 z`S__yX%qW;euph9eGI$@`a<=|2;vzLYCwoKAy*P7F<+3QLmcrUgyTgXJ8dAMqsM-> zGioX)R1aEK3*`AdosCHxNerc>JfE&wg7;>5P`~k4#n50DeWn)*Johm_(z`EV6vpfd zeMQ0b=*$0g=*45F@2?v)%Sg~eR)b|&!*+4ol3lZ)&pOBI1JmQGL-6+%)8OtD(E~t*JknhrC1ONp;OB^chkcE{%g-lwe2+Ra{HU zzZd(M0>y$RSU^WN_Aegw&}ySKW!bvwO>`k~wi1C(GT>1R*ya`CD|k)6qw`IPThXPOK#o-9gv6S^p9XQ# z;%3bAQ2%UUV7_5|ut|?53)3p7R$Kt0{B2H0+wR;GPZ|L53k0yt5K(W!_UCLv!QlhW zPvP|qqk%P7Eo^&qWhD@VG`@8dF*0c{AYM>l@v!O5y@RqxSIP?lwHMLLgQXQpO`OCl zpegwH(q(l63@sSywyPReAOUjUh2}Im4jug8)SqJpdL`okj3Lk;OkdRMF1j2*9ex^A z%IKeY@T!nZ`_rL%=UWE8Fb zTI8CywE0x`Lg!JHI(5N@B|cUJxj;GdEO-s;FNB`G-d}r8?i$=j^^721nHc4H##|fMeg6AOEakJzKyOLw zA{+pmrNhxQhQ0?ivOj6`{Jop}a$8->L41%3GZdc37?zq^WbWu-i@K05E>Oe(^{wo! z7j+XFTDPOH4xD8{gg!zjlJs6>&(V#;OD|hKYIMnNh3AtPK{T)2{pr(Sc<{c~FJD)H z_4X|SK;g*het+_`zw*Q6wjz3ewDr3dgj4oPfwA}Gm0TozW@?FFusDm+ny z(=S9d_rm+qfowLk0a0I=y|e5>kYm$Uy^6 z9`3NsE2#zco6Qb}wonH5BDz5KH~}FL1@H*%xYshee%!|CiQK>v7k4Hy=V8_uw=8NV33!dIGI7~J6Uu@Kxgbq+n!g^$gBAuL=f7ZCTD#$stvko$Tp=gll z=F6gl->lHT5TD5dCa`nt;pWg93~} znyjic(1DRo@Ah4dH*(>1F5^yuTPS@I`>_^KW3jS$96dd%n39=H_+G^elRqHANW}9) z?3gJ93ubvdAZil?ATy9QWbn@$n+6)_j31lRGnonSd5cL|uMOKE40wj}6*czs6^3TE z2xt>BG8D*}dT>-7{89VDbC3vh8qk?vjk$-OEv3iHf0YO(T?GdiIP{BSE}Q{nyLL2Qdf) ziaBPY9#KMqj6)@rJ8!-fIysogOZYYn{$MFq=9N}BXuiUmdfDyS%F%d-!r zuu$g=qdeQ-lByi}nn%uVz-PsM0)n~K4L#-tlo)Nb`fV^IpX4hUtA_k)j6<&teAJyq z6t&@u;5JnTmEZ%b>vrjYF)ZJo6;CGF&Mfk6YOo8ap>TrZpsRKsz^~t!K$_j`idVRwPL7isw}A0#iep;uLNb(>1koOaL6t% zUtI~G%~vMn`5>J=S$gI5PkXLbw;5Kv6<0#)_<|@^+-IUFUHtM{nU0TJdJ;yk00l`^ zGP22-4z?@u41CqK!T?06(aQw-J)+j@(EJ?3J>3_ySux2%&mIni!9+ZRg;A@wDlI!K zV3D@^o3Re^$hcixlJdnMHeq>;SP{(E8|g2ng^NSG8!|UDSAbG1nRBk@2Vo1wc_!ZL z8>2HefrT4rgpc9V0{V`%J9|FF&mvdzs&gO#tPVA+-&YMENv!7VBlWtQ5JYjnl?bSPCU*;4u z!Eh#WWzG!iX4inzd6c{vRBxr$REf8a&|mn_am9n_hOm`6{_`m|U(XJ*y7|<;bR#RH z{U*mn7|Hvq&|^?rq4zZH4g|-Ux*%oykG#iEjiR|RqjiyM`WvhR)W=&{BPSUI_}Zq~ zrfj-qglSy9t)!8jj_)#o@vxKDVf-V(*IUbKMaa`IN?o+d&Er`@dq)L%B<_I+pCo&r zq{d5*U~jL^q`Qida$>Y9!MH!j_ykFP7|k_}VixI183r`IPQ{0hkAb3A?p6*&koGMQ% zeH(9<`1&h&i*(Xh-3;86NCy(Q)gw%=|M27<5KmQR;%u$Kvy`Se=KAW#t3%e&Xol+z<49os5!q8u(#VJu-aDIqCo%>ytlVWqVHz(1HD?E;WIy5K z6>;{q!ISk$_{t5H^5KPiPa~6t_~86=u8e9U`;4uRKbXq#2XjMo{=QDjm&(?>3Fqq! z-a|CO#yO2g&KibP#oc+;VRwDA-Eir4t`Fas4dAH|`wUJ&-||X@-N;^p{$05ABOBF6zX#;NyT4ISsOX943phsH zSw!J7G!(rYo?ot-30?XX7SfCxe^EDVQ*bj#wCuIl3LmY$zJBwy0sb?RG}s*M0!is5 z&2AlTSk)YJ`+W0s=LzdUN}nEm?L~-$GMM`MS@X@5D!eP}V?C&1z`qSm-UHMavrF|i z86UGeb$g1(u3&qR_6`v)Zr+NWhqQt(y!bcFI4D&j?H+fE3~G0h!9hu$VrySaBHK9n zgsyvybod(Aqj8o$h!@3ODIkWME%-+8d7xGak6MrEiT@>det^o^G(2e(r z(R?WgGHxdD)66WO*7a@IAX{1T)msY0>F>O0A-akX5i*Xxd-GnRgD(Uc6)NxktKta@ z1NSsbvNv%RzB3tWp(AIhqy#X+b#wp)fe=7~YY6ZYKzIS5{9hda>3{UUZa8NL(Qxq!EUg1JHvwA+ znz>rQ1;EM9$;-md#lp#@$<8Op!!O9i%f!wu$j&}#_U_MrIXF0(+gN)4{~hv09xdPw z4F52IZ5%Azz$Wm$@PCnTv#<+ja3kY&>{EK9kfk*)EL-5zf!pp`|P}0fX$yLMI z#LNQVz{^0CgBuC|F-pS`DwdXR7GOYtT{mVq18yb#$Es@MWnr%)1t)cMvvG0+xCPh~ zgapR^H%9lrFxcF4P5)s5+gMr(s<_%%ID$>UfA7K3Q2rnUx8nb0f$!@7*Yly^4otvp jB>%DfKOP{O8!Yj^ApgxzER@6zhX8WY%2HJl#v%U)-fFps literal 0 HcmV?d00001 diff --git a/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon180x180.png b/packaging/ios/icons/Assets.xcassets/AppIcon.appiconset/AppIcon180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..af91234e894be254e4623ad725e6758b423f6f41 GIT binary patch literal 8996 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 Date: Thu, 9 Apr 2026 21:04:02 +0800 Subject: [PATCH 139/283] ui: polish settings screen spacing, colors, and card layout (#73) * ui: polish settings screen spacing, colors, and border radius - Define layout constants (SPACE_XS/SM/MD/LG/XL/XXL, RADIUS_SM/MD/LG) and settings-specific color tokens in styles.rs - Replace magic numbers with constants across settings_screen.rs, account_settings.rs, and navigation_tab_bar.rs - Fix bottom padding cutoff (bottom: 0 -> SETTINGS_CONTENT_PADDING) - Fix blue account bar text visibility (white text on blue background) - Use softer blue (COLOR_ACCOUNT_ACTIVE_BG) for active account bar - Soften divider line and close button proportions - Unify SubsectionLabel vertical rhythm locally (top: 12, bottom: 4) - Apply RADIUS_MD to all settings buttons locally - Extract hardcoded dropdown colors to semantic constants - Add bottom margin to Other Actions section * ui: add card containers for settings screen sections Wrap Avatar, Display Name, and Multiple Accounts sections in RoundedView cards with subtle #F8F8FA background for visual grouping. User ID and Other Actions remain flat for lighter visual weight. --- src/home/navigation_tab_bar.rs | 14 +- src/settings/account_settings.rs | 316 ++++++++++++++++++------------- src/settings/settings_screen.rs | 43 +++-- src/shared/styles.rs | 25 +++ 4 files changed, 237 insertions(+), 161 deletions(-) diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index 72b96bca7..4e9c096e4 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -49,8 +49,8 @@ script_mod! { mod.widgets.NavigationTabButton = RadioButtonTab { width: Fill, height: (NAVIGATION_TAB_BAR_SIZE - 5), - padding: 5, - margin: 3, + padding: (SPACE_XS), + margin: (SPACE_XS), align: Align{x: 0.5, y: 0.5} flow: Down, text: "", @@ -72,7 +72,7 @@ script_mod! { color_focus: (COLOR_NAVIGATION_TAB_BG_ACTIVE) border_size: 0.0 - border_radius: 4.0 + border_radius: (RADIUS_MD) border_color: #0000 border_color_hover: #0000 border_color_down: #0000 @@ -155,19 +155,19 @@ script_mod! { draw_icon +: { svg: (ICON_ADD) } } - mod.widgets.Separator = LineH { margin: 8 } + mod.widgets.Separator = LineH { margin: (SPACE_SM) } mod.widgets.NavigationTabBar = #(NavigationTabBar::register_widget(vm)) { Desktop := RoundedView { flow: Down, align: Align{x: 0.5} - padding: Inset{top: 8., bottom: 8} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_XS), right: (SPACE_XS)} width: (NAVIGATION_TAB_BAR_SIZE), height: Fill draw_bg +: { color: (COLOR_SECONDARY) - border_radius: 4.0 + border_radius: (RADIUS_LG) } CachedWidget { @@ -201,7 +201,7 @@ script_mod! { draw_bg +: { color: (COLOR_SECONDARY) - border_radius: 4.0 + border_radius: (RADIUS_LG) } CachedWidget { diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 78d3db37f..fad2b3a27 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -24,147 +24,178 @@ script_mod! { text: "Account Settings" } - avatar_section_label := SubsectionLabel { - text: "Your Avatar:" - } - - View { + // --- Avatar card --- + RoundedView { width: Fill, height: Fit - // TODO: I'd like to use RightWrap here, but Makepad doesn't yet - // support RightWrap with align: Align{y: 0.5}. - flow: Right, - align: Align{y: 0.5} - - our_own_avatar := Avatar { - width: 100, - height: 100, - margin: 10, - text_view +: { - text +: { - draw_text +: { - text_style: theme.font_regular { font_size: 35.0 } - } - } - } + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_SM)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + + avatar_section_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Your Avatar:" } View { - width: Fit, height: Fit - flow: Down, + width: Fill, height: Fit + // TODO: I'd like to use RightWrap here, but Makepad doesn't yet + // support RightWrap with align: Align{y: 0.5}. + flow: Right, align: Align{y: 0.5} - padding: Inset{ left: 10, right: 10 } - spacing: 10 - - View { - width: Fit, height: Fit - flow: Right, - align: Align{y: 0.5} - spacing: 10 - - 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) - icon_walk: Walk{width: 16, height: 16} - text: "Upload Avatar" - } - upload_avatar_spinner := LoadingSpinner { - width: 16, height: 16 - visible: false - draw_bg.color: (COLOR_ACTIVE_PRIMARY) + our_own_avatar := Avatar { + width: 100, + height: 100, + margin: 10, + text_view +: { + text +: { + draw_text +: { + text_style: theme.font_regular { font_size: 35.0 } + } + } } } View { width: Fit, height: Fit - flow: Right, + flow: Down, align: Align{y: 0.5} - spacing: 10 - - 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) - icon_walk: Walk{ width: 16, height: 16 } - text: "Delete Avatar" + padding: Inset{ left: (SPACE_SM), right: (SPACE_SM) } + spacing: (SPACE_SM) + + View { + width: Fit, height: Fit + flow: Right, + align: Align{y: 0.5} + spacing: (SPACE_SM) + + 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_bg +: { border_radius: (RADIUS_MD) } + draw_icon.svg: (ICON_UPLOAD) + icon_walk: Walk{width: 16, height: 16} + text: "Upload Avatar" + } + + upload_avatar_spinner := LoadingSpinner { + width: 16, height: 16 + visible: false + draw_bg.color: (COLOR_ACTIVE_PRIMARY) + } } - delete_avatar_spinner := LoadingSpinner { - width: 16, height: 16 - visible: false - draw_bg.color: (COLOR_ACTIVE_PRIMARY) + View { + width: Fit, height: Fit + flow: Right, + align: Align{y: 0.5} + spacing: (SPACE_SM) + + 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_bg +: { border_radius: (RADIUS_MD) } + draw_icon.svg: (ICON_TRASH) + icon_walk: Walk{ width: 16, height: 16 } + text: "Delete Avatar" + } + + delete_avatar_spinner := LoadingSpinner { + width: 16, height: 16 + visible: false + draw_bg.color: (COLOR_ACTIVE_PRIMARY) + } } } } } - display_name_section_label := SubsectionLabel { - text: "Your Display Name:" - } - - display_name_input := RobrixTextInput { - margin: Inset{top: 3, left: 5, right: 5, bottom: 8}, - width: 216, height: Fit - empty_text: "Add a display name..." - } - - View { + // --- Display Name card --- + RoundedView { width: Fill, height: Fit - flow: Flow.Right{wrap: true}, - align: Align{y: 0.5}, - spacing: 10 - - // These buttons are disabled by default, and enabled when the user - // changes the `display_name_input` text. - // These buttons start disabled; Rust code enables them and swaps - // their styles to RobrixNeutralIconButton / RobrixPositiveIconButton. - cancel_display_name_button := RobrixNeutralIconButton { - enabled: false, - width: Fit, height: Fit, - padding: 10, - margin: Inset{left: 5}, - draw_icon.svg: (ICON_FORBIDDEN) - icon_walk: Walk{width: 16, height: 16, margin: 0} - text: "Cancel" + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_SM)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) } - accept_display_name_button := RobrixPositiveIconButton { - enabled: false, - width: Fit, height: Fit, - 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" + display_name_section_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Your Display Name:" } - save_name_spinner := LoadingSpinner { - width: 16, height: 16 - margin: Inset{left: 5, top: 13} // vertically center with buttons - visible: false - draw_bg.color: (COLOR_ACTIVE_PRIMARY) + display_name_input := RobrixTextInput { + margin: Inset{top: 3, left: (SPACE_XS), right: (SPACE_XS), bottom: (SPACE_SM)}, + width: 216, height: Fit + empty_text: "Add a display name..." + } + + View { + width: Fill, height: Fit + flow: Flow.Right{wrap: true}, + align: Align{y: 0.5}, + spacing: (SPACE_SM) + + // These buttons are disabled by default, and enabled when the user + // changes the `display_name_input` text. + // These buttons start disabled; Rust code enables them and swaps + // their styles to RobrixNeutralIconButton / RobrixPositiveIconButton. + cancel_display_name_button := RobrixNeutralIconButton { + enabled: false, + width: Fit, height: Fit, + padding: 10, + margin: Inset{left: (SPACE_XS)}, + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: 0} + text: "Cancel" + } + + accept_display_name_button := RobrixPositiveIconButton { + enabled: false, + width: Fit, height: Fit, + padding: 10, + margin: Inset{left: (SPACE_XS)}, + draw_bg.border_radius: (RADIUS_MD) + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: 0} + text: "Save Name" + } + + save_name_spinner := LoadingSpinner { + width: 16, height: 16 + margin: Inset{left: 5, top: 13} // vertically center with buttons + visible: false + draw_bg.color: (COLOR_ACTIVE_PRIMARY) + } } } user_id_section_label := SubsectionLabel { + margin: Inset{top: (SPACE_MD), bottom: (SPACE_XS)} text: "Your User ID:" } View { width: Fill, height: Fit flow: Right, - spacing: 10 + spacing: (SPACE_SM) copy_user_id_button := RobrixNeutralIconButton { enable_long_press: true, - margin: Inset{left: 5} - padding: 12, + margin: Inset{left: (SPACE_XS)} + padding: (SPACE_MD), spacing: 0, draw_icon.svg: (ICON_COPY) icon_walk: Walk{width: 16, height: 16, margin: Inset{right: -2} } @@ -173,7 +204,7 @@ script_mod! { user_id := Label { width: Fill, height: Fit flow: Flow.Right{wrap: true}, - margin: Inset{top: 10} + margin: Inset{top: (SPACE_SM)} draw_text +: { color: (MESSAGE_TEXT_COLOR), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, @@ -182,15 +213,27 @@ script_mod! { } } - multiple_accounts_section_label := SubsectionLabel { - text: "Multiple Accounts:" - } - - View { + // --- Multiple Accounts card --- + RoundedView { width: Fill, height: Fit - flow: Down, - spacing: 8, - margin: Inset{left: 5, right: 5, bottom: 10} + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_SM)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + + multiple_accounts_section_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Multiple Accounts:" + } + + View { + width: Fill, height: Fit + flow: Down, + spacing: (SPACE_SM), // Account entries will be shown here // Active account (current) @@ -198,12 +241,12 @@ script_mod! { width: Fill, height: Fit flow: Right, align: Align{y: 0.5} - padding: Inset{left: 10, right: 10, top: 8, bottom: 8} - spacing: 10 + padding: Inset{left: (SPACE_MD), right: (SPACE_LG), top: (SPACE_SM), bottom: (SPACE_SM)} + spacing: (SPACE_SM) show_bg: true draw_bg +: { - color: (COLOR_ACTIVE_PRIMARY) - border_radius: 4.0 + color: (COLOR_ACCOUNT_ACTIVE_BG) + border_radius: (RADIUS_LG) } View { @@ -214,7 +257,7 @@ script_mod! { active_account_label := Label { width: Fill, height: Fit draw_text +: { - color: (COLOR_TEXT), + color: (COLOR_PRIMARY), text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } text: "@user:server" @@ -223,7 +266,7 @@ script_mod! { active_account_status_label := Label { width: Fit, height: Fit draw_text +: { - color: (COLOR_FG_ACCEPT_GREEN), + color: (COLOR_PRIMARY), text_style: MESSAGE_TEXT_STYLE { font_size: 9 }, } text: "Active" @@ -234,7 +277,7 @@ script_mod! { // Other accounts section (populated dynamically) other_accounts_label := Label { width: Fill, height: Fit - margin: Inset{top: 5, left: 2} + margin: Inset{top: (SPACE_XS), left: 2} visible: false draw_text +: { color: (MESSAGE_TEXT_COLOR), @@ -248,15 +291,15 @@ script_mod! { width: Fill, height: Fit flow: Right, align: Align{y: 0.5} - padding: Inset{left: 10, right: 10, top: 8, bottom: 8} - spacing: 10 + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_SM)} + spacing: (SPACE_SM) visible: false show_bg: true draw_bg +: { color: (COLOR_SECONDARY) - border_radius: 4.0 + border_radius: (RADIUS_LG) border_size: 1.0 - border_color: #555 + border_color: (COLOR_INACTIVE_BORDER) } View { @@ -285,7 +328,7 @@ script_mod! { account_count_label := Label { width: Fill, height: Fit - margin: Inset{top: 5, bottom: 5, left: 5} + margin: Inset{top: (SPACE_XS), bottom: (SPACE_XS), left: (SPACE_XS)} draw_text +: { color: (MESSAGE_TEXT_COLOR), text_style: MESSAGE_TEXT_STYLE { font_size: 10 }, @@ -295,36 +338,41 @@ script_mod! { add_account_button := RobrixIconButton { width: Fit, - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} - margin: Inset{top: 5} + padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + margin: Inset{top: (SPACE_XS)} + draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_ADD) icon_walk: Walk{width: 16, height: 16} text: "Add Another Account" } - } + } + } // end Multiple Accounts card other_actions_section_label := SubsectionLabel { + margin: Inset{top: (SPACE_MD), bottom: (SPACE_XS)} text: "Other actions:" } View { - // margin: Inset{top: 20}, width: Fill, height: Fit flow: Flow.Right{wrap: true}, align: Align{y: 0.5}, - spacing: 10 + spacing: (SPACE_SM) + margin: Inset{bottom: (SPACE_LG)} manage_account_button := RobrixIconButton { - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} - margin: Inset{left: 5} + padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + margin: Inset{left: (SPACE_XS)} + draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_EXTERNAL_LINK) icon_walk: Walk{width: 16, height: 16} text: "Manage Account" } logout_button := RobrixNegativeIconButton { - padding: Inset{top: 10, bottom: 10, left: 12, right: 15} - margin: Inset{left: 5} + padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + margin: Inset{left: (SPACE_XS)} + draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_LOGOUT) icon_walk: Walk{ width: 16, height: 16, margin: Inset{right: -2} } text: "Log out" diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 3ba531fef..ef4564537 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -16,19 +16,19 @@ script_mod! { flow: Overlay View { - padding: Inset{top: 5, left: 15, right: 15, bottom: 0}, + padding: Inset{top: (SPACE_SM), left: (SETTINGS_CONTENT_PADDING), right: (SETTINGS_CONTENT_PADDING), bottom: (SETTINGS_CONTENT_PADDING)}, flow: Down // The settings header shows a title, with a close button to the right. settings_header := View { flow: Right, width: Fill, height: Fit - margin: Inset{top: 5, left: 5, right: 5} - spacing: 10, + margin: Inset{top: (SPACE_SM), left: (SPACE_XS), right: (SPACE_XS)} + spacing: (SPACE_SM), settings_header_title := TitleLabel { padding: 0, - margin: Inset{ left: 1, top: 11 }, + margin: Inset{ left: 0, top: (SPACE_SM) }, text: "Add/Explore Rooms" draw_text +: { text_style: theme.font_regular {font_size: 18}, @@ -41,43 +41,46 @@ script_mod! { height: Fit, spacing: 0, margin: 0, - padding: 15, + padding: (SPACE_LG), draw_icon.svg: (ICON_CLOSE) - icon_walk: Walk{width: 14, height: 14} + icon_walk: Walk{width: 12, height: 12} } } // Make sure the dividing line is aligned with the close_button - LineH { padding: 10, margin: Inset{top: 10, right: 2} } + LineH { padding: 0, margin: Inset{top: (SPACE_SM), bottom: (SPACE_SM)} } settings_category_cards := View { width: Fill, height: Fit flow: Flow.Right{wrap: true} align: Align{y: 0.5} - spacing: 10 - margin: Inset{left: 5, right: 5, bottom: 8} + spacing: (SPACE_SM) + margin: Inset{left: (SPACE_XS), right: (SPACE_XS), bottom: (SPACE_SM)} category_account_button := RobrixNeutralIconButton { width: Fit, height: Fit, - padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_MD)} spacing: 0, icon_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { border_radius: (RADIUS_MD) } text: "Account" } category_preferences_button := RobrixNeutralIconButton { width: Fit, height: Fit, - padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_MD)} spacing: 0, icon_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { border_radius: (RADIUS_MD) } text: "Preferences" } category_labs_button := RobrixNeutralIconButton { width: Fit, height: Fit, - padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_MD)} spacing: 0, icon_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { border_radius: (RADIUS_MD) } text: "Labs" } } @@ -123,15 +126,15 @@ script_mod! { show_bg: true draw_bg +: { color: (COLOR_PRIMARY) - border_radius: 4.0 + border_radius: (RADIUS_SM) border_size: 1.0 - border_color: #xC8D9F2 + border_color: (COLOR_DROPDOWN_BORDER) } language_selector_label := Label { width: Fill, height: Fit draw_text +: { - color: #x333333 + color: (COLOR_DROPDOWN_TEXT) text_style: REGULAR_TEXT { font_size: 11 } } text: "English" @@ -140,7 +143,7 @@ script_mod! { language_arrow := ExpandArrow { width: 14, height: 14 draw_bg +: { - color: instance(#x888888) + color: instance((COLOR_DROPDOWN_ARROW)) } } } @@ -154,9 +157,9 @@ script_mod! { new_batch: true draw_bg +: { color: (COLOR_PRIMARY) - border_radius: 6.0 + border_radius: (RADIUS_MD) border_size: 1.0 - border_color: #xD3E1F6 + border_color: (COLOR_DROPDOWN_POPUP_BORDER) } lang_option_en := View { @@ -170,7 +173,7 @@ script_mod! { Label { width: Fit, height: Fit draw_text +: { - color: #x333333 + color: (COLOR_DROPDOWN_TEXT) text_style: REGULAR_TEXT { font_size: 11 } } text: "English" @@ -187,7 +190,7 @@ script_mod! { Label { width: Fit, height: Fit draw_text +: { - color: #x333333 + color: (COLOR_DROPDOWN_TEXT) text_style: REGULAR_TEXT { font_size: 11 } } text: "简体中文" diff --git a/src/shared/styles.rs b/src/shared/styles.rs index 72503e226..1f401d7a9 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -185,6 +185,31 @@ script_mod! { mod.widgets.COLOR_NAVIGATION_TAB_BG_HOVER = (mod.widgets.COLOR_SECONDARY * 0.85) mod.widgets.COLOR_NAVIGATION_TAB_BG_ACTIVE = #9 + // Layout spacing constants (4px grid) + mod.widgets.SPACE_XS = 4 + mod.widgets.SPACE_SM = 8 + mod.widgets.SPACE_MD = 12 + mod.widgets.SPACE_LG = 16 + mod.widgets.SPACE_XL = 20 + mod.widgets.SPACE_XXL = 24 + + // Border radius constants + mod.widgets.RADIUS_SM = 4.0 + mod.widgets.RADIUS_MD = 6.0 + mod.widgets.RADIUS_LG = 8.0 + + // Settings screen colors + mod.widgets.COLOR_ACCOUNT_ACTIVE_BG = #3B8CFF // softer blue for active account bar + mod.widgets.COLOR_DROPDOWN_TEXT = #x333333 // text in dropdown selectors + mod.widgets.COLOR_DROPDOWN_BORDER = #xC8D9F2 // dropdown border (light blue-gray) + mod.widgets.COLOR_DROPDOWN_POPUP_BORDER = #xD3E1F6 // popup border (slightly lighter) + mod.widgets.COLOR_DROPDOWN_ARROW = #x888888 // dropdown arrow icon + mod.widgets.COLOR_INACTIVE_BORDER = #xBBBBBB // inactive account entry border + + // Settings screen layout + mod.widgets.SETTINGS_CONTENT_PADDING = 16 + mod.widgets.SETTINGS_BUTTON_HEIGHT = 36 + mod.widgets.COLOR_IMAGE_VIEWER_BACKGROUND = #333333CC // 80% Opacity mod.widgets.COLOR_IMAGE_VIEWER_META_BACKGROUND = #E8E8E8 From 52beab915c58d56f0a19ae15b6921c32ad69de81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:16:15 +0800 Subject: [PATCH 140/283] Fix add-account SSO cancel retry flow and improve multiple-account cards (#72) * Fix add-account SSO cancel flow and polish multi-account cards * Use upstream account settings UI implementation --- specs/task-sso-reclick-after-cancel.spec.md | 86 +++++++++++++++++++++ src/app.rs | 8 +- src/login/login_screen.rs | 45 ++++++++--- 3 files changed, 126 insertions(+), 13 deletions(-) create mode 100644 specs/task-sso-reclick-after-cancel.spec.md diff --git a/specs/task-sso-reclick-after-cancel.spec.md b/specs/task-sso-reclick-after-cancel.spec.md new file mode 100644 index 000000000..52442064b --- /dev/null +++ b/specs/task-sso-reclick-after-cancel.spec.md @@ -0,0 +1,86 @@ +spec: task +name: "SSO Option Is Re-clickable After Cancel" +inherits: project +tags: [bugfix, login, sso, multi-account, ui] +estimate: 1d +--- + +## Intent + +Fix issue #43 (https://github.com/Project-Robius-China/robrix2/issues/43): in add-account flow, after starting SSO once and cancelling, the SSO provider buttons can become non-clickable when returning to the sign-in screen. The expected behavior is that cancellation must always restore a retryable SSO state, so users can click an SSO provider again without restarting the app. + +## Constraints + +- Keep existing async request path: `submit_async_request(MatrixRequest::SpawnSSOServer { ... })` +- Keep duplicate-request guard while SSO is truly pending (`sso_pending` should still block repeated clicks during active flow) +- Preserve existing add-account navigation behavior (show/hide login screen semantics in `App`) +- Do not change login/signup semantics unrelated to SSO cancellation + +## Decisions + +- Keep SSO re-entry logic in existing login flow; do not redesign authentication architecture +- SSO button enabled/disabled UI must be driven by real SSO lifecycle state, not stale local state from prior attempts +- Cancellation paths (SSO modal cancel, add-account cancel, and return to login screen) must converge to a state where SSO is clickable again +- Preserve existing behavior that blocks duplicate SSO launches while a request is genuinely in flight +- `LoginAction::LoginFailure` emitted during add-account flow must not flip global `logged_in` state or hide the existing home/settings screen + +## Boundaries + +### Allowed Changes +- src/login/login_screen.rs +- src/sliding_sync.rs +- src/app.rs (only if needed to reset add-account/login transition state) + +### Forbidden +- Do not change non-SSO login flows (password login/signup semantics) +- Do not add new dependencies +- Do not run `cargo fmt` or reformat unrelated code +- Do not change provider list/branding or add new SSO providers + +## Acceptance Criteria + +Scenario: Re-click SSO after cancelling an SSO attempt in add-account mode + Test: manual_test_add_account_sso_retry_after_cancel + Given the user is logged in and opens "Add another account" + When the user clicks any SSO provider and then cancels the SSO flow + Then returning to the add-account login screen shows SSO providers as enabled + And clicking the same provider again starts a new SSO attempt + +Scenario: Cancel add-account screen after SSO cancel, then re-open add-account + Test: manual_test_add_account_cancel_then_reopen_sso_clickable + Given an SSO attempt was cancelled during add-account flow + When the user presses add-account cancel and later opens add-account again + Then SSO providers are clickable on first try + +Scenario: Cancel add-account after SSO cancel returns to non-blank settings/home UI + Test: manual_test_add_account_cancel_returns_to_settings + Given the user is logged in and enters add-account flow from settings + And the user cancels an in-progress SSO flow + When the user presses add-account cancel to go back + Then the previous settings/home interface remains visible (not a blank page) + And the session remains logged in + +Scenario: Pending guard still blocks duplicate clicks only while truly pending + Test: manual_test_sso_pending_guard_scope + Given an SSO request is actively in flight + When the user repeatedly clicks SSO provider buttons + Then additional requests are ignored during pending + And once pending ends (success, failure, or cancel), providers become clickable again + +Scenario: UI affordance matches interactivity + Test: manual_test_sso_button_visual_state_after_cancel + Given an SSO flow has been cancelled + When the user returns to the login screen + Then SSO button cursor and visual mask indicate enabled/clickable state + +Scenario: Regression guard for non-SSO login + Test: manual_test_password_login_unchanged + Given the login screen is shown + When the user logs in with user ID and password + Then password-based login behavior remains unchanged + +## Out of Scope + +- Changing SSO backend protocol, callback URL format, or browser-launch mechanism +- Adding telemetry/analytics for SSO cancellation +- UX redesign of login screen layout or modal copy diff --git a/src/app.rs b/src/app.rs index 92fedfa48..2fb78e73f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -793,6 +793,9 @@ impl MatchEvent for App { if let Some(LoginAction::AddAccountSuccess) = action.downcast_ref() { log!("Received LoginAction::AddAccountSuccess, hiding login view."); self.app_state.adding_account = false; + self.ui + .modal(cx, ids!(login_screen_view.login_screen.login_status_modal)) + .close(cx); self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, false); self.ui.redraw(cx); continue; @@ -802,6 +805,9 @@ impl MatchEvent for App { if let Some(LoginAction::CancelAddAccount) = action.downcast_ref() { log!("Received LoginAction::CancelAddAccount, hiding login view."); self.app_state.adding_account = false; + self.ui + .modal(cx, ids!(login_screen_view.login_screen.login_status_modal)) + .close(cx); self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, false); self.ui.redraw(cx); continue; @@ -848,7 +854,7 @@ impl MatchEvent for App { // by `handle_session_changes`), navigate back to the login screen. // When not yet logged in, the login_screen widget handles displaying the failure modal. if let Some(LoginAction::LoginFailure(_)) = action.downcast_ref() { - if self.app_state.logged_in { + if self.app_state.logged_in && !self.app_state.adding_account { log!("Received LoginAction::LoginFailure while logged in; showing login screen."); self.app_state.logged_in = false; self.update_login_visibility(cx); diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 4bcbb3380..2a7e99221 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -356,6 +356,33 @@ pub struct LoginScreen { } impl LoginScreen { + fn set_sso_pending_state(&mut self, cx: &mut Cx, pending: bool) { + let mask = if pending { 1.0 } else { 0.0 }; + let cursor = if pending { MouseCursor::NotAllowed } else { MouseCursor::Hand }; + let button_set: &[&[LiveId]] = ids_array!( + apple_button, + facebook_button, + github_button, + gitlab_button, + google_button, + twitter_button + ); + for view_ref in self.view_set(cx, button_set).iter() { + let Some(mut view_mut) = view_ref.borrow_mut() else { continue }; + let mut image = view_mut.image(cx, ids!(image)); + script_apply_eval!(cx, image, { + draw_bg.mask: #(mask) + }); + view_mut.cursor = Some(cursor); + } + self.sso_pending = pending; + } + + fn reset_sso_state(&mut self, cx: &mut Cx) { + self.sso_redirect_url = None; + self.set_sso_pending_state(cx, false); + } + fn sync_mode_texts(&mut self, cx: &mut Cx) { self.view.label(cx, ids!(title)).set_text(cx, if self.signup_mode { @@ -461,6 +488,7 @@ impl WidgetMatchEvent for LoginScreen { // Handle cancel button for add-account mode if cancel_button.clicked(actions) { self.adding_account = false; + self.reset_sso_state(cx); // Reset the UI back to normal login mode self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "login.title.login_to_robrix")); cancel_button.set_visible(cx, false); @@ -620,17 +648,7 @@ impl WidgetMatchEvent for LoginScreen { self.redraw(cx); } Some(LoginAction::SsoPending(pending)) => { - let mask = if *pending { 1.0 } else { 0.0 }; - let cursor = if *pending { MouseCursor::NotAllowed } else { MouseCursor::Hand }; - for view_ref in self.view_set(cx, button_set).iter() { - let Some(mut view_mut) = view_ref.borrow_mut() else { continue }; - let mut image = view_mut.image(cx, ids!(image)); - script_apply_eval!(cx, image, { - draw_bg.mask: #(mask) - }); - view_mut.cursor = Some(cursor); - } - self.sso_pending = *pending; + self.set_sso_pending_state(cx, *pending); self.redraw(cx); } Some(LoginAction::SsoSetRedirectUrl(url)) => { @@ -638,6 +656,7 @@ impl WidgetMatchEvent for LoginScreen { } Some(LoginAction::ShowAddAccountScreen) => { self.adding_account = true; + self.reset_sso_state(cx); // Update UI to "add account" mode self.view.label(cx, ids!(title)).set_text(cx, tr_key(self.app_language, "settings.account.button.add_another_account")); cancel_button.set_visible(cx, true); @@ -648,6 +667,7 @@ impl WidgetMatchEvent for LoginScreen { Some(LoginAction::AddAccountSuccess) => { // Reset the login screen state self.adding_account = false; + self.reset_sso_state(cx); user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); @@ -686,7 +706,8 @@ impl WidgetMatchEvent for LoginScreen { let request_id = id!(SSO_CANCEL_BUTTON); let request = HttpRequest::new(format!("{}/?login_token=",sso_redirect_url), HttpMethod::GET); cx.http_request(request_id, request); - self.sso_redirect_url = None; + self.reset_sso_state(cx); + self.redraw(cx); } } From e1219a00bb95c11f800e7ffbfd33c0671ec657e7 Mon Sep 17 00:00:00 2001 From: Alvin <48358093+TigerInYourDream@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:41:25 +0800 Subject: [PATCH 141/283] ui: polish Preferences and Labs tabs with constants and card layout (#74) - Replace magic numbers with SPACE_*/RADIUS_* constants in bot_settings and translation_settings - Extract hardcoded colors to semantic constants (COLOR_DESCRIPTION_TEXT, COLOR_FIELD_LABEL, COLOR_DISABLED_TEXT) - Change header layout from flow: Right to flow: Down for proper title/description stacking (avoids baseline alignment issues) - Add blue active state to Toggle switches (color_active, mark_color_active) - Change Enabled label color from green to blue (COLOR_ACTIVE_PRIMARY) for consistency with app accent color - Wrap Preferences language section in card container (#F8F8FA) - Replace Labs LineH separators with card containers for each section (App Service, Translation, TSP) --- src/settings/bot_settings.rs | 26 +++---- src/settings/settings_screen.rs | 102 +++++++++++++++++++-------- src/settings/translation_settings.rs | 42 +++++------ src/shared/styles.rs | 3 + 4 files changed, 112 insertions(+), 61 deletions(-) diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index e7f34013d..93a7aaafb 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -14,7 +14,7 @@ script_mod! { mod.widgets.BotSettingsInfoLabel = Label { width: Fill height: Fit - margin: Inset{left: 5, top: 2, bottom: 2} + margin: Inset{left: (SPACE_XS), top: 2, bottom: 2} draw_text +: { color: (MESSAGE_TEXT_COLOR) text_style: REGULAR_TEXT { font_size: 10.5 } @@ -26,15 +26,14 @@ script_mod! { width: Fill height: Fit flow: Down - spacing: 10 + spacing: (SPACE_SM) app_service_header := View { width: Fill height: Fit - flow: Right - align: Align{y: 1.0} - spacing: 8 - margin: Inset{left: 5, right: 8, bottom: 2} + flow: Down + spacing: (SPACE_XS) + margin: Inset{left: (SPACE_XS), right: (SPACE_SM), bottom: 2} app_service_title := TitleLabel { width: Fit @@ -45,7 +44,7 @@ script_mod! { width: Fill margin: 0 draw_text +: { - color: #7A7A7A + color: (COLOR_DESCRIPTION_TEXT) text_style: REGULAR_TEXT { font_size: 9.5 } } text: "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands." @@ -57,17 +56,20 @@ script_mod! { height: Fit flow: Right align: Align{x: 0.0, y: 0.5} - spacing: 8 - margin: Inset{left: 5, bottom: 2} + spacing: (SPACE_SM) + margin: Inset{left: (SPACE_XS), bottom: 2} app_service_switch := Toggle { width: Fit height: Fit - padding: Inset{top: 8, right: 8, bottom: 8, left: 8} + padding: Inset{top: (SPACE_SM), right: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_SM)} text: "" active: false draw_bg +: { size: 20.0 + color_active: (COLOR_ACTIVE_PRIMARY) + border_color_active: (COLOR_ACTIVE_PRIMARY) + mark_color_active: #fff } } @@ -75,7 +77,7 @@ script_mod! { width: Fit height: Fit draw_text +: { - color: #999 + color: (COLOR_DISABLED_TEXT) text_style: REGULAR_TEXT { font_size: 10.5 } } text: "Disabled" @@ -139,7 +141,7 @@ impl BotSettings { script_apply_eval!(cx, switch_state_label, { text: #(tr_key(self.app_language, "settings.labs.app_service.status.enabled")), draw_text +: { - color: mod.widgets.COLOR_FG_ACCEPT_GREEN + color: mod.widgets.COLOR_ACTIVE_PRIMARY } }); } else { diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index ef4564537..b9e7289a0 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -104,24 +104,37 @@ script_mod! { visible: false width: Fill, height: Fit flow: Down - spacing: 8 + spacing: (SPACE_SM) preferences_language_title := TitleLabel { text: "Language" } - preferences_application_language_label := SubsectionLabel { - text: "Application language" - } + // --- Language card --- + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_XS)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } - // Custom language selector: button + popup list - // (replaces DropDown which has unsolvable arrow shader artifact) - language_selector_button := RoundedView { - width: 200, height: Fit - flow: Right - align: Align{y: 0.5} - padding: Inset{left: 12, right: 10, top: 10, bottom: 10} - margin: Inset{left: 5, top: 2, bottom: 2} + preferences_application_language_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Application language" + } + + // Custom language selector: button + popup list + // (replaces DropDown which has unsolvable arrow shader artifact) + language_selector_button := RoundedView { + width: 200, height: Fit + flow: Right + align: Align{y: 0.5} + padding: Inset{left: (SPACE_MD), right: 10, top: 10, bottom: 10} + margin: Inset{left: (SPACE_XS), top: 2, bottom: 2} cursor: MouseCursor.Hand show_bg: true draw_bg +: { @@ -198,33 +211,64 @@ script_mod! { } } - preferences_language_hint_label := Label { - width: Fill - height: Fit - margin: Inset{left: 5, right: 8, top: 3, bottom: 4} - draw_text +: { - color: (MESSAGE_TEXT_COLOR) - text_style: REGULAR_TEXT { font_size: 10.5 } + preferences_language_hint_label := Label { + width: Fill + height: Fit + margin: Inset{left: (SPACE_XS), right: (SPACE_SM), top: 3, bottom: (SPACE_XS)} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "The app will reload after selecting another language" } - text: "The app will reload after selecting another language" - } + } // end Language card } labs_settings_section := View { visible: false width: Fill, height: Fit flow: Down + spacing: (SPACE_SM) - bot_settings := BotSettings {} - - LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } - - translation_settings := TranslationSettings {} + // --- App Service card --- + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + bot_settings := BotSettings {} + } - LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + // --- Translation card --- + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + translation_settings := TranslationSettings {} + } - // The TSP wallet settings section. - tsp_settings_screen := TspSettingsScreen {} + // --- TSP card --- + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + // The TSP wallet settings section. + tsp_settings_screen := TspSettingsScreen {} + } } } } diff --git a/src/settings/translation_settings.rs b/src/settings/translation_settings.rs index fa74dfb80..cfbccc601 100644 --- a/src/settings/translation_settings.rs +++ b/src/settings/translation_settings.rs @@ -18,15 +18,14 @@ script_mod! { width: Fill height: Fit flow: Down - spacing: 10 + spacing: (SPACE_SM) translation_header := View { width: Fill height: Fit - flow: Right - align: Align{y: 1.0} - spacing: 8 - margin: Inset{left: 5, right: 8, bottom: 2} + flow: Down + spacing: (SPACE_XS) + margin: Inset{left: (SPACE_XS), right: (SPACE_SM), bottom: 2} translation_title := TitleLabel { width: Fit @@ -38,7 +37,7 @@ script_mod! { height: Fit margin: 0 draw_text +: { - color: #x7A7A7A + color: (COLOR_DESCRIPTION_TEXT) text_style: REGULAR_TEXT { font_size: 9.5 } } text: "Configure an OpenAI-compatible API for real-time message translation in the input bar." @@ -50,17 +49,20 @@ script_mod! { height: Fit flow: Right align: Align{x: 0.0, y: 0.5} - spacing: 8 - margin: Inset{left: 5, bottom: 2} + spacing: (SPACE_SM) + margin: Inset{left: (SPACE_XS), bottom: 2} translation_switch := Toggle { width: Fit height: Fit - padding: Inset{top: 8, right: 8, bottom: 8, left: 8} + padding: Inset{top: (SPACE_SM), right: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_SM)} text: "" active: false draw_bg +: { size: 20.0 + color_active: (COLOR_ACTIVE_PRIMARY) + border_color_active: (COLOR_ACTIVE_PRIMARY) + mark_color_active: #fff } } @@ -68,7 +70,7 @@ script_mod! { width: Fit height: Fit draw_text +: { - color: #999 + color: (COLOR_DISABLED_TEXT) text_style: REGULAR_TEXT { font_size: 10.5 } } text: "Disabled" @@ -80,8 +82,8 @@ script_mod! { width: Fill height: Fit flow: Down - spacing: 8 - margin: Inset{left: 5, right: 8} + spacing: (SPACE_SM) + margin: Inset{left: (SPACE_XS), right: (SPACE_SM)} View { width: Fill, height: Fit @@ -89,7 +91,7 @@ script_mod! { api_url_label := Label { width: Fit, height: Fit draw_text +: { - color: #555 + color: (COLOR_FIELD_LABEL) text_style: REGULAR_TEXT { font_size: 10 } } text: "API URL" @@ -107,7 +109,7 @@ script_mod! { api_key_label := Label { width: Fit, height: Fit draw_text +: { - color: #555 + color: (COLOR_FIELD_LABEL) text_style: REGULAR_TEXT { font_size: 10 } } text: "API Key" @@ -126,7 +128,7 @@ script_mod! { model_label := Label { width: Fit, height: Fit draw_text +: { - color: #555 + color: (COLOR_FIELD_LABEL) text_style: REGULAR_TEXT { font_size: 10 } } text: "Model" @@ -141,8 +143,8 @@ script_mod! { View { width: Fill, height: Fit flow: Right - spacing: 8 - margin: Inset{top: 4} + spacing: (SPACE_SM) + margin: Inset{top: (SPACE_XS)} save_button := RobrixIconButton { padding: Inset{top: 8, bottom: 8, left: 16, right: 16} @@ -160,10 +162,10 @@ script_mod! { test_result_label := Label { width: Fit, height: Fit - margin: Inset{left: 8} + margin: Inset{left: (SPACE_SM)} align: Align{y: 0.5} draw_text +: { - color: #x999999 + color: (COLOR_DISABLED_TEXT) text_style: REGULAR_TEXT { font_size: 10 } } text: "" @@ -373,7 +375,7 @@ impl TranslationSettings { script_apply_eval!(cx, switch_state_label, { text: #(tr_key(self.app_language, "settings.labs.translation.status.enabled")), draw_text +: { - color: mod.widgets.COLOR_FG_ACCEPT_GREEN + color: mod.widgets.COLOR_ACTIVE_PRIMARY } }); } else { diff --git a/src/shared/styles.rs b/src/shared/styles.rs index 1f401d7a9..d534b4165 100644 --- a/src/shared/styles.rs +++ b/src/shared/styles.rs @@ -205,6 +205,9 @@ script_mod! { mod.widgets.COLOR_DROPDOWN_POPUP_BORDER = #xD3E1F6 // popup border (slightly lighter) mod.widgets.COLOR_DROPDOWN_ARROW = #x888888 // dropdown arrow icon mod.widgets.COLOR_INACTIVE_BORDER = #xBBBBBB // inactive account entry border + mod.widgets.COLOR_DESCRIPTION_TEXT = #x7A7A7A // secondary description text + mod.widgets.COLOR_FIELD_LABEL = #x555555 // form field labels + mod.widgets.COLOR_DISABLED_TEXT = #x999999 // disabled/inactive state text // Settings screen layout mod.widgets.SETTINGS_CONTENT_PADDING = 16 From dd190fb0618c5534a10b1e45d796b0b6eb55e6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:47:37 +0800 Subject: [PATCH 142/283] fix(room-screen): stabilize room-info modal cancel and tab scoping (#75) --- .../task-room-info-modal-cancel-flow.spec.md | 113 ++++++++++++++++++ src/home/room_screen.rs | 61 +++++++++- 2 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 specs/task-room-info-modal-cancel-flow.spec.md diff --git a/specs/task-room-info-modal-cancel-flow.spec.md b/specs/task-room-info-modal-cancel-flow.spec.md new file mode 100644 index 000000000..65da07342 --- /dev/null +++ b/specs/task-room-info-modal-cancel-flow.spec.md @@ -0,0 +1,113 @@ +spec: task +name: "Room Info Modal Cancel Flow Consistency" +inherits: project +tags: [bugfix, room-info, modal, ui, navigation] +estimate: 1d +--- + +## Intent + +Fix the Room Info panel interaction bug where opening `Report Room` or `Leave Room` shows a modal, but clicking `Cancel` first collapses the Room Info pane and only a second cancel closes the modal. Also fix modal lifetime so these dialogs do not remain visible after switching to a different room. +The issue is in the Room Screen overlay interaction between `RoomInfoSlidingPane`, `ReportRoomModal`, and the leave-room `NegativeConfirmationModal`. Current behavior indicates modal cancel/dismiss actions are not fully isolated from underlying pane interactions, causing UI state desynchronization (pane closes unexpectedly, modal survives room switch). + +## Constraints + +- Keep existing action entry points from Room Info (`ReportRoom`, `LeaveRoom`) +- Keep Matrix async request path unchanged: + - `submit_async_request(MatrixRequest::ReportRoom { ... })` + - `submit_async_request(MatrixRequest::LeaveRoom { ... })` +- Do not change unrelated Room Info actions (`Invite`, `People`, profile navigation) +- Do not redesign room navigation architecture + +## Decisions + +- Modal cancel must be single-step: one cancel action closes the active modal immediately +- Canceling or dismissing modal must not close the Room Info pane +- Modal visibility is scoped to the currently displayed room: + - switching room closes any open report/leave modal + - modal must not remain visible in the newly selected room +- Room switching while modal is open must not submit report/leave requests +- Existing submit semantics remain unchanged: + - report submit sends `ReportRoom` and closes modal + - leave confirm sends `LeaveRoom` and closes modal + +## Boundaries + +### Allowed Changes +- `src/home/room_screen.rs` +- `src/shared/confirmation_modal.rs` (only if needed for dismiss/cancel event handling) + +### Forbidden +- Do not add new dependencies +- Do not change Matrix request payload format or backend behavior +- Do not change unrelated Room Info layout/content +- Do not run `cargo fmt` or reformat unrelated code + +## Acceptance Criteria + +Scenario: Cancel report modal in one click without collapsing Room Info pane + Test: manual_test_room_info_report_cancel_single_step + Given the user opens Room Info for a room + And the user opens the `Report Room` modal + When the user clicks `Cancel` + Then the report modal closes immediately + And the Room Info pane remains open + +Scenario: Cancel leave modal in one click without collapsing Room Info pane + Test: manual_test_room_info_leave_cancel_single_step + Given the user opens Room Info for a room + And the user opens the `Leave Room` confirm modal + When the user clicks `Cancel` + Then the leave modal closes immediately + And the Room Info pane remains open + +Scenario: Dismiss modal via backdrop or dismiss action does not close Room Info pane + Test: manual_test_room_info_modal_dismiss_keeps_info_open + Level: manual + Targets: room_screen_modal_lifecycle + Given a report or leave modal is open from Room Info + When the modal is dismissed via non-submit close path + Then the modal closes + And the Room Info pane stays open + +Scenario: Switching room closes modal and prevents cross-room modal leakage + Test: manual_test_room_info_modal_closed_on_room_switch + Given a report or leave modal is open in room A + When the user switches to room B + Then no report/leave modal is visible in room B + And room B interaction is not blocked by stale modal overlay + +Scenario: Returning to original room does not resurrect stale modal + Test: manual_test_room_info_modal_not_restored_after_room_switch + Given a modal was open in room A and the user switched to room B + When the user switches back to room A + Then the previous report/leave modal is not auto-reopened + +Scenario: Submit behavior remains unchanged + Test: manual_test_room_info_modal_submit_semantics_unchanged + Given the user opens report/leave modal from Room Info + When the user confirms the action + Then report confirmation calls `submit_async_request(MatrixRequest::ReportRoom { ... })` exactly once + And leave confirmation calls `submit_async_request(MatrixRequest::LeaveRoom { ... })` exactly once + And the modal closes + +Scenario: Room Info pane close behavior still works when no modal is active + Test: manual_test_room_info_pane_close_regression_guard + Given the Room Info pane is open and no modal is active + When the user performs the pane close action + Then the Room Info pane closes as before + +Scenario: Report with empty reason does not submit and shows validation + Test: manual_test_room_info_report_empty_reason_validation + Given the user opens the `Report Room` modal + And the reason input is empty + When the user clicks the report/submit button + Then no `submit_async_request(MatrixRequest::ReportRoom { ... })` request is sent + And a validation error is shown in the modal + And the modal remains open + +## Out of Scope + +- Changing copy/text/visual design of report or leave dialogs +- Adding telemetry for cancel/dismiss events +- Refactoring modal framework shared by unrelated screens diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index f15d72782..222e9d840 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,7 +1,7 @@ //! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline //! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc, time::Duration}; +use std::{borrow::Cow, cell::{Cell, RefCell}, ops::{DerefMut, Range}, sync::Arc, time::Duration}; use bytesize::ByteSize; use hashbrown::{HashMap, HashSet}; @@ -68,6 +68,18 @@ const TRANSLATION_LANG_POPUP_HEIGHT: f64 = TRANSLATION_LANG_POPUP_SCROLL_HEIGHT const TRANSLATION_LANG_POPUP_GAP: f64 = 6.0; const TRANSLATION_LANG_POPUP_MARGIN: f64 = 8.0; +thread_local! { + static ROOM_INFO_ACTION_MODAL_OPEN: Cell = const { Cell::new(false) }; +} + +fn set_room_info_action_modal_open(open: bool) { + ROOM_INFO_ACTION_MODAL_OPEN.with(|state| state.set(open)); +} + +fn is_room_info_action_modal_open() -> bool { + ROOM_INFO_ACTION_MODAL_OPEN.with(|state| state.get()) +} + /// #FFF4E5 const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); @@ -2437,7 +2449,7 @@ impl Widget for RoomInfoSlidingPane { } let area = self.view.area(); - let close_pane = if is_invite_modal_open() { + let close_pane = if is_invite_modal_open() || is_room_info_action_modal_open() { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) @@ -2856,8 +2868,14 @@ impl Widget for RoomScreen { let portal_list = self.portal_list(cx, ids!(timeline.list)); let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let threads_sliding_pane = self.threads_sliding_pane(cx, ids!(threads_sliding_pane)); + let threads_sliding_pane_widget_uid = threads_sliding_pane.widget_uid(); let room_info_sliding_pane = self.room_info_sliding_pane(cx, ids!(room_info_sliding_pane)); + let room_info_sliding_pane_widget_uid = room_info_sliding_pane.widget_uid(); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); + set_room_info_action_modal_open( + self.view.modal(cx, ids!(report_room_modal)).is_open() + || self.view.modal(cx, ids!(leave_room_confirm_modal)).is_open() + ); // Streaming animation frame handler if let Some(_ne) = self.streaming_next_frame.is_event(event) { @@ -3069,6 +3087,23 @@ impl Widget for RoomScreen { self.handle_message_actions(cx, actions, &portal_list, &loading_pane); for action in actions { + if let Some(RoomsListAction::Selected(selected_room)) = action.downcast_ref() { + if self.timeline_kind.as_ref() != selected_room.timeline_kind().as_ref() { + self.close_report_room_modal(cx); + self.close_leave_room_confirm_modal(cx); + } + } + if let Some(AppStateAction::RoomFocused(selected_room)) = action.downcast_ref() { + if self.timeline_kind.as_ref() != selected_room.timeline_kind().as_ref() { + self.close_report_room_modal(cx); + self.close_leave_room_confirm_modal(cx); + } + } + if let Some(AppStateAction::FocusNone) = action.downcast_ref() { + self.close_report_room_modal(cx); + self.close_leave_room_confirm_modal(cx); + } + // Handle actions related to restoring the previously-saved state of rooms. if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, ..}) = action.downcast_ref() { if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_name_id.room_id()) { @@ -3148,7 +3183,11 @@ impl Widget for RoomScreen { } } - match action.as_widget_action().cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(threads_sliding_pane_widget_uid) + .cast_ref() + { ThreadsPaneAction::OpenThread(thread_root_event_id) => { let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { continue }; threads_sliding_pane.hide(cx); @@ -3166,7 +3205,11 @@ impl Widget for RoomScreen { ThreadsPaneAction::None => {} } - match action.as_widget_action().cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(room_info_sliding_pane_widget_uid) + .cast_ref() + { RoomInfoPaneAction::InviteUser => { if let Some(room_name_id) = self.room_name_id.as_ref().cloned() { cx.action(InviteModalAction::Open(room_name_id)); @@ -3334,9 +3377,15 @@ impl Widget for RoomScreen { // We check which overlay views are visible in the order of those views' z-ordering, // such that the top-most views get a chance to handle the event first. // + let room_info_action_modal_open = + self.view.modal(cx, ids!(report_room_modal)).is_open() + || self.view.modal(cx, ids!(leave_room_confirm_modal)).is_open(); let is_interactive_hit = utils::is_interactive_hit_event(event); let is_pane_shown: bool; - if loading_pane.is_currently_shown(cx) { + if room_info_action_modal_open { + is_pane_shown = true; + } + else if loading_pane.is_currently_shown(cx) { is_pane_shown = true; if is_interactive_hit { loading_pane.handle_event(cx, event, scope); @@ -3369,7 +3418,7 @@ impl Widget for RoomScreen { // Makepad already delivers most events to all views regardless of visibility, // so the only thing we'd need here is the conditional below. - if !is_pane_shown || !is_interactive_hit { + if room_info_action_modal_open || !is_pane_shown || !is_interactive_hit { // Create a Scope with RoomScreenProps containing the room members. // This scope is needed by child widgets like MentionableTextInput during event handling. let room_props = if let Some(tl) = self.tl_state.as_ref() { From 38b2fb2325bb01efb34fbef5d357060c76a8205d Mon Sep 17 00:00:00 2001 From: Alvin <48358093+TigerInYourDream@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:41:20 +0800 Subject: [PATCH 143/283] ui: replace remaining magic numbers with SPACE_* constants in settings (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ui: replace remaining magic numbers with SPACE_* constants in settings Convert hardcoded padding/margin/spacing values to design system constants in account_settings.rs and settings_screen.rs: - 10 → SPACE_SM, 12 → SPACE_MD, 15 → SPACE_LG - 5 → SPACE_XS, 4 → SPACE_XS, 6 → SPACE_SM Closes #71 * ui: fix vertical alignment in Labs tab cards Remove inner left margins from BotSettings and TranslationSettings components — the parent card container's padding already provides consistent left alignment. Double indentation (card padding + inner margin) caused misaligned content within each card. * ui: align toggle pill with title text in Labs cards Add left padding (6px) to toggle_row to align the Toggle pill with the title/description text. Reduce toggle-to-label spacing from SPACE_SM to SPACE_XS for tighter visual grouping. --- src/settings/account_settings.rs | 22 +++++++++++----------- src/settings/bot_settings.rs | 9 +++++---- src/settings/settings_screen.rs | 10 +++++----- src/settings/translation_settings.rs | 9 +++++---- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index fad2b3a27..9b246ff89 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -51,7 +51,7 @@ script_mod! { our_own_avatar := Avatar { width: 100, height: 100, - margin: 10, + margin: (SPACE_SM), text_view +: { text +: { draw_text +: { @@ -77,7 +77,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} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} margin: 0, draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_UPLOAD) @@ -101,7 +101,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} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} margin: 0, draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_TRASH) @@ -155,7 +155,7 @@ script_mod! { cancel_display_name_button := RobrixNeutralIconButton { enabled: false, width: Fit, height: Fit, - padding: 10, + padding: (SPACE_SM), margin: Inset{left: (SPACE_XS)}, draw_icon.svg: (ICON_FORBIDDEN) icon_walk: Walk{width: 16, height: 16, margin: 0} @@ -165,7 +165,7 @@ script_mod! { accept_display_name_button := RobrixPositiveIconButton { enabled: false, width: Fit, height: Fit, - padding: 10, + padding: (SPACE_SM), margin: Inset{left: (SPACE_XS)}, draw_bg.border_radius: (RADIUS_MD) draw_icon.svg: (ICON_CHECKMARK) @@ -175,7 +175,7 @@ script_mod! { save_name_spinner := LoadingSpinner { width: 16, height: 16 - margin: Inset{left: 5, top: 13} // vertically center with buttons + margin: Inset{left: (SPACE_XS), top: 13} // vertically center with buttons visible: false draw_bg.color: (COLOR_ACTIVE_PRIMARY) } @@ -277,7 +277,7 @@ script_mod! { // Other accounts section (populated dynamically) other_accounts_label := Label { width: Fill, height: Fit - margin: Inset{top: (SPACE_XS), left: 2} + margin: Inset{top: (SPACE_XS), left: (SPACE_XS)} visible: false draw_text +: { color: (MESSAGE_TEXT_COLOR), @@ -319,7 +319,7 @@ script_mod! { switch_account_button := RobrixIconButton { width: Fit, height: Fit - padding: Inset{top: 6, bottom: 6, left: 10, right: 10} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_SM), right: (SPACE_SM)} draw_icon.svg: (ICON_JUMP) icon_walk: Walk{width: 14, height: 14} text: "Switch" @@ -338,7 +338,7 @@ script_mod! { add_account_button := RobrixIconButton { width: Fit, - padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} margin: Inset{top: (SPACE_XS)} draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_ADD) @@ -361,7 +361,7 @@ script_mod! { margin: Inset{bottom: (SPACE_LG)} manage_account_button := RobrixIconButton { - padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} margin: Inset{left: (SPACE_XS)} draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_EXTERNAL_LINK) @@ -370,7 +370,7 @@ script_mod! { } logout_button := RobrixNegativeIconButton { - padding: Inset{top: 10, bottom: 10, left: (SPACE_MD), right: 15} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} margin: Inset{left: (SPACE_XS)} draw_bg +: { border_radius: (RADIUS_MD) } draw_icon.svg: (ICON_LOGOUT) diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index 93a7aaafb..ac3ac2bda 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -14,7 +14,7 @@ script_mod! { mod.widgets.BotSettingsInfoLabel = Label { width: Fill height: Fit - margin: Inset{left: (SPACE_XS), top: 2, bottom: 2} + margin: Inset{top: 2, bottom: 2} draw_text +: { color: (MESSAGE_TEXT_COLOR) text_style: REGULAR_TEXT { font_size: 10.5 } @@ -33,7 +33,7 @@ script_mod! { height: Fit flow: Down spacing: (SPACE_XS) - margin: Inset{left: (SPACE_XS), right: (SPACE_SM), bottom: 2} + margin: Inset{bottom: 2} app_service_title := TitleLabel { width: Fit @@ -56,8 +56,9 @@ script_mod! { height: Fit flow: Right align: Align{x: 0.0, y: 0.5} - spacing: (SPACE_SM) - margin: Inset{left: (SPACE_XS), bottom: 2} + spacing: (SPACE_XS) + padding: Inset{left: 6} + margin: Inset{bottom: 2} app_service_switch := Toggle { width: Fit diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index b9e7289a0..30d1bd965 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -133,7 +133,7 @@ script_mod! { width: 200, height: Fit flow: Right align: Align{y: 0.5} - padding: Inset{left: (SPACE_MD), right: 10, top: 10, bottom: 10} + padding: Inset{left: (SPACE_MD), right: (SPACE_SM), top: (SPACE_SM), bottom: (SPACE_SM)} margin: Inset{left: (SPACE_XS), top: 2, bottom: 2} cursor: MouseCursor.Hand show_bg: true @@ -165,7 +165,7 @@ script_mod! { visible: false width: 200, height: Fit flow: Down - padding: Inset{top: 4, bottom: 4} + padding: Inset{top: (SPACE_XS), bottom: (SPACE_XS)} show_bg: true new_batch: true draw_bg +: { @@ -179,7 +179,7 @@ script_mod! { width: Fill, height: 36 flow: Right align: Align{y: 0.5} - padding: Inset{left: 12, right: 12} + padding: Inset{left: (SPACE_MD), right: (SPACE_MD)} cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: #0000 } @@ -196,7 +196,7 @@ script_mod! { width: Fill, height: 36 flow: Right align: Align{y: 0.5} - padding: Inset{left: 12, right: 12} + padding: Inset{left: (SPACE_MD), right: (SPACE_MD)} cursor: MouseCursor.Hand show_bg: true draw_bg +: { color: #0000 } @@ -214,7 +214,7 @@ script_mod! { preferences_language_hint_label := Label { width: Fill height: Fit - margin: Inset{left: (SPACE_XS), right: (SPACE_SM), top: 3, bottom: (SPACE_XS)} + margin: Inset{left: (SPACE_XS), right: (SPACE_SM), top: (SPACE_XS), bottom: (SPACE_XS)} draw_text +: { color: (MESSAGE_TEXT_COLOR) text_style: REGULAR_TEXT { font_size: 10.5 } diff --git a/src/settings/translation_settings.rs b/src/settings/translation_settings.rs index cfbccc601..7c6b0e6c5 100644 --- a/src/settings/translation_settings.rs +++ b/src/settings/translation_settings.rs @@ -25,7 +25,7 @@ script_mod! { height: Fit flow: Down spacing: (SPACE_XS) - margin: Inset{left: (SPACE_XS), right: (SPACE_SM), bottom: 2} + margin: Inset{bottom: 2} translation_title := TitleLabel { width: Fit @@ -49,8 +49,9 @@ script_mod! { height: Fit flow: Right align: Align{x: 0.0, y: 0.5} - spacing: (SPACE_SM) - margin: Inset{left: (SPACE_XS), bottom: 2} + spacing: (SPACE_XS) + padding: Inset{left: 6} + margin: Inset{bottom: 2} translation_switch := Toggle { width: Fit @@ -83,7 +84,7 @@ script_mod! { height: Fit flow: Down spacing: (SPACE_SM) - margin: Inset{left: (SPACE_XS), right: (SPACE_SM)} + margin: 0 View { width: Fill, height: Fit From 4ccc6ae3b31800cf5857825e624c6daa46181912 Mon Sep 17 00:00:00 2001 From: Alvin <48358093+TigerInYourDream@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:53:14 +0800 Subject: [PATCH 144/283] ui: polish Manage Room Bots modal styling (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lighten form background from COLOR_SECONDARY (#E3E3E3) to #F5F5F7 - Unify dropdown text to 11.5px matching input fields - Add vertical centering (align + padding) to DropDownFlat widgets - Override dropdown colors for light background readability - Soften field labels from #333 to #666 for better hierarchy - Increase title font from 13px to 14px - Widen form padding (14→16) and modal spacing (16→18) - Adjust button widths and spacing for visual balance --- src/home/bot_binding_modal.rs | 64 ++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/src/home/bot_binding_modal.rs b/src/home/bot_binding_modal.rs index 1316b0465..a8e6dfa6c 100644 --- a/src/home/bot_binding_modal.rs +++ b/src/home/bot_binding_modal.rs @@ -21,7 +21,7 @@ script_mod! { height: Fit draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } - color: #333 + color: #666 } text: "" } @@ -36,7 +36,7 @@ script_mod! { align: Align{x: 0.5} flow: Down padding: Inset{top: 28, right: 24, bottom: 20, left: 24} - spacing: 16 + spacing: 18 show_bg: true draw_bg +: { @@ -48,7 +48,7 @@ script_mod! { width: Fill height: Fit draw_text +: { - text_style: TITLE_TEXT { font_size: 13 } + text_style: TITLE_TEXT { font_size: 14 } color: #000 } text: "Manage Room Bots" @@ -63,12 +63,12 @@ script_mod! { height: Fit flow: Down spacing: 12 - padding: 14 + padding: 16 show_bg: true draw_bg +: { - color: (COLOR_SECONDARY) - border_radius: 4.0 + color: #F5F5F7 + border_radius: 6.0 } current_room_bots_label := mod.widgets.BotBindingModalLabel { @@ -78,6 +78,26 @@ script_mod! { current_room_bots_dropdown := DropDownFlat { width: Fill height: 40 + align: Align{y: 0.5} + padding: Inset{left: 12, top: 11, bottom: 11, right: 30} + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #333 + color_hover: uniform(#222) + color_focus: uniform(#222) + color_down: uniform(#222) + } + draw_bg +: { + color: uniform(#fff) + color_hover: uniform(#F0F0F2) + color_focus: uniform(#F0F0F2) + color_down: uniform(#E8E8EA) + border_color: uniform(#CCC) + border_color_hover: uniform(#AAA) + border_color_focus: uniform((COLOR_ACTIVE_PRIMARY)) + arrow_color: uniform(#888) + arrow_color_hover: uniform(#555) + } labels: ["No bots currently added"] } @@ -88,6 +108,26 @@ script_mod! { known_bots_dropdown := DropDownFlat { width: Fill height: 40 + align: Align{y: 0.5} + padding: Inset{left: 12, top: 11, bottom: 11, right: 30} + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #333 + color_hover: uniform(#222) + color_focus: uniform(#222) + color_down: uniform(#222) + } + draw_bg +: { + color: uniform(#fff) + color_hover: uniform(#F0F0F2) + color_focus: uniform(#F0F0F2) + color_down: uniform(#E8E8EA) + border_color: uniform(#CCC) + border_color_hover: uniform(#AAA) + border_color_focus: uniform((COLOR_ACTIVE_PRIMARY)) + arrow_color: uniform(#888) + arrow_color_hover: uniform(#555) + } labels: ["Custom bot user ID"] } @@ -98,7 +138,7 @@ script_mod! { user_id_input := RobrixTextInput { width: Fill height: Fit - padding: 10 + padding: 12 draw_text +: { text_style: REGULAR_TEXT { font_size: 11.5 } color: #000 @@ -113,7 +153,7 @@ script_mod! { remark_input := RobrixTextInput { width: Fill height: Fit - padding: 10 + padding: 12 draw_text +: { text_style: REGULAR_TEXT { font_size: 11.5 } color: #000 @@ -153,10 +193,10 @@ script_mod! { height: Fit flow: Right align: Align{x: 1.0, y: 0.5} - spacing: 12 + spacing: 14 cancel_button := RobrixNeutralIconButton { - width: 110 + width: 100 align: Align{x: 0.5, y: 0.5} padding: 12 draw_icon.svg: (ICON_FORBIDDEN) @@ -165,7 +205,7 @@ script_mod! { } unbind_button := RobrixNegativeIconButton { - width: 128 + width: 120 align: Align{x: 0.5, y: 0.5} padding: 12 draw_icon.svg: (ICON_CLOSE) @@ -174,7 +214,7 @@ script_mod! { } bind_button := RobrixPositiveIconButton { - width: 128 + width: 120 align: Align{x: 0.5, y: 0.5} padding: 12 draw_icon.svg: (ICON_ADD_USER) From 71da563522f16aa1c5e395eaaaa5f59306969a6d Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 11 Apr 2026 14:07:38 +0800 Subject: [PATCH 145/283] updated ROADMAP --- roadmap/bot-features-gap-analysis-en.md | 99 +++++++++++++++++++++++++ roadmap/bot-features-gap-analysis-zh.md | 99 +++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 roadmap/bot-features-gap-analysis-en.md create mode 100644 roadmap/bot-features-gap-analysis-zh.md diff --git a/roadmap/bot-features-gap-analysis-en.md b/roadmap/bot-features-gap-analysis-en.md new file mode 100644 index 000000000..31ecdc0c3 --- /dev/null +++ b/roadmap/bot-features-gap-analysis-en.md @@ -0,0 +1,99 @@ +# Robrix2 Bot Features vs Telegram Bot Features — Gap Analysis + +> Analysis date: 2026-04-11 +> Reference: https://core.telegram.org/bots/features + +## Current Robrix2 Bot Features + +| Category | Feature | Status | +|----------|---------|--------| +| Bot Lifecycle | Create Bot (`/createbot`) | :white_check_mark: | +| | Delete Bot (`/deletebot`) | :white_check_mark: | +| | List Bots (`/listbots`) | :white_check_mark: | +| | Bot Help (`/bothelp`) | :white_check_mark: | +| Room Binding | Bind/unbind Bot to rooms | :white_check_mark: | +| | Invite/kick Bot (Matrix SDK) | :white_check_mark: | +| | Multiple Bots per room | :white_check_mark: | +| Bot Discovery | Auto-detect Bot members in rooms | :white_check_mark: | +| | Parse `/listbots` replies to extract Bot IDs | :white_check_mark: | +| | Heuristic Bot username recognition | :white_check_mark: | +| Settings | App Service toggle | :white_check_mark: | +| | BotFather user ID configuration | :white_check_mark: | +| | Per-room Bot remarks | :white_check_mark: | +| | Persistent storage | :white_check_mark: | +| UI | AppServicePanel (action panel) | :white_check_mark: | +| | Create/Delete/Bind Bot modals | :white_check_mark: | +| | "Manage Bots" in room context menu | :white_check_mark: | +| | i18n (English & Chinese) | :white_check_mark: | + +--- + +## Gap Analysis with Telegram + +### P0 — Missing Core Interaction Capabilities + +| Telegram Feature | Description | Robrix2 Status | Gap | +|------------------|-------------|----------------|-----| +| **Bot Commands** | User types `/` to see a command list; tap to send | :x: No command discovery | Bots cannot declare supported commands to the client; users must type manually | +| **Inline Keyboards** | Clickable buttons below messages (callback, URL, switch, etc.) | :x: None | Bot messages cannot carry interactive buttons; users can only reply with plain text | +| **Reply Keyboards** | Bot replaces user keyboard with preset options | :x: None | Cannot guide users to choose from fixed options | +| **Callback Queries** | User taps an Inline button; Bot receives callback and can update the message | :x: None | No structured interaction loop between Bot and user | + +### P1 — Missing Important UX Features + +| Telegram Feature | Description | Robrix2 Status | Gap | +|------------------|-------------|----------------|-----| +| **Inline Mode** | `@bot_name query` in any chat triggers Bot results | :x: None | Can only interact in the Bot's room; no cross-room invocation | +| **Deep Linking** | Link `t.me/bot?start=param` passes parameters to start Bot | :x: None | Cannot share Bot via links with context | +| **Bot Profile** | Bot About, Description, avatar, description image | :warning: Partial | Relies on Matrix profile; no dedicated Bot profile editing UI | +| **Menu Button** | Bot menu button in chat window | :x: None | No dedicated entry point to quickly access Bot functions | +| **Bot-to-Bot Communication** | Bots can interact with each other | :x: None | No Bot orchestration or chaining capability | +| **Privacy Mode** | In groups, Bot only receives `/command` and replies by default | :warning: Matrix-dependent | Matrix has no equivalent concept; Bots receive all messages by default | + +### P2 — Missing Advanced / Monetization Features + +| Telegram Feature | Description | Robrix2 Status | +|------------------|-------------|----------------| +| **Payments / Stars** | Built-in payment flow, digital currency | :x: None | +| **Mini Apps (Web Apps)** | Bot-embedded JS web applications | :x: None | +| **HTML5 Games** | Gaming platform with leaderboards | :x: None | +| **Stickers / Custom Emoji** | Bot-created sticker packs | :x: None | +| **Paid Media / Subscriptions** | Paid content, tiered subscriptions | :x: None | +| **Ad Revenue Sharing** | Share ad revenue with bots | :x: None | +| **Web Login** | Bot-powered third-party website authentication | :x: None | +| **Managed Bots** | Manage other bots on behalf of owners | :x: None | +| **Bots for Business** | Enterprise customer service bot mode | :x: None | +| **Attachment Menu** | Invoke Bot directly from attachment menu | :x: None | +| **i18n Auto-adaptation** | Bot auto-switches language based on user locale | :x: None | +| **Bot Health Monitoring** | Reply rate and response time alerts | :x: None | + +--- + +## Recommended Roadmap + +### Phase 1 — Make Bots Truly Interactive + +1. **Bot Command Declaration & Discovery** — Bots register command lists; show available commands when user types `/` +2. **Inline Keyboards (Buttons)** — Messages carry clickable buttons with callback support +3. **Callback Mechanism** — Button taps trigger Bot callbacks and message updates + +### Phase 2 — Extend Bot Reach + +4. **Inline Mode** — `@bot` cross-room queries +5. **Deep Linking** — Share Bots via links with parameters +6. **Bot Menu Button** — Dedicated Bot menu entry in chat window +7. **Bot Profile Editing** — Dedicated About/Description/Avatar management + +### Phase 3 — Platform Features + +8. Mini Apps / Web Apps +9. Payment Integration +10. Bot-to-Bot Communication & Orchestration + +--- + +## Summary + +Robrix2's current Bot functionality is concentrated at the **management layer** (create, delete, bind, discover) — equivalent to Telegram's BotFather management portion. However, at the **user interaction layer** (Commands, Keyboards, Inline Mode, Callbacks) it is nearly zero — which is precisely the core of Telegram's Bot ecosystem. + +The biggest gap is not the absence of advanced features (payments, games, etc.), but the **lack of structured interaction between Bots and users**. Users can only send plain text to Bots, and Bots can only reply with plain text — no buttons, no command menus, no callback updates. This severely limits Bot utility. diff --git a/roadmap/bot-features-gap-analysis-zh.md b/roadmap/bot-features-gap-analysis-zh.md new file mode 100644 index 000000000..0c579cb06 --- /dev/null +++ b/roadmap/bot-features-gap-analysis-zh.md @@ -0,0 +1,99 @@ +# Robrix2 Bot 功能 vs Telegram Bot 功能差距分析 + +> 分析日期: 2026-04-11 +> 参考文档: https://core.telegram.org/bots/features + +## 当前 Robrix2 已有的功能 + +| 类别 | 功能 | 状态 | +|------|------|------| +| Bot 生命周期 | 创建 Bot (`/createbot`) | :white_check_mark: | +| | 删除 Bot (`/deletebot`) | :white_check_mark: | +| | 列出 Bot (`/listbots`) | :white_check_mark: | +| | Bot 帮助 (`/bothelp`) | :white_check_mark: | +| 房间绑定 | 绑定/解绑 Bot 到房间 | :white_check_mark: | +| | 邀请/踢出 Bot (Matrix SDK) | :white_check_mark: | +| | 多 Bot 绑定同一房间 | :white_check_mark: | +| Bot 发现 | 自动检测房间内 Bot 成员 | :white_check_mark: | +| | 解析 `/listbots` 回复提取 Bot ID | :white_check_mark: | +| | 启发式 Bot 用户名识别 | :white_check_mark: | +| 设置 | App Service 开关 | :white_check_mark: | +| | BotFather 用户 ID 配置 | :white_check_mark: | +| | 每房间 Bot 备注 | :white_check_mark: | +| | 持久化存储 | :white_check_mark: | +| UI | AppServicePanel (操作面板) | :white_check_mark: | +| | 创建/删除/绑定 Bot 模态框 | :white_check_mark: | +| | 房间右键菜单"管理 Bot" | :white_check_mark: | +| | i18n (中英文) | :white_check_mark: | + +--- + +## 与 Telegram 的差距 + +### P0 — 核心交互能力缺失 + +| Telegram 功能 | 描述 | Robrix2 现状 | 差距 | +|---------------|------|-------------|------| +| **Bot Commands** | 用户输入 `/` 弹出命令列表,点击即发送 | :x: 无命令发现机制 | Bot 无法向客户端声明自己支持哪些命令,用户只能手动输入 | +| **Inline Keyboards** | 消息下方显示可点击按钮(回调、URL、切换等) | :x: 无 | Bot 消息无法携带交互按钮,用户只能通过纯文本回复 | +| **Reply Keyboards** | Bot 替换用户键盘为预设选项 | :x: 无 | 无法引导用户从固定选项中选择 | +| **Callback Queries** | 用户点击 Inline 按钮后 Bot 收到回调并可更新消息 | :x: 无 | 缺少 Bot 与用户之间的结构化交互循环 | + +### P1 — 重要体验功能缺失 + +| Telegram 功能 | 描述 | Robrix2 现状 | 差距 | +|---------------|------|-------------|------| +| **Inline Mode** | 在任意聊天中 `@bot_name query` 触发 Bot 返回结果 | :x: 无 | 只能在 Bot 所在房间交互,不能跨房间调用 | +| **Deep Linking** | 通过链接 `t.me/bot?start=param` 传参启动 Bot | :x: 无 | 无法通过链接分享 Bot 并传递上下文 | +| **Bot Profile** | Bot 的 About、Description、头像、描述图片 | :warning: 部分 | 依赖 Matrix profile,无 Bot 专属 profile 编辑 UI | +| **Menu Button** | 聊天窗口的 Bot 菜单按钮 | :x: 无 | 无专属入口快速调出 Bot 功能 | +| **Bot-to-Bot 通信** | Bot 之间可以互相交互 | :x: 无 | 无 Bot 间编排/链式调用能力 | +| **Privacy Mode** | 群组中 Bot 默认只收到 `/command` 和 reply | :warning: 依赖 Matrix | Matrix 无对等概念,Bot 默认收到所有消息 | + +### P2 — 高级/商业化功能缺失 + +| Telegram 功能 | 描述 | Robrix2 现状 | +|---------------|------|-------------| +| **Payments / Stars** | 内置支付流程、数字货币 | :x: 无 | +| **Mini Apps (Web Apps)** | Bot 内嵌 JS Web 应用 | :x: 无 | +| **HTML5 Games** | 游戏平台,排行榜 | :x: 无 | +| **Stickers / Custom Emoji** | Bot 创建贴纸包 | :x: 无 | +| **Paid Media / Subscriptions** | 付费内容、订阅分层 | :x: 无 | +| **Ad Revenue Sharing** | 广告收入分成 | :x: 无 | +| **Web Login** | Bot 驱动的第三方网站认证 | :x: 无 | +| **Managed Bots** | 代管其他 Bot | :x: 无 | +| **Bots for Business** | 企业客服 Bot 模式 | :x: 无 | +| **Attachment Menu** | 附件菜单直接调用 Bot | :x: 无 | +| **i18n 自动适配** | Bot 根据用户语言自动切换 | :x: 无 | +| **Bot 健康监控** | 回复率、响应时间告警 | :x: 无 | + +--- + +## 建议的优先路线图 + +### Phase 1 — 让 Bot 真正"可交互" + +1. **Bot Commands 声明与发现** — Bot 注册命令列表,用户输入 `/` 时显示可用命令 +2. **Inline Keyboards (按钮)** — 消息附带可点击按钮,支持回调 +3. **Callback 机制** — 点击按钮后触发 Bot 回调并更新消息 + +### Phase 2 — 扩展 Bot 触达范围 + +4. **Inline Mode** — `@bot` 跨房间查询 +5. **Deep Linking** — 链接分享 Bot 并传参 +6. **Bot Menu Button** — 聊天窗口专属 Bot 菜单入口 +7. **Bot Profile 编辑** — 专属 About/Description/Avatar 管理 + +### Phase 3 — 平台化 + +8. Mini Apps / Web Apps +9. 支付集成 +10. Bot 间通信与编排 + +--- + +## 总结 + +Robrix2 目前的 Bot 功能集中在**管理层**(创建、删除、绑定、发现),相当于 Telegram 的 BotFather 管理部分。但在**用户交互层**(Commands、Keyboards、Inline Mode、Callback)几乎为零——这恰恰是 Telegram Bot 生态最核心的部分。 + +最大的差距不是缺少高级功能(支付、游戏等),而是**Bot 与用户之间缺乏结构化交互能力**。用户只能发纯文本给 Bot,Bot 也只能回纯文本,没有按钮、没有命令菜单、没有回调更新。这使得 Bot 的实用性大打折扣。 From d82ad42061f09cb571d6b914de040f21939eaac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:25:24 +0800 Subject: [PATCH 146/283] Add proxy support (#59) * Add proxy support * Update proxy support * fix(ui): improve proxy and logout modal responsiveness - keep login page proxy settings icon pinned to top-right - make login proxy modal width adaptive on small screens - make settings proxy section adaptive with desktop max width - make logout confirmation modal width adaptive * fix: remove login window size hooks and pin proxy settings icon * update cargo.lock --- Cargo.lock | 48 ++- Cargo.toml | 7 +- resources/i18n/en.json | 42 +++ resources/i18n/zh-CN.json | 42 +++ src/lib.rs | 1 + src/login/login_screen.rs | 501 ++++++++++++++++++++++++++++- src/logout/logout_confirm_modal.rs | 21 +- src/persistence/matrix_state.rs | 16 +- src/proxy_config.rs | 138 ++++++++ src/settings/settings_screen.rs | 402 ++++++++++++++++++++++- src/sliding_sync.rs | 39 ++- 11 files changed, 1226 insertions(+), 31 deletions(-) create mode 100644 src/proxy_config.rs diff --git a/Cargo.lock b/Cargo.lock index 62acc0128..01e06fe40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,7 +1830,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -2034,7 +2034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -2853,7 +2853,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", @@ -5685,7 +5685,7 @@ dependencies = [ [[package]] name = "robius-directories" version = "6.0.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ "dirs-sys 0.5.0", "jni", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "robius-location" version = "0.2.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ "android-build", "cfg-if", @@ -5710,7 +5710,7 @@ dependencies = [ [[package]] name = "robius-open" version = "0.2.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ "block2", "cfg-if", @@ -5724,6 +5724,20 @@ dependencies = [ "windows 0.56.0", ] +[[package]] +name = "robius-proxy" +version = "0.2.0" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" +dependencies = [ + "cfg-if", + "core-foundation 0.9.4", + "jni", + "robius-android-env", + "system-configuration 0.7.0", + "system-configuration-sys", + "windows 0.56.0", +] + [[package]] name = "robius-use-makepad" version = "0.1.1" @@ -5773,6 +5787,7 @@ dependencies = [ "robius-directories", "robius-location", "robius-open", + "robius-proxy", "robius-use-makepad", "ruma", "sanitize-filename", @@ -5980,7 +5995,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -6048,7 +6063,7 @@ dependencies = [ "security-framework 3.5.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -6860,6 +6875,17 @@ dependencies = [ "system-configuration-sys", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration-sys" version = "0.6.0" @@ -6891,7 +6917,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -7368,7 +7394,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -7987,7 +8013,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 564970744..a2781bb13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,10 @@ makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = ## Including this crate automatically configures all `robius-*` crates to work with Makepad. robius-use-makepad = "0.1.1" -robius-open = { git = "https://github.com/project-robius/robius" } -robius-directories = { git = "https://github.com/project-robius/robius" } -robius-location = { git = "https://github.com/project-robius/robius" } +robius-open = { git = "https://github.com/Project-Robius-China/robius2.git" } +robius-directories = { git = "https://github.com/Project-Robius-China/robius2.git" } +robius-location = { git = "https://github.com/Project-Robius-China/robius2.git" } +robius-proxy = { git = "https://github.com/Project-Robius-China/robius2.git" } anyhow = "1.0" diff --git a/resources/i18n/en.json b/resources/i18n/en.json index f3da6e37c..2183bb931 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -20,6 +20,27 @@ "settings.preferences.language.title": "Language", "settings.preferences.language.application_label": "Application language", "settings.preferences.language.reload_hint": "The app will reload after selecting another language", + "settings.preferences.proxy.title": "Network proxy settings", + "settings.preferences.proxy.input": "http://127.0.0.1:7890", + "settings.preferences.proxy.hint": "Optional. Example: http://127.0.0.1:7890 or socks5://127.0.0.1:1080", + "settings.preferences.proxy.button.save": "Save Proxy", + "settings.preferences.proxy.button.clear": "Clear Proxy", + "settings.preferences.proxy.use_proxy": "Use proxy", + "settings.preferences.proxy.address": "Address", + "settings.preferences.proxy.port": "Port", + "settings.preferences.proxy.account": "Account", + "settings.preferences.proxy.password": "Password", + "settings.preferences.proxy.input.address": "127.0.0.1", + "settings.preferences.proxy.input.port": "7890", + "settings.preferences.proxy.input.account": "", + "settings.preferences.proxy.input.password": "", + "settings.preferences.proxy.popup.saved": "Saved proxy settings. Re-login to apply to the current Matrix session.", + "settings.preferences.proxy.popup.cleared": "Cleared proxy settings.", + "settings.preferences.proxy.popup.invalid": "Invalid proxy URL.", + "settings.preferences.proxy.popup.clear_failed": "Failed to clear proxy settings.", + "settings.preferences.proxy.error.missing_address": "Please enter the proxy address.", + "settings.preferences.proxy.error.missing_port": "Please enter the proxy port.", + "settings.preferences.proxy.error.invalid_port": "Proxy port must be a number between 1 and 65535.", "language.option.english": "English", "language.option.chinese_simplified": "Simplified Chinese", @@ -29,7 +50,26 @@ "login.input.password": "Password", "login.input.confirm_password": "Confirm password", "login.input.homeserver": "matrix.org", + "login.input.proxy": "http://127.0.0.1:7890", "login.label.homeserver_optional": "Homeserver URL (optional)", + "login.label.proxy_optional": "Proxy URL (optional)", + "login.proxy_settings.entry": "Proxy settings", + "login.proxy_settings.title": "Network proxy settings", + "login.proxy_settings.use_proxy": "Use proxy", + "login.proxy_settings.address": "Address", + "login.proxy_settings.port": "Port", + "login.proxy_settings.account": "Account", + "login.proxy_settings.password": "Password", + "login.proxy_settings.input.address": "127.0.0.1", + "login.proxy_settings.input.port": "7890", + "login.proxy_settings.input.account": "", + "login.proxy_settings.input.password": "", + "login.proxy_settings.save": "Save", + "login.proxy_settings.saved.title": "Proxy Settings Saved", + "login.proxy_settings.saved.body": "Proxy settings were saved successfully.", + "login.proxy_settings.error.missing_address": "Please enter the proxy address.", + "login.proxy_settings.error.missing_port": "Please enter the proxy port.", + "login.proxy_settings.error.invalid_port": "Proxy port must be a number between 1 and 65535.", "login.button.login": "Login", "login.button.create_account": "Create account", "login.sso.prompt": "Or, login with an SSO provider:", @@ -43,6 +83,8 @@ "login.status.missing_password.body": "Please enter a valid password.", "login.status.password_mismatch.title": "Passwords do not match", "login.status.password_mismatch.body": "Please enter the same password in both password fields.", + "login.status.invalid_proxy.title": "Invalid Proxy URL", + "login.status.invalid_proxy.body": "Please enter a valid proxy URL.\n\nDetails: {error}", "login.status.creating_account.title": "Creating account...", "login.status.creating_account.body": "Waiting for the homeserver to create your account...", "login.status.logging_in.title": "Logging in...", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index e4cd0495c..9cb8d9f93 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -20,6 +20,27 @@ "settings.preferences.language.title": "语言", "settings.preferences.language.application_label": "应用语言", "settings.preferences.language.reload_hint": "选择其他语言后,应用将重新加载", + "settings.preferences.proxy.title": "网络代理设置", + "settings.preferences.proxy.input": "http://127.0.0.1:7890", + "settings.preferences.proxy.hint": "可选。例如:http://127.0.0.1:7890 或 socks5://127.0.0.1:1080", + "settings.preferences.proxy.button.save": "保存代理", + "settings.preferences.proxy.button.clear": "清除代理", + "settings.preferences.proxy.use_proxy": "使用代理", + "settings.preferences.proxy.address": "地址", + "settings.preferences.proxy.port": "端口", + "settings.preferences.proxy.account": "账号", + "settings.preferences.proxy.password": "密码", + "settings.preferences.proxy.input.address": "127.0.0.1", + "settings.preferences.proxy.input.port": "7890", + "settings.preferences.proxy.input.account": "", + "settings.preferences.proxy.input.password": "", + "settings.preferences.proxy.popup.saved": "代理设置已保存。请重新登录以应用到当前 Matrix 会话。", + "settings.preferences.proxy.popup.cleared": "代理设置已清除。", + "settings.preferences.proxy.popup.invalid": "代理 URL 无效。", + "settings.preferences.proxy.popup.clear_failed": "清除代理设置失败。", + "settings.preferences.proxy.error.missing_address": "请输入代理地址。", + "settings.preferences.proxy.error.missing_port": "请输入代理端口。", + "settings.preferences.proxy.error.invalid_port": "代理端口必须是 1 到 65535 之间的数字。", "language.option.english": "English", "language.option.chinese_simplified": "简体中文", @@ -29,7 +50,26 @@ "login.input.password": "密码", "login.input.confirm_password": "确认密码", "login.input.homeserver": "matrix.org", + "login.input.proxy": "http://127.0.0.1:7890", "login.label.homeserver_optional": "Homeserver URL(可选)", + "login.label.proxy_optional": "代理 URL(可选)", + "login.proxy_settings.entry": "代理设置", + "login.proxy_settings.title": "网络代理设置", + "login.proxy_settings.use_proxy": "使用代理", + "login.proxy_settings.address": "地址", + "login.proxy_settings.port": "端口", + "login.proxy_settings.account": "账号", + "login.proxy_settings.password": "密码", + "login.proxy_settings.input.address": "127.0.0.1", + "login.proxy_settings.input.port": "7890", + "login.proxy_settings.input.account": "", + "login.proxy_settings.input.password": "", + "login.proxy_settings.save": "保存", + "login.proxy_settings.saved.title": "代理设置已保存", + "login.proxy_settings.saved.body": "代理设置保存成功。", + "login.proxy_settings.error.missing_address": "请输入代理地址。", + "login.proxy_settings.error.missing_port": "请输入代理端口。", + "login.proxy_settings.error.invalid_port": "代理端口必须是 1 到 65535 之间的数字。", "login.button.login": "登录", "login.button.create_account": "创建账号", "login.sso.prompt": "或者,使用 SSO 提供商登录:", @@ -43,6 +83,8 @@ "login.status.missing_password.body": "请输入有效密码。", "login.status.password_mismatch.title": "两次密码不一致", "login.status.password_mismatch.body": "请在两个密码输入框中输入相同的密码。", + "login.status.invalid_proxy.title": "代理 URL 无效", + "login.status.invalid_proxy.body": "请输入有效的代理 URL。\n\n详情:{error}", "login.status.creating_account.title": "正在创建账号...", "login.status.creating_account.body": "正在等待服务器创建你的账号...", "login.status.logging_in.title": "正在登录...", diff --git a/src/lib.rs b/src/lib.rs index 81e858724..bbee1b286 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,7 @@ pub mod utils; /// Multi-account management for supporting multiple Matrix accounts simultaneously. pub mod account_manager; pub mod temp_storage; +pub mod proxy_config; pub mod location; pub mod image_utils; diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 2a7e99221..36f41f766 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -47,6 +47,7 @@ script_mod! { ..mod.widgets.SolidView width: Fill, height: Fill, + flow: Overlay align: Align{x: 0.5, y: 0.5} show_bg: true, draw_bg +: { @@ -211,7 +212,6 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } } - login_button := RobrixIconButton { width: 275, @@ -314,7 +314,7 @@ script_mod! { // Cancel button for add-account mode (hidden by default) cancel_button := RobrixIconButton { - width: Fit, height: Fit + width: Fit, height: Fit, padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{top: 10, bottom: 5} align: Align{x: 0.5, y: 0.5} @@ -331,12 +331,255 @@ script_mod! { login_status_modal_inner := mod.widgets.LoginStatusModal {} } } + + proxy_settings_modal := Modal { + can_dismiss: true, + content +: { + proxy_settings_modal_inner := RoundedView { + width: 380, height: Fit, + flow: Down + spacing: 12.0 + padding: Inset{top: 18, left: 16, right: 16, bottom: 16} + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 10.0 + border_size: 1.0 + border_color: #D8D8D8 + } + + proxy_settings_header := View { + width: Fill, height: Fit, + flow: Right, + align: Align{x: 1.0, y: 0.5} + spacing: 8.0 + + proxy_settings_title := Label { + width: Fill, height: Fit + draw_text +: { + color: (COLOR_ACTIVE_PRIMARY) + text_style: TITLE_TEXT {font_size: 14} + } + text: "Network proxy settings" + } + + proxy_settings_close_button := RobrixNeutralIconButton { + width: Fit, height: Fit + padding: Inset{left: 7, right: 4, top: 7, bottom: 7} + text: "" + icon_walk: Walk{width: 14, height: 14, margin: 0} + draw_icon.svg: (ICON_CLOSE) + } + } + + proxy_use_card := RoundedView { + width: Fill, height: Fit, + flow: Right, + align: Align{x: 1.0, y: 0.5} + show_bg: true + draw_bg +: { + color: #F5F5F5 + border_radius: 8.0 + border_size: 1.0 + border_color: #DADADA + } + padding: Inset{top: 12, bottom: 12, left: 12, right: 12} + + proxy_use_label := Label { + width: Fill, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Use proxy" + } + + proxy_use_toggle := Toggle { + width: 52, height: 28 + text: "" + active: false + icon_walk: Walk{width: 0, height: 0, margin: 0} + label_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { + size: 18.0 + color: #E3E7EF + color_hover: #E3E7EF + color_down: #D5DBE6 + color_active: (COLOR_ACTIVE_PRIMARY) + border_radius: 14.0 + border_size: 1.5 + border_color: #7E879A + border_color_hover: #7E879A + border_color_down: #6F788D + border_color_active: (COLOR_ACTIVE_PRIMARY_DARKER) + mark_color: #2D3A57 + mark_color_hover: #2D3A57 + mark_color_down: #2D3A57 + mark_color_active: #FFFFFF + mark_color_active_hover: #FFFFFF + } + } + } + + proxy_fields_section := RoundedView { + visible: false + width: Fill, height: Fit, + flow: Down + spacing: 0 + show_bg: true + draw_bg +: { + color: #F5F5F5 + border_radius: 8.0 + border_size: 1.0 + border_color: #DADADA + } + padding: Inset{top: 4, left: 12, right: 12, bottom: 8} + + proxy_address_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + proxy_address_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Address" + } + + proxy_address_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "127.0.0.1" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + proxy_port_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + proxy_port_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Port" + } + + proxy_port_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "7890" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + proxy_account_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + proxy_account_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Account" + } + + proxy_account_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + proxy_password_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + proxy_password_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Password" + } + + proxy_password_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "" + is_password: true, + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + } + + proxy_settings_save_button := RobrixIconButton { + width: 120, height: 40 + align: Align{x: 0.5, y: 0.5} + text: "Save" + } + } + } + } + } + + } + + proxy_settings_button_anchor := View { + width: Fill, height: Fill + flow: Down + align: Align{x: 0.0, y: 0.0} + + View { + width: Fill, height: Fit + flow: Right + padding: Inset{top: 10, right: 10} + + View { + width: Fill, height: Fit + } + + proxy_settings_button := RobrixNeutralIconButton { + width: Fit, height: Fit + spacing: 0 + padding: 8 + text: "" + label_walk: Walk{width: 0, height: 0, margin: 0} + icon_walk: Walk{width: 14, height: 14, margin: 0} + draw_icon.svg: (ICON_SETTINGS) + } } } } } -#[derive(Script, ScriptHook, Widget)] +#[derive(Script, Widget)] pub struct LoginScreen { #[source] source: ScriptObjectRef, #[deref] view: View, @@ -353,9 +596,20 @@ pub struct LoginScreen { #[rust] app_language: AppLanguage, /// Boolean to indicate if we're in "add account" mode (adding another Matrix account). #[rust] adding_account: bool, + #[rust] use_proxy_enabled: bool, } impl LoginScreen { + fn sync_proxy_settings_modal_layout(&mut self, cx: &mut Cx) { + let rect = self.view.area().rect(cx); + let available_width = (rect.size.x - 24.0).max(260.0); + let modal_width = available_width.min(380.0); + let mut proxy_settings_modal_inner = self.view.view(cx, ids!(proxy_settings_modal_inner)); + script_apply_eval!(cx, proxy_settings_modal_inner, { + width: #(modal_width) + }); + } + fn set_sso_pending_state(&mut self, cx: &mut Cx, pending: bool) { let mask = if pending { 1.0 } else { 0.0 }; let cursor = if pending { MouseCursor::NotAllowed } else { MouseCursor::Hand }; @@ -424,8 +678,30 @@ impl LoginScreen { .set_empty_text(cx, tr_key(self.app_language, "login.input.confirm_password").to_string()); self.view.text_input(cx, ids!(homeserver_input)) .set_empty_text(cx, tr_key(self.app_language, "login.input.homeserver").to_string()); + self.view.text_input(cx, ids!(proxy_address_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.proxy_settings.input.address").to_string()); + self.view.text_input(cx, ids!(proxy_port_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.proxy_settings.input.port").to_string()); + self.view.text_input(cx, ids!(proxy_account_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.proxy_settings.input.account").to_string()); + self.view.text_input(cx, ids!(proxy_password_input)) + .set_empty_text(cx, tr_key(self.app_language, "login.proxy_settings.input.password").to_string()); self.view.label(cx, ids!(homeserver_hint_label)) .set_text(cx, tr_key(self.app_language, "login.label.homeserver_optional")); + self.view.label(cx, ids!(proxy_settings_title)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.title")); + self.view.label(cx, ids!(proxy_use_label)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.use_proxy")); + self.view.label(cx, ids!(proxy_address_label)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.address")); + self.view.label(cx, ids!(proxy_port_label)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.port")); + self.view.label(cx, ids!(proxy_account_label)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.account")); + self.view.label(cx, ids!(proxy_password_label)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.password")); + self.view.button(cx, ids!(proxy_settings_save_button)) + .set_text(cx, tr_key(self.app_language, "login.proxy_settings.save")); self.view.label(cx, ids!(sso_prompt_label)) .set_text(cx, tr_key(self.app_language, "login.sso.prompt")); let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); @@ -434,6 +710,121 @@ impl LoginScreen { self.sync_mode_texts(cx); } + fn set_use_proxy_enabled(&mut self, cx: &mut Cx, enabled: bool) { + self.use_proxy_enabled = enabled; + self.view + .check_box(cx, ids!(proxy_use_toggle)) + .set_active(cx, enabled); + self.view + .view(cx, ids!(proxy_fields_section)) + .set_visible(cx, enabled); + self.redraw(cx); + } + + fn load_saved_proxy_to_form(&mut self, cx: &mut Cx) { + let saved_proxy = crate::proxy_config::load_saved_proxy_url(); + let Some(saved_proxy) = saved_proxy else { + self.set_use_proxy_enabled(cx, false); + self.view.text_input(cx, ids!(proxy_address_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(proxy_port_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(proxy_account_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(proxy_password_input)).set_text(cx, ""); + return; + }; + + let Ok(parsed_url) = Url::parse(&saved_proxy) else { + self.set_use_proxy_enabled(cx, true); + self.view.text_input(cx, ids!(proxy_address_input)).set_text(cx, &saved_proxy); + self.view.text_input(cx, ids!(proxy_port_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(proxy_account_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(proxy_password_input)).set_text(cx, ""); + return; + }; + + self.set_use_proxy_enabled(cx, true); + self.view + .text_input(cx, ids!(proxy_address_input)) + .set_text(cx, parsed_url.host_str().unwrap_or_default()); + self.view + .text_input(cx, ids!(proxy_port_input)) + .set_text(cx, &parsed_url.port().map(|p| p.to_string()).unwrap_or_default()); + self.view + .text_input(cx, ids!(proxy_account_input)) + .set_text(cx, parsed_url.username()); + self.view + .text_input(cx, ids!(proxy_password_input)) + .set_text(cx, parsed_url.password().unwrap_or_default()); + } + + fn build_proxy_url_from_form(&mut self, cx: &mut Cx) -> Result, String> { + if !self.use_proxy_enabled { + return Ok(None); + } + + let address = self.view.text_input(cx, ids!(proxy_address_input)).text(); + let port_text = self.view.text_input(cx, ids!(proxy_port_input)).text(); + let account = self.view.text_input(cx, ids!(proxy_account_input)).text(); + let password = self.view.text_input(cx, ids!(proxy_password_input)).text(); + + let address = address.trim().to_owned(); + let port_text = port_text.trim().to_owned(); + let account = account.trim().to_owned(); + let password = password.trim().to_owned(); + + if address.is_empty() { + return Err(tr_key(self.app_language, "login.proxy_settings.error.missing_address").to_string()); + } + + if port_text.is_empty() { + return Err(tr_key(self.app_language, "login.proxy_settings.error.missing_port").to_string()); + } + + let port: u16 = port_text + .parse() + .map_err(|_| tr_key(self.app_language, "login.proxy_settings.error.invalid_port").to_string())?; + + let mut proxy_url = if address.contains("://") { + Url::parse(&address) + .map_err(|e| format!("Invalid proxy URL: {e}"))? + } else { + let mut url = Url::parse("http://127.0.0.1") + .map_err(|e| format!("Failed to initialize proxy URL builder: {e}"))?; + url.set_host(Some(&address)) + .map_err(|e| format!("Invalid proxy address `{address}`: {e}"))?; + url + }; + + proxy_url + .set_port(Some(port)) + .map_err(|()| format!("Invalid proxy port `{port}`"))?; + + if account.is_empty() { + proxy_url + .set_username("") + .map_err(|()| String::from("Invalid proxy account value"))?; + proxy_url + .set_password(None) + .map_err(|()| String::from("Invalid proxy password value"))?; + } else { + proxy_url + .set_username(&account) + .map_err(|()| String::from("Invalid proxy account value"))?; + if password.is_empty() { + proxy_url + .set_password(None) + .map_err(|()| String::from("Invalid proxy password value"))?; + } else { + proxy_url + .set_password(Some(&password)) + .map_err(|()| String::from("Invalid proxy password value"))?; + } + } + + let proxy_url = proxy_url.to_string(); + crate::proxy_config::validate_proxy_url(&proxy_url)?; + Ok(Some(proxy_url)) + } + fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { self.signup_mode = signup_mode; self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); @@ -448,6 +839,16 @@ impl LoginScreen { } } +impl ScriptHook for LoginScreen { + fn on_after_new(&mut self, vm: &mut ScriptVm) { + vm.with_cx_mut(|cx| { + self.load_saved_proxy_to_form(cx); + self.set_app_language(cx, self.app_language); + self.sync_proxy_settings_modal_layout(cx); + }); + } +} + impl Widget for LoginScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { @@ -457,6 +858,9 @@ impl Widget for LoginScreen { if self.app_language != app_language { self.set_app_language(cx, app_language); } + if matches!(event, Event::WindowGeomChange(_)) { + self.sync_proxy_settings_modal_layout(cx); + } self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } @@ -484,6 +888,56 @@ impl WidgetMatchEvent for LoginScreen { let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); + let proxy_settings_modal = self.view.modal(cx, ids!(proxy_settings_modal)); + + if self.view.button(cx, ids!(proxy_settings_button)).clicked(actions) { + self.sync_proxy_settings_modal_layout(cx); + proxy_settings_modal.open(cx); + self.redraw(cx); + } + + if self.view.button(cx, ids!(proxy_settings_close_button)).clicked(actions) { + proxy_settings_modal.close(cx); + self.redraw(cx); + } + + if let Some(enabled) = self.view.check_box(cx, ids!(proxy_use_toggle)).changed(actions) { + self.set_use_proxy_enabled(cx, enabled); + } + + if self.view.button(cx, ids!(proxy_settings_save_button)).clicked(actions) { + match self.build_proxy_url_from_form(cx) { + Ok(proxy_url) => { + if let Err(e) = crate::proxy_config::save_proxy_url(proxy_url.as_deref()) { + warning!("Failed to persist proxy configuration from proxy settings modal: {e}"); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.invalid_proxy.title")); + let error_text = tr_fmt(self.app_language, "login.status.invalid_proxy.body", &[ + ("error", e.as_str()), + ]); + login_status_modal_inner.set_status(cx, &error_text); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); + login_status_modal.open(cx); + } else { + proxy_settings_modal.close(cx); + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.proxy_settings.saved.title")); + login_status_modal_inner.set_status(cx, tr_key(self.app_language, "login.proxy_settings.saved.body")); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); + login_status_modal.open(cx); + } + self.redraw(cx); + } + Err(proxy_validation_error) => { + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.invalid_proxy.title")); + let error_text = tr_fmt(self.app_language, "login.status.invalid_proxy.body", &[ + ("error", proxy_validation_error.as_str()), + ]); + login_status_modal_inner.set_status(cx, &error_text); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); + login_status_modal.open(cx); + self.redraw(cx); + } + } + } // Handle cancel button for add-account mode if cancel_button.clicked(actions) { @@ -536,6 +990,23 @@ impl WidgetMatchEvent for LoginScreen { login_status_modal_inner.set_status(cx, tr_key(self.app_language, "login.status.password_mismatch.body")); login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); } else { + let proxy = match self.build_proxy_url_from_form(cx) { + Ok(proxy) => proxy, + Err(proxy_validation_error) => { + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.invalid_proxy.title")); + let error_text = tr_fmt(self.app_language, "login.status.invalid_proxy.body", &[ + ("error", proxy_validation_error.as_str()), + ]); + login_status_modal_inner.set_status(cx, &error_text); + login_status_modal_inner.button_ref(cx).set_text(cx, tr_key(self.app_language, "login.status.okay")); + login_status_modal.open(cx); + self.redraw(cx); + return; + } + }; + if let Err(e) = crate::proxy_config::save_proxy_url(proxy.as_deref()) { + warning!("Failed to persist proxy configuration from login screen: {e}"); + } self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, if self.signup_mode { tr_key(self.app_language, "login.status.creating_account.title") @@ -556,12 +1027,14 @@ impl WidgetMatchEvent for LoginScreen { user_id, password, homeserver: homeserver.is_empty().not().then_some(homeserver), + proxy: proxy.clone(), }) } else { LoginRequest::LoginByPassword(LoginByPassword { user_id, password, homeserver: homeserver.is_empty().not().then_some(homeserver), + proxy: proxy.clone(), is_add_account: self.adding_account, }) })); @@ -714,10 +1187,30 @@ impl WidgetMatchEvent for LoginScreen { // Handle any of the SSO login buttons being clicked for (view_ref, brand) in self.view_set(cx, button_set).iter().zip(&provider_brands) { if view_ref.finger_up(actions).is_some() && !self.sso_pending { + let proxy = match self.build_proxy_url_from_form(cx) { + Ok(proxy) => proxy, + Err(proxy_validation_error) => { + login_status_modal_inner.set_title(cx, tr_key(self.app_language, "login.status.invalid_proxy.title")); + let error_text = tr_fmt(self.app_language, "login.status.invalid_proxy.body", &[ + ("error", proxy_validation_error.as_str()), + ]); + login_status_modal_inner.set_status(cx, &error_text); + let login_status_modal_button = login_status_modal_inner.button_ref(cx); + login_status_modal_button.set_text(cx, tr_key(self.app_language, "login.status.okay")); + login_status_modal_button.set_enabled(cx, true); + login_status_modal.open(cx); + self.redraw(cx); + continue; + } + }; + if let Err(e) = crate::proxy_config::save_proxy_url(proxy.as_deref()) { + warning!("Failed to persist proxy configuration from SSO login flow: {e}"); + } submit_async_request(MatrixRequest::SpawnSSOServer{ identity_provider_id: format!("oidc-{}",brand), brand: brand.to_string(), - homeserver_url: homeserver_input.text() + homeserver_url: homeserver_input.text(), + proxy, }); } } diff --git a/src/logout/logout_confirm_modal.rs b/src/logout/logout_confirm_modal.rs index 506162acf..957da4775 100644 --- a/src/logout/logout_confirm_modal.rs +++ b/src/logout/logout_confirm_modal.rs @@ -15,7 +15,7 @@ script_mod! { width: Fit, height: Fit, - RoundedView { + modal_card := RoundedView { width: 400, height: Fit, flow: Down, @@ -86,6 +86,7 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct LogoutConfirmModal { #[deref] view: View, + #[rust] modal_width: f64, /// Whether the modal is in a final state, meaning the user can only click "Okay" to close it. /// /// * Set to `Some(true)` after a successful logout Action @@ -168,6 +169,7 @@ pub enum ClearedComponentType { impl Widget for LogoutConfirmModal { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.sync_modal_layout(cx); self.view.handle_event(cx, event, scope); self.widget_match_event(cx, event, scope); } @@ -293,6 +295,23 @@ impl WidgetMatchEvent for LogoutConfirmModal { } impl LogoutConfirmModal { + fn sync_modal_layout(&mut self, cx: &mut Cx) { + let rect = self.view.area().rect(cx); + if rect.size.x <= 1.0 { + return; + } + let available_width = (rect.size.x - 28.0).max(280.0); + let modal_width = available_width.min(400.0); + if (self.modal_width - modal_width).abs() <= 0.5 { + return; + } + self.modal_width = modal_width; + let mut modal_card = self.view.view(cx, ids!(modal_card)); + script_apply_eval!(cx, modal_card, { + width: #(modal_width) + }); + } + /// Sets the message text displayed in the body of the modal. pub fn set_message(&mut self, cx: &mut Cx, message: &str) { self.label(cx, ids!(message)).set_text(cx, message); diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index 8d3e81a51..635562bcb 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -178,15 +178,23 @@ pub async fn restore_session( status: status_str, }); // Build the client with the previous settings from the session. - let client = Client::builder() + let mut client_builder = Client::builder() .homeserver_url(client_session.homeserver.clone()) .sqlite_store(client_session.db_path.clone(), Some(&client_session.passphrase)) .with_threading_support(matrix_sdk::ThreadingSupport::Enabled { with_subscriptions: true, }) - .handle_refresh_tokens() - .build() - .await?; + .handle_refresh_tokens(); + let saved_proxy = crate::proxy_config::load_saved_proxy_url(); + if let Some(proxy) = saved_proxy.as_deref() { + if let Err(e) = crate::proxy_config::apply_proxy_to_process_env(Some(proxy)) { + warning!("Failed to apply proxy env before restoring Matrix session: {e}"); + } + } + if let Some(proxy) = saved_proxy { + client_builder = client_builder.proxy(proxy); + } + let client = client_builder.build().await?; let sliding_sync_version = sliding_sync_version.into(); client.set_sliding_sync_version(sliding_sync_version); let status_str = format!("Authenticating previous login session for {}...", user_session.meta.user_id); diff --git a/src/proxy_config.rs b/src/proxy_config.rs new file mode 100644 index 000000000..264c57c8c --- /dev/null +++ b/src/proxy_config.rs @@ -0,0 +1,138 @@ +use std::path::PathBuf; + +use makepad_widgets::{error, warning}; +use robius_proxy::ProxyConfig; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::app_data_dir; + + +const PROXY_STATE_FILE_NAME: &str = "proxy_state.json"; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(default)] +struct ProxyState { + proxy_url: Option, +} + +fn proxy_state_file_path() -> PathBuf { + app_data_dir().join(PROXY_STATE_FILE_NAME) +} + +pub fn normalize_proxy_url(proxy_url: Option<&str>) -> Option { + proxy_url + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) +} + +pub fn validate_proxy_url(proxy_url: &str) -> Result<(), String> { + let proxy_url = proxy_url.trim(); + if proxy_url.is_empty() { + return Ok(()); + } + + let parsed_url = Url::parse(proxy_url) + .map_err(|e| format!("Invalid proxy URL: {e}"))?; + + match parsed_url.scheme() { + "http" | "https" | "socks5" | "socks5h" => Ok(()), + scheme => Err(format!( + "Unsupported proxy URL scheme `{scheme}`. Use http, https, socks5, or socks5h." + )), + } +} + +pub fn load_saved_proxy_url() -> Option { + let proxy_state_bytes = match std::fs::read(proxy_state_file_path()) { + Ok(bytes) => bytes, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None, + Err(e) => { + warning!("Failed to read proxy state file: {e}"); + return None; + } + }; + + let proxy_state: ProxyState = match serde_json::from_slice(&proxy_state_bytes) { + Ok(state) => state, + Err(e) => { + warning!("Failed to parse proxy state file: {e}"); + return None; + } + }; + + normalize_proxy_url(proxy_state.proxy_url.as_deref()) +} + +pub fn resolve_effective_proxy_url(proxy_override: Option<&str>) -> Option { + normalize_proxy_url(proxy_override) + .or_else(load_saved_proxy_url) +} + +pub fn save_proxy_url(proxy_url: Option<&str>) -> Result, String> { + let normalized_proxy_url = normalize_proxy_url(proxy_url); + if let Some(proxy_url) = normalized_proxy_url.as_ref() { + validate_proxy_url(proxy_url)?; + } + + let state_path = proxy_state_file_path(); + if let Some(parent_dir) = state_path.parent() { + std::fs::create_dir_all(parent_dir) + .map_err(|e| format!("Failed to create proxy state directory: {e}"))?; + } + + let proxy_state = ProxyState { + proxy_url: normalized_proxy_url.clone(), + }; + let serialized_proxy_state = serde_json::to_vec(&proxy_state) + .map_err(|e| format!("Failed to serialize proxy state: {e}"))?; + std::fs::write(&state_path, serialized_proxy_state) + .map_err(|e| format!("Failed to write proxy state file {}: {e}", state_path.display()))?; + + apply_proxy_to_process_env(normalized_proxy_url.as_deref())?; + + Ok(normalized_proxy_url) +} + +fn build_env_proxy_config(proxy_url: &str) -> ProxyConfig { + let mut config = ProxyConfig::new() + .direct(false) + .manual_all(proxy_url) + .manual_http(proxy_url) + .manual_https(proxy_url) + .bypass(["localhost", "127.0.0.1", "::1"]); + + if proxy_url.to_ascii_lowercase().starts_with("socks") { + config = config.manual_socks(proxy_url); + } + + config +} + +pub fn apply_proxy_to_process_env(proxy_url: Option<&str>) -> Result<(), String> { + match normalize_proxy_url(proxy_url) { + Some(proxy_url) => { + validate_proxy_url(&proxy_url)?; + build_env_proxy_config(&proxy_url) + .apply_to_env() + .map_err(|e| format!("Failed to apply proxy to process env: {e:?}"))?; + } + None => { + ProxyConfig::clear_env() + .map_err(|e| format!("Failed to clear proxy env vars: {e:?}"))?; + } + } + + Ok(()) +} + +pub fn load_and_apply_saved_proxy_to_process_env() -> Option { + let saved_proxy = load_saved_proxy_url(); + if let Some(proxy_url) = saved_proxy.as_deref() { + if let Err(e) = apply_proxy_to_process_env(Some(proxy_url)) { + error!("Failed to apply saved proxy to process env: {e}"); + } + } + saved_proxy +} diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index f3d686fba..5b41edda6 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,5 +1,6 @@ use makepad_widgets::*; +use url::Url; use crate::{app::{AppState, BotSettingsState}, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, i18n::{AppLanguage, I18nKey, language_dropdown_labels, tr, tr_fmt, tr_key}, persistence, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt, translation_settings::TranslationSettingsWidgetExt}, shared::{expand_arrow::ExpandArrow, popup_list::{PopupKind, enqueue_popup_notification}, styles::{apply_neutral_button_style, apply_primary_button_style}}, sliding_sync::current_user_id, updater::{UpdateCheckOutcome, check_for_updates}}; @@ -231,7 +232,192 @@ script_mod! { } text: "The app will reload after selecting another language" } - } // end Language card + } + + LineH { width: 400, padding: 10, margin: Inset{top: 12, bottom: 5} } + + preferences_proxy_title := TitleLabel { + text: "Proxy" + draw_text +: { + color: (COLOR_ACTIVE_PRIMARY) + } + } + + preferences_proxy_use_card := RoundedView { + width: Fill, height: Fit, + flow: Right + align: Align{x: 1.0, y: 0.5} + show_bg: true + draw_bg +: { + color: #F5F5F5 + border_radius: 8.0 + border_size: 1.0 + border_color: #DADADA + } + padding: Inset{top: 12, bottom: 12, left: 12, right: 12} + margin: Inset{left: 5, right: 8, top: 2} + + preferences_proxy_use_label := Label { + width: Fill, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Use proxy" + } + + preferences_proxy_use_toggle := Toggle { + width: 52, height: 28 + text: "" + active: false + icon_walk: Walk{width: 0, height: 0, margin: 0} + label_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { + size: 18.0 + color: #E3E7EF + color_hover: #E3E7EF + color_down: #D5DBE6 + color_active: (COLOR_ACTIVE_PRIMARY) + border_radius: 14.0 + border_size: 1.5 + border_color: #7E879A + border_color_hover: #7E879A + border_color_down: #6F788D + border_color_active: (COLOR_ACTIVE_PRIMARY_DARKER) + mark_color: #2D3A57 + mark_color_hover: #2D3A57 + mark_color_down: #2D3A57 + mark_color_active: #FFFFFF + mark_color_active_hover: #FFFFFF + } + } + } + + preferences_proxy_fields_section := RoundedView { + visible: false + width: Fill, height: Fit, + flow: Down + spacing: 0 + show_bg: true + draw_bg +: { + color: #F5F5F5 + border_radius: 8.0 + border_size: 1.0 + border_color: #DADADA + } + padding: Inset{top: 4, left: 12, right: 12, bottom: 8} + margin: Inset{left: 5, right: 8, top: 4} + + preferences_proxy_address_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + preferences_proxy_address_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Address" + } + + preferences_proxy_address_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "127.0.0.1" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + preferences_proxy_port_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + preferences_proxy_port_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Port" + } + + preferences_proxy_port_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "7890" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + preferences_proxy_account_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + preferences_proxy_account_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Account" + } + + preferences_proxy_account_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "" + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + + LineH { draw_bg.color: #DDDDDD } + + preferences_proxy_password_row := View { + width: Fill, height: Fit, + flow: Right + align: Align{y: 0.5} + spacing: 8.0 + padding: Inset{top: 8, bottom: 8} + + preferences_proxy_password_label := Label { + width: 90, height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 12} + } + text: "Password" + } + + preferences_proxy_password_input := RobrixTextInput { + width: Fill, height: Fit, + flow: Right, + empty_text: "" + is_password: true, + padding: Inset{top: 5, bottom: 5, left: 10, right: 10} + } + } + } + + preferences_proxy_save_button := RobrixIconButton { + width: Fit, height: Fit + padding: Inset{left: 15, right: 15, top: 8, bottom: 8} + margin: Inset{left: 5, top: 5, bottom: 4} + text: "Save Proxy" + } } labs_settings_section := View { @@ -393,12 +579,15 @@ pub struct SettingsScreen { #[rust] selected_category: SettingsCategory, #[rust] app_language: AppLanguage, + #[rust] preferences_use_proxy_enabled: bool, + #[rust] preferences_proxy_layout_width: f64, #[rust] language_popup_visible: bool, #[rust] is_update_checking: bool, } impl Widget for SettingsScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.sync_preferences_proxy_layout(cx); let app_language = scope.data.get::() .map(|app_state| app_state.app_language) .unwrap_or_default(); @@ -484,6 +673,48 @@ impl Widget for SettingsScreen { if let Event::Actions(actions) = event { // Handle language selector click — moved to finger_up below + if let Some(enabled) = self.view.check_box(cx, ids!(preferences_proxy_use_toggle)).changed(actions) { + self.set_preferences_use_proxy_enabled(cx, enabled); + } + + if self.view.button(cx, ids!(preferences_proxy_save_button)).clicked(actions) { + match self.build_proxy_url_from_preferences(cx) { + Ok(proxy_url) => { + match crate::proxy_config::save_proxy_url(proxy_url.as_deref()) { + Ok(_) => { + enqueue_popup_notification( + tr_key(self.app_language, "settings.preferences.proxy.popup.saved").to_string(), + PopupKind::Success, + Some(4.0), + ); + } + Err(proxy_error) => { + enqueue_popup_notification( + format!( + "{}\n\n{}", + tr_key(self.app_language, "settings.preferences.proxy.popup.invalid"), + proxy_error + ), + PopupKind::Error, + None, + ); + } + } + } + Err(proxy_error) => { + enqueue_popup_notification( + format!( + "{}\n\n{}", + tr_key(self.app_language, "settings.preferences.proxy.popup.invalid"), + proxy_error + ), + PopupKind::Error, + None, + ); + } + } + } + if self.view.button(cx, ids!(category_account_button)).clicked(actions) { self.set_selected_category(cx, SettingsCategory::Account); } @@ -579,6 +810,27 @@ impl Widget for SettingsScreen { } impl SettingsScreen { + fn sync_preferences_proxy_layout(&mut self, cx: &mut Cx) { + let rect = self.view.area().rect(cx); + if rect.size.x <= 1.0 { + return; + } + let available_width = (rect.size.x - 42.0).max(260.0); + let card_width = available_width.min(360.0); + if (self.preferences_proxy_layout_width - card_width).abs() <= 0.5 { + return; + } + self.preferences_proxy_layout_width = card_width; + let mut proxy_use_card = self.view.view(cx, ids!(preferences_proxy_use_card)); + script_apply_eval!(cx, proxy_use_card, { + width: #(card_width) + }); + let mut proxy_fields_section = self.view.view(cx, ids!(preferences_proxy_fields_section)); + script_apply_eval!(cx, proxy_fields_section, { + width: #(card_width) + }); + } + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { self.app_language = app_language; self.sync_app_language(cx); @@ -609,6 +861,39 @@ impl SettingsScreen { self.view .label(cx, ids!(preferences_language_hint_label)) .set_text(cx, tr(self.app_language, I18nKey::LanguageReloadHint)); + self.view + .label(cx, ids!(preferences_proxy_title)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.title")); + self.view + .label(cx, ids!(preferences_proxy_use_label)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.use_proxy")); + self.view + .label(cx, ids!(preferences_proxy_address_label)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.address")); + self.view + .label(cx, ids!(preferences_proxy_port_label)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.port")); + self.view + .label(cx, ids!(preferences_proxy_account_label)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.account")); + self.view + .label(cx, ids!(preferences_proxy_password_label)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.password")); + self.view + .text_input(cx, ids!(preferences_proxy_address_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.preferences.proxy.input.address").to_string()); + self.view + .text_input(cx, ids!(preferences_proxy_port_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.preferences.proxy.input.port").to_string()); + self.view + .text_input(cx, ids!(preferences_proxy_account_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.preferences.proxy.input.account").to_string()); + self.view + .text_input(cx, ids!(preferences_proxy_password_input)) + .set_empty_text(cx, tr_key(self.app_language, "settings.preferences.proxy.input.password").to_string()); + self.view + .button(cx, ids!(preferences_proxy_save_button)) + .set_text(cx, tr_key(self.app_language, "settings.preferences.proxy.button.save")); self.update_language_button_text(cx); self.view .account_settings(cx, ids!(account_settings)) @@ -640,6 +925,119 @@ impl SettingsScreen { self.view.redraw(cx); } + fn set_preferences_use_proxy_enabled(&mut self, cx: &mut Cx, enabled: bool) { + self.preferences_use_proxy_enabled = enabled; + self.view + .check_box(cx, ids!(preferences_proxy_use_toggle)) + .set_active(cx, enabled); + self.view + .view(cx, ids!(preferences_proxy_fields_section)) + .set_visible(cx, enabled); + self.view.redraw(cx); + } + + fn load_saved_proxy_to_preferences_form(&mut self, cx: &mut Cx) { + let saved_proxy = crate::proxy_config::load_saved_proxy_url(); + let Some(saved_proxy) = saved_proxy else { + self.set_preferences_use_proxy_enabled(cx, false); + self.view.text_input(cx, ids!(preferences_proxy_address_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(preferences_proxy_port_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(preferences_proxy_account_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(preferences_proxy_password_input)).set_text(cx, ""); + return; + }; + + let Ok(parsed_url) = Url::parse(&saved_proxy) else { + self.set_preferences_use_proxy_enabled(cx, true); + self.view.text_input(cx, ids!(preferences_proxy_address_input)).set_text(cx, &saved_proxy); + self.view.text_input(cx, ids!(preferences_proxy_port_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(preferences_proxy_account_input)).set_text(cx, ""); + self.view.text_input(cx, ids!(preferences_proxy_password_input)).set_text(cx, ""); + return; + }; + + self.set_preferences_use_proxy_enabled(cx, true); + self.view + .text_input(cx, ids!(preferences_proxy_address_input)) + .set_text(cx, parsed_url.host_str().unwrap_or_default()); + self.view + .text_input(cx, ids!(preferences_proxy_port_input)) + .set_text(cx, &parsed_url.port().map(|p| p.to_string()).unwrap_or_default()); + self.view + .text_input(cx, ids!(preferences_proxy_account_input)) + .set_text(cx, parsed_url.username()); + self.view + .text_input(cx, ids!(preferences_proxy_password_input)) + .set_text(cx, parsed_url.password().unwrap_or_default()); + } + + fn build_proxy_url_from_preferences(&mut self, cx: &mut Cx) -> Result, String> { + if !self.preferences_use_proxy_enabled { + return Ok(None); + } + + let address = self.view.text_input(cx, ids!(preferences_proxy_address_input)).text(); + let port_text = self.view.text_input(cx, ids!(preferences_proxy_port_input)).text(); + let account = self.view.text_input(cx, ids!(preferences_proxy_account_input)).text(); + let password = self.view.text_input(cx, ids!(preferences_proxy_password_input)).text(); + + let address = address.trim().to_owned(); + let port_text = port_text.trim().to_owned(); + let account = account.trim().to_owned(); + let password = password.trim().to_owned(); + + if address.is_empty() { + return Err(tr_key(self.app_language, "settings.preferences.proxy.error.missing_address").to_string()); + } + if port_text.is_empty() { + return Err(tr_key(self.app_language, "settings.preferences.proxy.error.missing_port").to_string()); + } + let port: u16 = port_text + .parse() + .map_err(|_| tr_key(self.app_language, "settings.preferences.proxy.error.invalid_port").to_string())?; + + let mut proxy_url = if address.contains("://") { + Url::parse(&address) + .map_err(|e| format!("Invalid proxy URL: {e}"))? + } else { + let mut url = Url::parse("http://127.0.0.1") + .map_err(|e| format!("Failed to initialize proxy URL builder: {e}"))?; + url.set_host(Some(&address)) + .map_err(|e| format!("Invalid proxy address `{address}`: {e}"))?; + url + }; + + proxy_url + .set_port(Some(port)) + .map_err(|()| format!("Invalid proxy port `{port}`"))?; + + if account.is_empty() { + proxy_url + .set_username("") + .map_err(|()| String::from("Invalid proxy account value"))?; + proxy_url + .set_password(None) + .map_err(|()| String::from("Invalid proxy password value"))?; + } else { + proxy_url + .set_username(&account) + .map_err(|()| String::from("Invalid proxy account value"))?; + if password.is_empty() { + proxy_url + .set_password(None) + .map_err(|()| String::from("Invalid proxy password value"))?; + } else { + proxy_url + .set_password(Some(&password)) + .map_err(|()| String::from("Invalid proxy password value"))?; + } + } + + let proxy_url = proxy_url.to_string(); + crate::proxy_config::validate_proxy_url(&proxy_url)?; + Ok(Some(proxy_url)) + } + fn update_language_button_text(&mut self, cx: &mut Cx) { let labels = language_dropdown_labels(self.app_language); let selected_idx = self.app_language.dropdown_index(); @@ -779,10 +1177,12 @@ impl SettingsScreen { }; self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); self.view.bot_settings(cx, ids!(bot_settings)).populate(cx, bot_settings); + self.load_saved_proxy_to_preferences_form(cx); self.view.translation_settings(cx, ids!(translation_settings)).populate(cx, translation_config); self.set_app_language(cx, app_language); self.set_update_checking(cx, false); self.set_selected_category(cx, SettingsCategory::Account); + self.sync_preferences_proxy_layout(cx); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index ae0070887..456f5d586 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -101,7 +101,9 @@ impl From for Cli { homeserver: login.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), - proxy: None, + proxy: login.proxy + .map(|proxy| proxy.trim().to_owned()) + .filter(|proxy| !proxy.is_empty()), login_screen: false, verbose: false, } @@ -116,7 +118,9 @@ impl From for Cli { homeserver: registration.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), - proxy: None, + proxy: registration.proxy + .map(|proxy| proxy.trim().to_owned()) + .filter(|proxy| !proxy.is_empty()), login_screen: false, verbose: false, } @@ -349,8 +353,14 @@ async fn build_client( .with_enable_share_history_on_invite(true) .handle_refresh_tokens(); - if let Some(proxy) = cli.proxy.as_ref() { - builder = builder.proxy(proxy.clone()); + let effective_proxy = crate::proxy_config::resolve_effective_proxy_url(cli.proxy.as_deref()); + if let Some(proxy) = effective_proxy.as_deref() { + if let Err(e) = crate::proxy_config::apply_proxy_to_process_env(Some(proxy)) { + warning!("Failed to apply proxy env before building Matrix client: {e}"); + } + } + if let Some(proxy) = effective_proxy { + builder = builder.proxy(proxy); } // Use a 60 second timeout for all requests to the homeserver. @@ -427,6 +437,7 @@ async fn login( user_id: registration.user_id.clone(), password: registration.password.clone(), homeserver: registration.homeserver.clone(), + proxy: registration.proxy.clone(), }); let localpart = registration_localpart(®istration.user_id)?; let (client, client_session) = build_client(&cli, app_data_dir()).await?; @@ -944,6 +955,7 @@ pub enum MatrixRequest { brand: String, homeserver_url: String, identity_provider_id: String, + proxy: Option, }, /// Subscribe to typing notices for the given room. /// @@ -1135,6 +1147,7 @@ pub struct LoginByPassword { pub user_id: String, pub password: String, pub homeserver: Option, + pub proxy: Option, /// Whether this login is for adding another account (multi-account mode). pub is_add_account: bool, } @@ -1145,6 +1158,7 @@ pub struct RegisterAccount { pub user_id: String, pub password: String, pub homeserver: Option, + pub proxy: Option, } @@ -2548,8 +2562,8 @@ async fn matrix_worker_task( 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::SpawnSSOServer { brand, homeserver_url, identity_provider_id, proxy } => { + spawn_sso_server(brand, homeserver_url, identity_provider_id, proxy, login_sender.clone()).await; } MatrixRequest::ResolveRoomAlias(room_alias) => { @@ -3157,6 +3171,8 @@ pub fn block_on_async_with_timeout( /// /// Returns a handle to the Tokio runtime that is used to run async background tasks. pub fn start_matrix_tokio() -> Result { + crate::proxy_config::load_and_apply_saved_proxy_to_process_env(); + // 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") @@ -5556,6 +5572,7 @@ async fn spawn_sso_server( brand: String, homeserver_url: String, identity_provider_id: String, + proxy: Option, login_sender: Sender, ) { Cx::post_action(LoginAction::SsoPending(true)); @@ -5575,6 +5592,13 @@ async fn spawn_sso_server( let client_and_session_opt = DEFAULT_SSO_CLIENT.lock().unwrap().take(); Handle::current().spawn(async move { + let effective_proxy = crate::proxy_config::resolve_effective_proxy_url(proxy.as_deref()); + if let Some(proxy) = effective_proxy.as_deref() { + if let Err(e) = crate::proxy_config::apply_proxy_to_process_env(Some(proxy)) { + warning!("Failed to apply proxy env before SSO login: {e}"); + } + } + // 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; @@ -5583,7 +5607,7 @@ async fn spawn_sso_server( // 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() || ( + if client_and_session.is_none() || effective_proxy.is_some() || ( !homeserver_url.is_empty() && homeserver_url != "matrix.org" && Url::parse(&homeserver_url) != Url::parse("https://matrix-client.matrix.org/") @@ -5592,6 +5616,7 @@ async fn spawn_sso_server( match build_client( &Cli { homeserver: homeserver_url.is_empty().not().then_some(homeserver_url), + proxy: effective_proxy, ..Default::default() }, app_data_dir(), From 87eb8774355d74722b0c86bfdd4d85b42e4478b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:17:18 +0800 Subject: [PATCH 147/283] mark crate dm-unencrypted room (#81) --- src/app.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/app.rs b/src/app.rs index f4123db8e..63671252f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2263,16 +2263,15 @@ impl BotSettingsState { /// Returns `true` if new DM rooms for this target user should be encrypted. /// - /// BotFather DM rooms are created unencrypted so that appservice bots that do - /// not support E2EE can still receive and reply to messages. + /// New DM rooms are always created unencrypted so appservice bots can + /// receive and reply to messages without E2EE support. pub fn should_create_encrypted_dm( &self, target_user_id: &UserId, current_user_id: Option<&UserId>, ) -> bool { - self.resolved_bot_user_id(current_user_id) - .map(|bot_user_id| bot_user_id.as_str() != target_user_id.as_str()) - .unwrap_or(true) + let _ = (target_user_id, current_user_id); + false } } From add0b5874b4a94482596131f18911787168459bc Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 11 Apr 2026 18:40:56 +0800 Subject: [PATCH 148/283] Fix bot badge alignment in room timeline --- src/home/room_screen.rs | 120 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 7 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 7e6ee4f38..5808f2f37 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -67,6 +67,55 @@ const TRANSLATION_LANG_POPUP_SCROLL_HEIGHT: f64 = 288.0; const TRANSLATION_LANG_POPUP_HEIGHT: f64 = TRANSLATION_LANG_POPUP_SCROLL_HEIGHT + 8.0; const TRANSLATION_LANG_POPUP_GAP: f64 = 6.0; const TRANSLATION_LANG_POPUP_MARGIN: f64 = 8.0; +const MESSAGE_PROFILE_TOP_MARGIN: f64 = 4.5; +const MESSAGE_PROFILE_AVATAR_SIZE: f64 = 48.0; +const MESSAGE_USERNAME_ROW_HEIGHT: f64 = 18.0; +const MESSAGE_USERNAME_ROW_BOTTOM_MARGIN: f64 = 9.0; +const MESSAGE_USERNAME_RIGHT_MARGIN: f64 = 4.0; +const BOT_BADGE_HEIGHT: f64 = 16.0; +const BOT_BADGE_HORIZONTAL_PADDING: f64 = 6.0; +const BOT_BADGE_BORDER_RADIUS: f64 = 3.0; +const BOT_BADGE_TEXT_FONT_SIZE: f64 = 8.5; +const BOT_BADGE_TEXT_TOP_DROP: f64 = -0.08; + +const fn centered_top_margin(outer_top_margin: f64, outer_height: f64, inner_height: f64) -> f64 { + outer_top_margin + ((outer_height - inner_height) * 0.5) +} + +#[cfg(test)] +const fn center_y(top_margin: f64, height: f64) -> f64 { + top_margin + (height * 0.5) +} + +const MESSAGE_USERNAME_ROW_TOP_MARGIN: f64 = centered_top_margin( + MESSAGE_PROFILE_TOP_MARGIN, + MESSAGE_PROFILE_AVATAR_SIZE, + MESSAGE_USERNAME_ROW_HEIGHT, +); + +#[cfg(test)] +fn message_profile_avatar_center_y() -> f64 { + center_y(MESSAGE_PROFILE_TOP_MARGIN, MESSAGE_PROFILE_AVATAR_SIZE) +} + +#[cfg(test)] +fn message_username_row_center_y() -> f64 { + center_y(MESSAGE_USERNAME_ROW_TOP_MARGIN, MESSAGE_USERNAME_ROW_HEIGHT) +} + +#[cfg(test)] +fn bot_badge_center_y_within_username_row() -> f64 { + let bot_badge_top_margin = MESSAGE_USERNAME_ROW_TOP_MARGIN + + ((MESSAGE_USERNAME_ROW_HEIGHT - BOT_BADGE_HEIGHT) * 0.5); + center_y(bot_badge_top_margin, BOT_BADGE_HEIGHT) +} + +#[cfg(test)] +fn bot_badge_label_center_y() -> f64 { + let bot_badge_label_top_margin = (BOT_BADGE_HEIGHT - BOT_BADGE_TEXT_FONT_SIZE) * 0.5 + + (BOT_BADGE_TEXT_FONT_SIZE * BOT_BADGE_TEXT_TOP_DROP); + center_y(bot_badge_label_top_margin, BOT_BADGE_TEXT_FONT_SIZE) +} thread_local! { static ROOM_INFO_ACTION_MODAL_OPEN: Cell = const { Cell::new(false) }; @@ -643,11 +692,11 @@ script_mod! { align: Align{x: 0.5, y: 0.0} // centered horizontally, top aligned width: 65.0, height: Fit, - margin: Inset{top: 4.5, right: 10} + margin: Inset{top: #(MESSAGE_PROFILE_TOP_MARGIN), right: 10} flow: Down, avatar := Avatar { - width: 48, - height: 48, + width: #(MESSAGE_PROFILE_AVATAR_SIZE), + height: #(MESSAGE_PROFILE_AVATAR_SIZE), } timestamp := Timestamp { margin: Inset{ top: 5.9 } @@ -664,13 +713,18 @@ script_mod! { username_view := View { flow: Right, - width: Fill, - height: Fit, + align: Align{y: 0.5}, + width: Fit, + height: #(MESSAGE_USERNAME_ROW_HEIGHT), + margin: Inset{ + top: #(MESSAGE_USERNAME_ROW_TOP_MARGIN), + bottom: #(MESSAGE_USERNAME_ROW_BOTTOM_MARGIN), + } username := Label { - width: Fill, + width: Fit, flow: Right, // do not wrap padding: 0, - margin: Inset{bottom: 9.0, top: 20.0, right: 10.0,} + margin: Inset{right: #(MESSAGE_USERNAME_RIGHT_MARGIN)} max_lines: 1 text_overflow: Ellipsis draw_text +: { @@ -679,6 +733,32 @@ script_mod! { } text: "" } + bot_badge := RoundedView { + visible: false + width: Fit + height: #(BOT_BADGE_HEIGHT) + align: Align{x: 0.5, y: 0.5} + new_batch: true + padding: Inset{left: #(BOT_BADGE_HORIZONTAL_PADDING), right: #(BOT_BADGE_HORIZONTAL_PADDING)} + show_bg: true + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + border_radius: #(BOT_BADGE_BORDER_RADIUS) + } + bot_badge_label := Label { + width: Fit + height: Fit + padding: 0 + draw_text +: { + text_style: REGULAR_TEXT { + font_size: #(BOT_BADGE_TEXT_FONT_SIZE) + top_drop: #(BOT_BADGE_TEXT_TOP_DROP) + } + color: #fff + } + text: "bot" + } + } } message := HtmlOrPlaintext { } @@ -7379,6 +7459,10 @@ fn populate_message_view( } username_label.set_text(cx, &username); new_drawn_status.profile_drawn = profile_drawn; + + // Show/hide the bot badge based on sender's user ID + let sender_is_bot = is_likely_bot_user_id(event_tl_item.sender(), None); + item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, sender_is_bot); } else { // Server notices are drawn with a red color avatar background and username. @@ -7390,6 +7474,7 @@ fn populate_message_view( color: (mod.widgets.COLOR_FG_DANGER_RED) } }); + item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, false); new_drawn_status.profile_drawn = true; } } @@ -9104,4 +9189,25 @@ mod tests { container_rect.size.x - TRANSLATION_LANG_POPUP_MARGIN ); } + + #[test] + fn center_username_row_aligns_with_avatar_center() { + assert_eq!( + message_profile_avatar_center_y(), + message_username_row_center_y(), + ); + } + + #[test] + fn center_bot_badge_aligns_with_username_row_center() { + assert_eq!( + message_username_row_center_y(), + bot_badge_center_y_within_username_row(), + ); + } + + #[test] + fn bot_badge_text_is_centered_within_badge() { + assert!(bot_badge_label_center_y() < (BOT_BADGE_HEIGHT * 0.5)); + } } From 2fcede10da1167b40b5048632252ff6052865fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=EF=BC=88=E7=BE=85=E5=81=A5=E5=B3=AF=EF=BC=89?= <150460738+tyreseluo@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:52:16 +0800 Subject: [PATCH 149/283] style(settings): align contribute and update sections with settings UI (#82) - unify contribute category button spacing and border radius with other tabs - refactor contribute/about area into card-based layout matching existing settings pages - update check-for-updates button style and spacing for consistent visual hierarchy --- src/settings/settings_screen.rs | 135 +++++++++++++++++++------------- 1 file changed, 82 insertions(+), 53 deletions(-) diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 5b41edda6..91dd4b3cf 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -89,9 +89,10 @@ script_mod! { category_contribute_button := RobrixNeutralIconButton { width: Fit, height: Fit, - padding: Inset{top: 9, bottom: 9, left: 14, right: 14} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_MD)} spacing: 0, icon_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { border_radius: (RADIUS_MD) } text: "Contribute" } } @@ -471,70 +472,98 @@ script_mod! { visible: false width: Fill, height: Fit flow: Down - spacing: 8 + spacing: (SPACE_SM) - contribute_title := TitleLabel { - text: "Contribute" - } + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_XS)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } - contribute_description := Label { - width: Fill - height: Fit - flow: Flow.Right{wrap: true} - margin: Inset{left: 5, right: 8, top: 1, bottom: 2} - draw_text +: { - color: (MESSAGE_TEXT_COLOR) - text_style: REGULAR_TEXT { font_size: 10.5 } + contribute_title := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Contribute" } - text: "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2" - } - contribute_repo_link := LinkLabel { - width: Fit, height: Fit, - flow: Flow.Right{wrap: true}, - margin: Inset{left: 5, right: 8, top: 0, bottom: 4} - draw_text +: { - text_style: REGULAR_TEXT { font_size: 10.5 } - color: #x0000EE, - color_hover: (COLOR_LINK_HOVER), + contribute_description := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + margin: Inset{left: (SPACE_XS), right: (SPACE_XS), top: 0, bottom: 2} + draw_text +: { + color: (COLOR_DESCRIPTION_TEXT) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Contribute to Robrix on GitHub: https://github.com/Project-Robius-China/robrix2" } - text: "https://github.com/Project-Robius-China/robrix2" - } - about_title := TitleLabel { - text: "About Robrix" + contribute_repo_link := LinkLabel { + width: Fit, height: Fit, + flow: Flow.Right{wrap: true}, + margin: Inset{left: (SPACE_XS), right: (SPACE_XS), top: 0, bottom: 0} + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #x0000EE, + color_hover: (COLOR_LINK_HOVER), + } + text: "https://github.com/Project-Robius-China/robrix2" + } } - about_description := Label { - width: Fill - height: Fit - flow: Flow.Right{wrap: true} - margin: Inset{left: 5, right: 8, top: 1, bottom: 2} - draw_text +: { - color: (MESSAGE_TEXT_COLOR) - text_style: REGULAR_TEXT { font_size: 10.5 } + RoundedView { + width: Fill, height: Fit + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) } - text: "Robrix is a multi-platform Matrix chat client built with Makepad and Robius." - } - contribute_current_version_label := Label { - width: Fill - height: Fit - margin: Inset{left: 5, right: 8, top: 2, bottom: 3} - draw_text +: { - color: (MESSAGE_TEXT_COLOR) - text_style: REGULAR_TEXT { font_size: 10.5 } + about_title := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "About Robrix" } - text: "Current version: 0.0.0" - } - contribute_check_update_button := RobrixNeutralIconButton { - width: Fit, height: Fit, - margin: Inset{left: 5} - padding: Inset{top: 9, bottom: 9, left: 14, right: 14} - spacing: 0, - icon_walk: Walk{width: 0, height: 0, margin: 0} - text: "Check for Updates" + about_description := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + margin: Inset{left: (SPACE_XS), right: (SPACE_XS), top: 0, bottom: 2} + draw_text +: { + color: (COLOR_DESCRIPTION_TEXT) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Robrix is a multi-platform Matrix chat client built with Makepad and Robius." + } + + LineH { margin: Inset{top: (SPACE_SM), bottom: (SPACE_XS)} } + + contribute_current_version_label := Label { + width: Fill + height: Fit + margin: Inset{left: (SPACE_XS), right: (SPACE_XS), top: 0, bottom: 4} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "Current version: 0.0.0" + } + + contribute_check_update_button := RobrixIconButton { + width: Fit, height: Fit, + margin: Inset{left: (SPACE_XS)} + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_MD)} + spacing: 0, + icon_walk: Walk{width: 0, height: 0, margin: 0} + draw_bg +: { border_radius: (RADIUS_MD) } + text: "Check for Updates" + } } } } From 98e38b501169ecbb04af36e9d63dc29081ccb681 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sat, 11 Apr 2026 18:58:22 +0800 Subject: [PATCH 150/283] WIP: TG bot UI alignment - mentionable text input and i18n updates Co-Authored-By: Claude Opus 4.6 (1M context) --- palpo-and-octos-deploy/compose.yml | 2 +- palpo-and-octos-deploy/config/botfather.json | 6 +- resources/i18n/en.json | 5 + resources/i18n/zh-CN.json | 5 + src/shared/mentionable_text_input.rs | 378 ++++++++++++++++++- 5 files changed, 381 insertions(+), 15 deletions(-) diff --git a/palpo-and-octos-deploy/compose.yml b/palpo-and-octos-deploy/compose.yml index 8dace9827..3d74fbc2f 100644 --- a/palpo-and-octos-deploy/compose.yml +++ b/palpo-and-octos-deploy/compose.yml @@ -66,7 +66,7 @@ services: - "8009:8009" # Appservice listener (receives events from Palpo) - "8010:8080" # Octos dashboard / admin API environment: - DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + MOONSHOT_API_KEY: ${MOONSHOT_API_KEY} RUST_LOG: ${RUST_LOG:-octos=debug,info} volumes: - ./data/octos:/root/.octos diff --git a/palpo-and-octos-deploy/config/botfather.json b/palpo-and-octos-deploy/config/botfather.json index 2120139ea..b96b35d45 100644 --- a/palpo-and-octos-deploy/config/botfather.json +++ b/palpo-and-octos-deploy/config/botfather.json @@ -3,9 +3,9 @@ "name": "BotFather", "enabled": true, "config": { - "provider": "deepseek", - "model": "deepseek-chat", - "api_key_env": "DEEPSEEK_API_KEY", + "provider": "moonshot", + "model": "kimi-k2.5", + "api_key_env": "MOONSHOT_API_KEY", "channels": [ { "type": "matrix", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index f3da6e37c..4ea73bbfa 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -404,6 +404,11 @@ "room_screen.popup.bot.sent_deletebot": "Sent `/deletebot` for {matrix_user_id} to BotFather.", "room_screen.popup.bot.state_unavailable_create_command": "App state is unavailable, so the create-bot command was not sent.", "room_screen.popup.bot.state_unavailable_delete_command": "App state is unavailable, so the delete-bot command was not sent.", + "slash_command.bothelp.description": "Show bot management help", + "slash_command.createbot.description": "Create a new child bot", + "slash_command.deletebot.description": "Delete an existing bot", + "slash_command.header": "Bot Commands", + "slash_command.listbots.description": "List all available bots", "room_screen.fallback.unnamed_room": "Unnamed Room", "room_screen.unsupported.prefix": "[Unsupported]", "room_screen.read_marker.new_messages": "New Messages", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index e4cd0495c..dc4ef6d5a 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -402,6 +402,11 @@ "room_screen.popup.bot.sent_deletebot": "已向 BotFather 发送 `/deletebot`({matrix_user_id})。", "room_screen.popup.bot.state_unavailable_create_command": "应用状态当前不可用,未发送创建机器人命令。", "room_screen.popup.bot.state_unavailable_delete_command": "应用状态当前不可用,未发送删除机器人命令。", + "slash_command.bothelp.description": "显示 Bot 管理帮助", + "slash_command.createbot.description": "创建一个新的子 Bot", + "slash_command.deletebot.description": "删除一个已有的 Bot", + "slash_command.header": "Bot 命令", + "slash_command.listbots.description": "列出所有可用的 Bot", "room_screen.fallback.unnamed_room": "未命名房间", "room_screen.unsupported.prefix": "[不支持]", "room_screen.read_marker.new_messages": "新消息", diff --git a/src/shared/mentionable_text_input.rs b/src/shared/mentionable_text_input.rs index 1a0bd8354..b16fc010b 100644 --- a/src/shared/mentionable_text_input.rs +++ b/src/shared/mentionable_text_input.rs @@ -94,7 +94,9 @@ //! - [`MentionSearchState`]: State machine enum managing search lifecycle //! - [`MentionableTextInputAction`]: Actions for external communication (power levels, member updates) //! +use crate::app::AppState; use crate::avatar_cache::*; +use crate::i18n::{AppLanguage, tr_key}; use crate::shared::avatar::AvatarWidgetRefExt; use crate::shared::bouncing_dots::BouncingDotsWidgetRefExt; use crate::shared::styles::COLOR_UNKNOWN_ROOM_AVATAR; @@ -175,6 +177,75 @@ fn popup_status_item_is_selectable(_kind: PopupStatusItemKind) -> bool { false } +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +enum PopupMode { + #[default] + None, + Mention, + SlashCommand, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct SlashCommand { + command: &'static str, + description_key: &'static str, +} + +const MENTION_POPUP_HEADER_TEXT: &str = "Users in this Room"; +const SLASH_COMMANDS: &[SlashCommand] = &[ + SlashCommand { + command: "/createbot", + description_key: "slash_command.createbot.description", + }, + SlashCommand { + command: "/deletebot", + description_key: "slash_command.deletebot.description", + }, + SlashCommand { + command: "/listbots", + description_key: "slash_command.listbots.description", + }, + SlashCommand { + command: "/bothelp", + description_key: "slash_command.bothelp.description", + }, +]; + +fn bot_command_popup_enabled(app_service_enabled: bool, _app_service_room_bound: bool) -> bool { + app_service_enabled +} + +fn find_slash_command_trigger_position(text: &str, cursor_pos: usize) -> Option { + if cursor_pos == 0 || cursor_pos > text.len() { + return None; + } + + let current_segment = text.get(..cursor_pos)?; + let line_start = current_segment.rfind('\n').map(|idx| idx + 1).unwrap_or(0); + let line = text.get(line_start..cursor_pos)?; + + if !line.starts_with('/') || line[1..].chars().any(char::is_whitespace) { + return None; + } + + Some(line_start) +} + +fn matching_slash_commands(search_text: &str) -> Vec { + let query = search_text.trim().trim_start_matches('/').to_ascii_lowercase(); + SLASH_COMMANDS + .iter() + .copied() + .filter(|command| { + command + .command + .trim_start_matches('/') + .to_ascii_lowercase() + .starts_with(&query) + }) + .collect() +} + fn member_list_ready_for_mentions(member_count: usize, sync_pending: bool) -> bool { member_count > 0 && !sync_pending } @@ -332,6 +403,62 @@ script_mod! { } } + mod.widgets.SlashCommandListItem = View { + width: Fill + height: Fit + margin: Inset{left: 4 right: 4} + padding: Inset{left: 12 right: 12 top: 8 bottom: 8} + cursor: MouseCursor.Hand + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 4.0 + selected: instance(0.0) + + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + sdf.box(0. 0. self.rect_size.x self.rect_size.y self.border_radius) + let highlight = #x1E90FF30 + sdf.fill(Pal.premul(self.color.mix(highlight self.selected))) + return sdf.result + } + } + + animator: Animator { + highlight: { + default: @off + off: AnimatorState { + from: { all: Forward { duration: 0.12 } } + apply: { draw_bg: { selected: 0.0 } } + } + on: AnimatorState { + from: { all: Forward { duration: 0.08 } } + apply: { draw_bg: { selected: 1.0 } } + } + } + } + + flow: Down + spacing: 2.0 + + command_name := Label { + height: Fit + draw_text +: { + color: (COLOR_ACTIVE_PRIMARY) + text_style: BOLD_TEXT {font_size: 11.0} + } + } + + description := Label { + width: Fill + height: Fit + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 10.0} + } + } + } + // Template for loading indicator when members are being fetched mod.widgets.LoadingIndicator = View { width: Fill @@ -494,6 +621,9 @@ pub struct MentionableTextInput { /// Template for the @room mention list item #[live] room_mention_list_item: Option, + /// Template for slash command list items + #[live] + slash_command_list_item: Option, /// Template for loading indicator #[live] loading_indicator: Option, @@ -546,6 +676,9 @@ pub struct MentionableTextInput { /// Whether focus should be restored in the next draw_walk cycle #[rust] pending_draw_focus_restore: bool, + /// Which kind of popup content is currently active. + #[rust] + active_popup_mode: PopupMode, } impl Widget for MentionableTextInput { @@ -571,6 +704,17 @@ impl Widget for MentionableTextInput { } } + if self.is_slash_command_popup_active() { + if let Event::KeyDown(key_event) = event { + if key_event.key_code == KeyCode::Escape { + self.close_mention_popup(cx); + self.pending_draw_focus_restore = true; + self.redraw(cx); + return; + } + } + } + self.cmd_text_input.handle_event(cx, event, scope); // Best practice: Always check Scope first to get current context @@ -633,7 +777,7 @@ impl Widget for MentionableTextInput { // Handle item selection from mention popup if let Some(selected) = self.cmd_text_input.item_selected(actions) { - self.on_user_selected(cx, scope, selected); + self.on_popup_item_selected(cx, scope, selected); } // Handle build items request @@ -796,6 +940,8 @@ impl Widget for MentionableTextInput { self.pending_popup_cleanup = true; // Guarantee cleanup executes even if search completes and stops requesting frames cx.new_next_frame(); + } else if !has_focus && self.is_slash_command_popup_active() { + self.close_mention_popup(cx); } } @@ -830,6 +976,32 @@ impl Widget for MentionableTextInput { } impl MentionableTextInput { + fn current_app_language(scope: &mut Scope) -> AppLanguage { + scope + .data + .get::() + .map(|app_state| app_state.app_language) + .unwrap_or_default() + } + + fn set_popup_header_text(&mut self, cx: &mut Cx, text: &str) { + self.cmd_text_input + .view(cx, ids!(popup.header_view)) + .set_visible(cx, true); + self.cmd_text_input + .label(cx, ids!(popup.header_view.header_label)) + .set_text(cx, text); + } + + fn set_popup_header_for_mentions(&mut self, cx: &mut Cx) { + self.set_popup_header_text(cx, MENTION_POPUP_HEADER_TEXT); + } + + fn set_popup_header_for_slash_commands(&mut self, cx: &mut Cx, scope: &mut Scope) { + let app_language = Self::current_app_language(scope); + self.set_popup_header_text(cx, tr_key(app_language, "slash_command.header")); + } + fn active_search_text(&self) -> Option { match &self.search_state { MentionSearchState::WaitingForMembers { @@ -901,6 +1073,10 @@ impl MentionableTextInput { ) } + fn is_slash_command_popup_active(&self) -> bool { + self.active_popup_mode == PopupMode::SlashCommand + } + /// Generate the next unique identifier for a background search job. fn allocate_search_id(&mut self) -> u64 { if self.next_search_id == 0 { @@ -1101,6 +1277,86 @@ impl MentionableTextInput { items_added } + fn add_slash_command_items( + &mut self, + cx: &mut Cx, + app_language: AppLanguage, + commands: &[SlashCommand], + ) -> usize { + let Some(item_ptr) = self.slash_command_list_item else { + return 0; + }; + + let mut items_added = 0; + for command in commands { + let item = crate::widget_ref_from_live_ptr(cx, Some(item_ptr)); + item.label(cx, ids!(command_name)) + .set_text(cx, command.command); + item.label(cx, ids!(description)) + .set_text(cx, tr_key(app_language, command.description_key)); + self.cmd_text_input.add_item(cx, item); + items_added += 1; + } + + items_added + } + + fn update_slash_command_list(&mut self, cx: &mut Cx, scope: &mut Scope, search_text: &str) { + let room_props = scope + .props + .get::() + .expect("RoomScreenProps should be available in scope for MentionableTextInput"); + + if !bot_command_popup_enabled( + room_props.app_service_enabled, + room_props.app_service_room_bound, + ) { + if self.is_slash_command_popup_active() { + self.close_mention_popup(cx); + } + return; + } + + self.cancel_active_search(); + self.search_state = MentionSearchState::Idle; + self.last_search_text = None; + self.loading_indicator_ref = None; + self.active_popup_mode = PopupMode::SlashCommand; + + self.cmd_text_input.clear_items(cx); + self.cmd_text_input.reset_list_scroll(cx); + self.set_popup_header_for_slash_commands(cx, scope); + + let commands = matching_slash_commands(search_text); + if commands.is_empty() { + self.close_mention_popup(cx); + return; + } + + let app_language = Self::current_app_language(scope); + let items_added = self.add_slash_command_items(cx, app_language, &commands); + + const SLASH_COMMAND_ITEM_HEIGHT: f64 = 48.0; + const LIST_PADDING: f64 = 4.0; + let max_scroll_height = if cx.display_context.is_desktop() { + DESKTOP_MAX_SCROLL_HEIGHT + } else { + MOBILE_MAX_SCROLL_HEIGHT + }; + let content_height = (items_added as f64 * SLASH_COMMAND_ITEM_HEIGHT) + LIST_PADDING; + self.set_list_scroll_height(cx, content_height.min(max_scroll_height)); + + let popup = self.cmd_text_input.view(cx, ids!(popup)); + popup.set_visible(cx, items_added > 0); + + let text_input_area = self.cmd_text_input.text_input_ref().area(); + if cx.has_key_focus(text_input_area) { + self.cmd_text_input.text_input_ref().set_key_focus(cx); + } + + self.redraw(cx); + } + /// Update popup visibility and layout based on current state fn update_popup_visibility(&mut self, cx: &mut Cx, scope: &mut Scope, has_items: bool) { let popup = self.cmd_text_input.view(cx, ids!(popup)); @@ -1245,6 +1501,49 @@ impl MentionableTextInput { self.pending_draw_focus_restore = true; } + fn on_slash_command_selected(&mut self, cx: &mut Cx, selected: WidgetRef) { + let command = selected.label(cx, ids!(command_name)).text(); + if command.is_empty() { + return; + } + + let text_input_ref = self.cmd_text_input.text_input_ref(); + let current_text = text_input_ref.text(); + let head = text_input_ref.borrow().map_or(0, |p| p.cursor().index); + + if let Some(start_idx) = find_slash_command_trigger_position(¤t_text, head) { + let command_to_insert = format!("{command} "); + let new_text = utils::safe_replace_by_byte_indices( + ¤t_text, + start_idx, + head, + &command_to_insert, + ); + + self.cmd_text_input.set_text(cx, &new_text); + let new_pos = start_idx + command_to_insert.len(); + text_input_ref.set_cursor( + cx, + Cursor { + index: new_pos, + prefer_next_row: false, + }, + false, + ); + } + + self.close_mention_popup(cx); + self.pending_draw_focus_restore = true; + } + + fn on_popup_item_selected(&mut self, cx: &mut Cx, scope: &mut Scope, selected: WidgetRef) { + match self.active_popup_mode { + PopupMode::Mention => self.on_user_selected(cx, scope, selected), + PopupMode::SlashCommand => self.on_slash_command_selected(cx, selected), + PopupMode::None => {} + } + } + /// Core text change handler that manages mention context fn handle_text_change(&mut self, cx: &mut Cx, scope: &mut Scope, text: String) { // If search was just cancelled, clear the flag and don't re-trigger search @@ -1258,7 +1557,7 @@ impl MentionableTextInput { if trimmed_text.is_empty() { self.possible_mentions.clear(); self.possible_room_mention = false; - if self.is_searching() { + if self.is_searching() || self.is_slash_command_popup_active() { self.close_mention_popup(cx); } return; @@ -1271,17 +1570,25 @@ impl MentionableTextInput { .map_or(0, |p| p.cursor().index); // Check if we're currently searching and the @ symbol was deleted - if let Some(start_pos) = self.get_trigger_position() { - // Check if the @ symbol at the start position still exists - if start_pos >= text.len() - || text.get(start_pos..start_pos + 1).is_some_and(|c| c != "@") - { - // The @ symbol was deleted, stop searching - self.close_mention_popup(cx); - return; + if self.active_popup_mode == PopupMode::Mention { + if let Some(start_pos) = self.get_trigger_position() { + // Check if the @ symbol at the start position still exists + if start_pos >= text.len() + || text.get(start_pos..start_pos + 1).is_some_and(|c| c != "@") + { + // The @ symbol was deleted, stop searching + self.close_mention_popup(cx); + return; + } } } + if self.active_popup_mode == PopupMode::SlashCommand + && find_slash_command_trigger_position(&text, cursor_pos).is_none() + { + self.close_mention_popup(cx); + } + // Look for trigger position for @ menu if let Some(trigger_pos) = self.find_mention_trigger_position(&text, cursor_pos) { let search_text = @@ -1318,7 +1625,11 @@ impl MentionableTextInput { // Redraw to ensure UI updates are visible self.redraw(cx); - } else if self.is_searching() { + } else if let Some(trigger_pos) = find_slash_command_trigger_position(&text, cursor_pos) { + let search_text = + utils::safe_substring_by_byte_indices(&text, trigger_pos + 1, cursor_pos); + self.update_slash_command_list(cx, scope, &search_text); + } else if self.is_searching() || self.is_slash_command_popup_active() { self.close_mention_popup(cx); } } @@ -1543,6 +1854,9 @@ impl MentionableTextInput { .get::() .expect("RoomScreenProps should be available in scope for MentionableTextInput"); + self.active_popup_mode = PopupMode::Mention; + self.set_popup_header_for_mentions(cx); + // Get trigger position from current state (if in searching mode) let trigger_pos = match &self.search_state { MentionSearchState::WaitingForMembers { @@ -1891,6 +2205,7 @@ impl MentionableTextInput { self.last_search_text = None; self.search_results_pending = false; self.loading_indicator_ref = None; + self.active_popup_mode = PopupMode::None; // Reset change detection state self.last_member_count = 0; @@ -2139,4 +2454,45 @@ mod tests { }, )); } + + #[test] + fn slash_command_popup_requires_enabled_bot_features_even_when_unbound() { + assert!(bot_command_popup_enabled(true, true)); + assert!(bot_command_popup_enabled(true, false)); + assert!(!bot_command_popup_enabled(false, true)); + } + + #[test] + fn slash_command_trigger_is_found_at_input_start() { + assert_eq!(find_slash_command_trigger_position("/li", "/li".len()), Some(0)); + } + + #[test] + fn slash_command_trigger_is_found_after_newline() { + let text = "hello\n/list"; + assert_eq!( + find_slash_command_trigger_position(text, text.len()), + Some("hello\n".len()) + ); + } + + #[test] + fn slash_command_trigger_is_not_found_mid_line() { + let text = "hello /list"; + assert_eq!(find_slash_command_trigger_position(text, text.len()), None); + } + + #[test] + fn slash_commands_filter_by_prefix_without_leading_slash() { + let commands = matching_slash_commands("li"); + assert_eq!(commands, vec![SlashCommand { + command: "/listbots", + description_key: "slash_command.listbots.description", + }]); + } + + #[test] + fn slash_commands_return_empty_for_unknown_prefix() { + assert!(matching_slash_commands("zzzznotacommand").is_empty()); + } } From a9c4ab3610b1112c485fb3d6f951da4ab3191bc8 Mon Sep 17 00:00:00 2001 From: Alvin <48358093+TigerInYourDream@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:35:35 +0800 Subject: [PATCH 151/283] ui: wrap User ID and Other Actions sections in card layout (#85) Wrap the User ID and Other Actions sections in RoundedView cards with the same #F8F8FA background and RADIUS_LG corners used by the Avatar, Display Name, and Multiple Accounts cards for visual consistency. --- src/settings/account_settings.rs | 116 ++++++++++++++++++------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 9b246ff89..efb026b07 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -182,34 +182,46 @@ script_mod! { } } - user_id_section_label := SubsectionLabel { - margin: Inset{top: (SPACE_MD), bottom: (SPACE_XS)} - text: "Your User ID:" - } - - View { + // --- User ID card --- + RoundedView { width: Fill, height: Fit - flow: Right, - spacing: (SPACE_SM) - - copy_user_id_button := RobrixNeutralIconButton { - enable_long_press: true, - margin: Inset{left: (SPACE_XS)} - padding: (SPACE_MD), - spacing: 0, - draw_icon.svg: (ICON_COPY) - icon_walk: Walk{width: 16, height: 16, margin: Inset{right: -2} } + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_SM)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } + + user_id_section_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Your User ID:" } - user_id := Label { + View { width: Fill, height: Fit - flow: Flow.Right{wrap: true}, - margin: Inset{top: (SPACE_SM)} - draw_text +: { - color: (MESSAGE_TEXT_COLOR), - text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + flow: Right, + align: Align{y: 0.5} + spacing: (SPACE_SM) + + copy_user_id_button := RobrixNeutralIconButton { + enable_long_press: true, + padding: (SPACE_MD), + spacing: 0, + draw_icon.svg: (ICON_COPY) + icon_walk: Walk{width: 16, height: 16, margin: Inset{right: -2} } + } + + user_id := Label { + width: Fill, height: Fit + flow: Flow.Right{wrap: true}, + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "You are not logged in." } - text: "You are not logged in." } } @@ -348,34 +360,44 @@ script_mod! { } } // end Multiple Accounts card - other_actions_section_label := SubsectionLabel { - margin: Inset{top: (SPACE_MD), bottom: (SPACE_XS)} - text: "Other actions:" - } - - View { + // --- Other actions card --- + RoundedView { width: Fill, height: Fit - flow: Flow.Right{wrap: true}, - align: Align{y: 0.5}, - spacing: (SPACE_SM) - margin: Inset{bottom: (SPACE_LG)} + flow: Down + padding: Inset{left: (SPACE_MD), right: (SPACE_MD), top: (SPACE_SM), bottom: (SPACE_MD)} + margin: Inset{top: (SPACE_SM), bottom: (SPACE_LG)} + show_bg: true + draw_bg +: { + color: #F8F8FA + border_radius: (RADIUS_LG) + } - manage_account_button := RobrixIconButton { - padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} - margin: Inset{left: (SPACE_XS)} - draw_bg +: { border_radius: (RADIUS_MD) } - draw_icon.svg: (ICON_EXTERNAL_LINK) - icon_walk: Walk{width: 16, height: 16} - text: "Manage Account" + other_actions_section_label := SubsectionLabel { + margin: Inset{top: 0, bottom: (SPACE_XS)} + text: "Other actions:" } - logout_button := RobrixNegativeIconButton { - padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} - margin: Inset{left: (SPACE_XS)} - draw_bg +: { border_radius: (RADIUS_MD) } - draw_icon.svg: (ICON_LOGOUT) - icon_walk: Walk{ width: 16, height: 16, margin: Inset{right: -2} } - text: "Log out" + View { + width: Fill, height: Fit + flow: Flow.Right{wrap: true}, + align: Align{y: 0.5}, + spacing: (SPACE_SM) + + manage_account_button := RobrixIconButton { + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} + draw_bg +: { border_radius: (RADIUS_MD) } + draw_icon.svg: (ICON_EXTERNAL_LINK) + icon_walk: Walk{width: 16, height: 16} + text: "Manage Account" + } + + logout_button := RobrixNegativeIconButton { + padding: Inset{top: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_MD), right: (SPACE_LG)} + draw_bg +: { border_radius: (RADIUS_MD) } + draw_icon.svg: (ICON_LOGOUT) + icon_walk: Walk{ width: 16, height: 16, margin: Inset{right: -2} } + text: "Log out" + } } } } From 7481a007b5b3229519ffb8e8a2753fe0ebb251fe Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sun, 12 Apr 2026 02:41:46 +0800 Subject: [PATCH 152/283] Implement explicit Matrix bot targeting UX --- .../2026-04-11-tg-bot-architecture-review.md | 199 ++++ ...26-04-11-tg-bot-explicit-targeting-plan.md | 635 +++++++++++ .../plans/2026-04-11-tg-bot-ui-alignment.md | 363 +++++++ .../appservices/octos-registration.yaml | 5 +- palpo-and-octos-deploy/config/botfather.json | 1 + resources/i18n/en.json | 6 + resources/i18n/zh-CN.json | 6 + ...k-tg-bot-explicit-room-no-fallback.spec.md | 103 ++ specs/task-tg-bot-explicit-targeting.spec.md | 207 ++++ specs/task-tg-bot-ui-alignment.spec.md | 114 ++ src/app.rs | 1 + src/home/room_screen.rs | 161 ++- src/room/room_input_bar.rs | 993 +++++++++++++++++- src/shared/html_or_plaintext.rs | 27 +- src/sliding_sync.rs | 152 ++- 15 files changed, 2906 insertions(+), 67 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md create mode 100644 docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md create mode 100644 docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md create mode 100644 specs/task-tg-bot-explicit-room-no-fallback.spec.md create mode 100644 specs/task-tg-bot-explicit-targeting.spec.md create mode 100644 specs/task-tg-bot-ui-alignment.spec.md diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md b/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md new file mode 100644 index 000000000..723b2b857 --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md @@ -0,0 +1,199 @@ +# Codex TG Bot 架构方案审查 + +## Context + +Codex 提出将 Robrix2 的 bot 交互模型从"implicit room-bound bot routing"转向"explicit bot targeting",对标 Telegram。本文档审查该方案的技术可行性、架构合理性,以及 Codex 未覆盖的设计问题。 + +**架构原则:** Telegram-style bot UX on top of Matrix semantics。Robrix2 是 Matrix IM 客户端,不是 Telegram 客户端。底层保留 Matrix 的 room/user/message 模型,只在客户端 UX 层补上 Telegram 风格的显式、低摩擦 bot 交互体验。不应把 Telegram 的协议级 bot 概念(原生命令列表、BotFather、menu button)直接映射为 Robrix 的产品默认。 + +**修正说明:** Bot ID 不一致(`"bot"` vs `"octosbot"`)确实存在。实际部署配置 `palpo-and-octos-deploy/config/botfather.json:16` 用的是 `"octosbot"`,与 `src/app.rs:2069` 的默认值 `"bot"` 不匹配。Codex 在这点上是对的。但 `DEFAULT_BOTFATHER_LOCALPART` 是通用默认值,不应绑定到特定 appservice 实现。 + +--- + +## 一、核心论断审查 + +**Codex 论断:** Robrix2 是"implicit room-bound bot routing",Telegram 是"explicit bot targeting"。要对齐,应把 binding 降为内部实现,把显式 target 升为用户可见模型。 + +**审查结论:论断正确,方向正确。** + +代码证据: +- `resolve_target_user_id()` (`room_input_bar.rs:861-876`) — 三级优先级 explicit > reply > fallback,但两个 send path 都传 `None` 给 explicit,从未使用 +- `active_target_user_id` (`room_input_bar.rs:605`) — 状态字段存在但:(a) 从不被 UI 显示,(b) 发送后不清零,(c) 没有用户主动设置的入口 +- `bound_bot_user_id` 在 `RoomScreenProps` 中作为 fallback 默默路由消息 + +**一句话:路由管道已经预留了显式 target 的参数位,但状态模型、bot 判定、UI 层都还没接入——不止是 UI 接线。** + +--- + +## 二、P0/P1/P2 分层评估 + +### P0:基础修正 — 认可 + +| 项目 | 评估 | 备注 | +|------|------|------| +| bot/octosbot 配置对齐 | **应做但不改全局默认** | `botfather_user_id` 已是用户可配置项(`app.rs:2218`),应通过文档/preset 引导 Palpo+OctOS 用户配置,不应把 `DEFAULT_BOTFATHER_LOCALPART` 硬改为 `"octosbot"` | +| 改善绑定错误提示 | **应做** | 当前 "Bind BotFather..." 文案对用户不友好 | +| 改善自动检测 | **低优先** | `is_likely_bot_user_id()` 已有合理的启发式规则 | + +### P1:核心模型对齐 — 认可,但有设计缺口 + +**Codex 方案:** 输入框加 target chip(`To room` / `To @configured bot`) + +**技术可行性:非常高。** 代码库已有所有基础设施: + +| 已有基础 | 位置 | 复用方式 | +|----------|------|----------| +| 状态字段 `active_target_user_id` | `room_input_bar.rs:605` | 直接用 | +| 三级路由函数 `resolve_target_user_id()` | `room_input_bar.rs:861` | 已支持 explicit target 参数 | +| 上下文指示器 UI 模式 | `reply_preview.rs:77-123` `ReplyingPreview` | 完全照搬:Label + 取消按钮,位于输入框上方 | +| Bot 绑定状态 | `RoomScreenProps.bound_bot_user_id` | 决定默认 target | +| 状态持久化 | `RoomInputBarState` (`room_input_bar.rs:1596`) | 已保存 `active_target_user_id` | + +**Codex 未覆盖的 4 个设计问题:** + +1. **⚠️ Reply 不区分 bot 和人(Matrix 语义风险)** + - 当前 `reply_target_user_id` 直接取被回复消息的 sender(`room_input_bar.rs:1112-1115`),不检查是否为 bot + - 这个值传入 `resolve_target_user_id()` 后作为 `target_user_id`,最终在 `sliding_sync.rs:2726` 触发 `ensure_target_user_joined_room()` + - **问题:** 回复普通用户的消息,也会被当作 bot 定向处理。在 Matrix 语义下,reply 首先是原生回复关系,不应默认变成"把消息定向给被回复的人" + - **方案:** 必须在 resolver 中增加 bot 判定——只有 reply-to-bot 才参与 bot targeting,reply-to-human 只作为普通 Matrix reply + - **⚠️ Bot 判定不能只用 `is_likely_bot_user_id()`。** 该函数只覆盖 localpart 启发式和父 bot 精确匹配(`room_screen.rs:432`),不查 `known_bot_user_ids()`。代码中更完整的 bot 识别逻辑在 `detected_bot_binding_for_members()`(`room_screen.rs:360`),它先查 `resolved_bot_user_id()`,再查 `known_bot_user_ids()`,最后做启发式。但注意:该函数是"房间级绑定检测"(接受 `&[RoomMember]`),不是通用的"单个 user_id 是否是 bot"判定器 + - **正确方案:** 需要新建一个独立的 `is_known_or_likely_bot(user_id: &UserId, bot_settings: &BotSettingsState, current_user_id: Option<&UserId>) -> bool` 函数,合并 `known_bot_user_ids()` 精确查询、`resolved_bot_user_id(current_user_id)` 匹配和 `is_likely_bot_user_id()` 启发式。注意:`resolved_bot_user_id()` 需要 `current_user_id` 参数来将 localpart-only 的配置值解析为完整 MXID(`app.rs:2218`)。这是一个新函数,不是从 `detected_bot_binding_for_members()` 抽取——后者的职责是房间级绑定发现,不应被当作单 user_id 判定的前身 + - **⚠️ 依赖传递问题:** 该函数需要 `BotSettingsState` 和 `current_user_id`,但 `reply_target_user_id` 的决策点在 `room_input_bar` 的发送路径中(`room_input_bar.rs:1112`),而当前 `RoomScreenProps` 只带了 `bound_bot_user_id` 等少量 bot 上下文(`room_screen.rs:6480`)。P1 实现需要二选一: + - **方案 A:** 扩展 `RoomScreenProps`,把 `&BotSettingsState` 引用或 resolved parent bot user_id 传入 `room_input_bar`,让 bot 判定在发送路径中本地完成 + - **方案 B:** 将 reply-target 的 bot 判定上移到持有 `AppState` / `current_user_id` 的 `room_screen` 层,在传入 `room_input_bar` 之前就完成过滤 + - 推荐 **方案 A**(改动面更小,`RoomScreenProps` 已经是 bot 上下文的传递通道) + +2. **"To room" vs "无 target" 语义不同** + - 当前 `active_target_user_id = None` + `bound_bot_user_id = Some` → 消息发给 bot(fallback) + - 用户点击 "To room" 意味着**显式不发给 bot** + - 需要区分 "未设置 target"(用 fallback)和 "显式选择 room"(跳过 fallback) + - **方案:** 使用 `TargetSource` enum(见第 4 点),`ExplicitRoom` 表示用户主动选择发给房间,resolver 遇到此状态时跳过 fallback + +2. **Target 何时清零?** + - Codex 没说。当前 `active_target_user_id` 发送后不清零(sticky) + - Telegram 官方文档只明确了群里可以通过 reply 或 `/command@OtherBot` 与 bot 通信([Bot Features](https://core.telegram.org/bots/features#bot-to-bot-communication)),未规定"后续 target 是否自动清零" + - **这是产品决策,不是 Telegram parity 事实。** 推荐行为:reply target 发送后清零,显式 bot target 保持 sticky — 但这需要作为明确的设计选择记录,而非伪装成对标 Telegram 的既有行为 + +3. **首次进入 bot room 的默认状态 + target 来源模型** + - 绑定了 bot 的房间,首次进入时 target chip 应该显示什么? + - "To room" 但 fallback 实际发给 bot → **矛盾** + - "To @configured bot" 但用户没有主动选择 → **可能困惑** + - 当前 resolver 只区分"有某个 user_id"或"没有",丢失了 target 来源信息 + - **方案:** 拆分为"持久化的用户意图"和"运行时计算的 resolved target"两层: + + **持久化层(存入 `RoomInputBarState`):** + ``` + ExplicitOverride { + None, // 用户没有主动覆盖,使用房间默认行为 + Bot(bot_user_id), // 用户主动选择的 bot + Room, // 用户主动选择发给房间(跳过默认 bot) + } + ``` + + **运行时计算层(resolve 时根据上下文推导):** + ``` + ResolvedTarget { + NoTarget, // 普通 Matrix 房间,没有任何 bot target + RoomDefault(bot_user_id), // 来自 bound_bot_user_id + ExplicitOverride::None + ExplicitBot(bot_user_id), // 来自 ExplicitOverride::Bot + ExplicitRoom, // 来自 ExplicitOverride::Room + ReplyBot(bot_user_id), // 来自当前 replying_to + bot 判定 + } + ``` + + - **持久化什么:** 只持久化 `ExplicitOverride`(用户的显式意图)。`RoomDefault` 和 `ReplyBot` 是派生状态,在 resolve 时从 `bound_bot_user_id` 和 `replying_to` 实时计算。这避免了房间绑定变化、取消回复时的陈旧值问题 + - **运行时 resolve 逻辑:** + 1. 如果有 `replying_to` 且被回复者是 bot → `ReplyBot(bot_user_id)` + 2. 否则看 `ExplicitOverride`:`Bot(id)` → `ExplicitBot(id)`,`Room` → `ExplicitRoom` + 3. 否则(`ExplicitOverride::None`):如果有 `bound_bot_user_id` → `RoomDefault(bot)`,否则 → `NoTarget` + - **混合场景决策(ExplicitOverride::Bot + reply-to-human):** + - 用户已设置 `ExplicitOverride::Bot(octosbot)`,然后 reply 一个普通人的消息 + - reply-to-human 不触发 `ReplyBot`(步骤 1 的 bot 判定不通过),继续走步骤 2 + - resolve 结果:`ExplicitBot(octosbot)`,同时挂上对普通人消息的 Matrix reply 关系 + - **产品语义:** 消息定向发给 bot,同时在 Matrix 协议层是对那条人类消息的 reply。这是合理的——用户可能想让 bot 看到被引用的上下文 + - **UI 展示:** target chip 显示 "To @bot",reply preview 正常显示被引用的消息,两者独立 + - **UI 展示:** + - `NoTarget`:chip 不显示 + - `RoomDefault`:淡色 "Default: @bot" + - `ExplicitBot`:正常色 "To @bot" + - `ExplicitRoom`:chip 显示 "To room" + - `ReplyBot`:chip 显示 "Reply → @bot"(临时,取消 reply 即消失) + - **chip × 行为:** 清除 `ExplicitOverride` 回到 `None`,resolve 自动回退到 `RoomDefault`(有绑定 bot 时)或 `NoTarget`(无 bot 时) + - **产品决策:** 首次进入任何房间时 `ExplicitOverride` 为 `None`,resolve 根据是否有 `bound_bot_user_id` 决定显示 + +### P2:Telegram 化交互 — 方向对,优先级合理 + +| 项目 | 评估 | +|------|------| +| Menu button 替代 `/bot` | 可做,但 `/bot` 可保留给 power user | +| 命令分类(纯命令 send-on-select / 参数命令 insert) | 方向合理,但当前硬编码命令表(`mentionable_text_input.rs:188-195`)就是设计本身——spec 明确将"动态命令注册"列为 out of scope(`task-tg-bot-ui-alignment.spec.md:48`)。OctOS 的 slash 命令本质上也是"在聊天里输入的文本命令",不是客户端可发现的协议能力。不应在静态 `SlashCommand` 上堆字段固化,但也不应把"动态注册"默认为自然的下一步——那需要一个新的 Matrix-side 元数据/协议设计,属于独立的未来方向 | +| `/command@bot` 显式寻址 | 长期目标,需解析语法 + 多 bot room 支持 | + +--- + +## 三、架构判断 + +**Codex 的核心设计决策是正确的:** + +> "底层继续复用现在的 target_user_id 机制,不推翻现有发送链路" + +这是最合理的路径。`resolve_target_user_id()` 已经设计了三级优先级,需要: +1. 让 UI 能显示当前 resolved target +2. 让用户能主动设置 explicit target(当前两个 send path 都传 `None`) +3. 让用户能清除 target(切回 "To room") + +**⚠️ 不止是 UI 接线。** 当前 `resolve_target_user_id()` 的签名只接受 `Option`,能表达"某个 bot"或"没有显式 bot",但表达不了"显式发给 room、禁止 fallback"这个第三种状态(`room_input_bar.rs:861`)。需要两层模型(见第二节第 4 点): +- **持久化层:** 将 `active_target_user_id: Option` 改为 `ExplicitOverride { None, Bot(UserId), Room }`,只存用户显式意图 +- **运行时层:** `resolve_target()` 从 `ExplicitOverride` + `bound_bot_user_id` + `replying_to` 实时推导 `ResolvedTarget` +- 抽取 `is_known_or_likely_bot(user_id, bot_settings, current_user_id)`(见第二节第 1 点),供 resolve 时判断 reply target 是否为 bot + +--- + +## 四、Gap 总结 + +| Gap | 严重程度 | 需要决策 | +|-----|---------|---------| +| Reply 不区分 bot 和人 | **高** — 回复普通用户也触发 bot targeting | 需要统一 bot 判定函数(`known_bot_user_ids()` + `is_likely_bot_user_id()`),在 resolver 中检查 | +| "To room" vs "未设置 target" 的语义区分 | **高** — 不解决会导致 target chip 说谎 | 需要两层模型:持久化 `ExplicitOverride` + 运行时 `ResolvedTarget`,拆分用户意图和派生状态 | +| Target 清零时机 | **中** — 影响用户心智模型 | 这是产品决策,不是 TG parity 事实 | +| Target 跨导航持久化 | **已定义** | 持久化 `ExplicitOverride`(用户意图)。`ReplyBot` 不独立持久化,但因 `replying_to` 已持久化(`room_input_bar.rs:1595`)而间接恢复 | +| Reply 与 Target 所有权 | **高** — 必须定义 | 取消 reply → 清掉 ReplyBot;清掉 target → 保留 Matrix reply。ReplyBot 从 replying_to + bot 判定实时推导,不独立持久化 | +| 首次进入 bot room 的默认 target | **中** — 影响首次体验 | 建议用 `RoomDefault(bot)` 而非直接显示 "To @bot" | +| 静态命令表的定位 | **低** — spec 已明确 out of scope | 硬编码命令表就是当前设计,不应默认为"过渡层";动态注册需新的 Matrix-side 协议设计,属独立未来方向 | +| 文档中 `@octosbot` 硬编码 | **低** — 通用架构不应绑定特定 appservice | 改为 "configured bot" / "default bot",只在 OctOS 章节举例 | +| 用户如何主动切换 target | **高** — P1 必答题 | 无切换入口则 target chip 只是展示,不是交互模型。至少需定下一种:点 chip 弹出切换菜单 / reply bot 自动切换 / slash qualifier 选 target | +| 多 bot room 场景 | **低** — 当前不是主要场景 | P2 解决 | + +--- + +## 五、推荐的实施路径 + +认可 Codex 的 P0 → P1 → P2 分层,补充设计细节后可以写 spec: + +**P0(先做):** 改善绑定错误文案 + 为 Palpo+OctOS 部署提供 migration/preset 示例值。注意:`DEFAULT_BOTFATHER_LOCALPART` 是通用默认值,`botfather_user_id` 本身已是用户可配置项(`src/app.rs:2218`),UI 文案也定义为通用输入(`resources/i18n/en.json:274`)。不应把全局默认硬改为 `"octosbot"`,而应通过文档/示例引导 Palpo+OctOS 用户配置正确的值 + +**P1(核心):** Target 状态模型 + chip + 切换入口 +- 将 `active_target_user_id: Option` 重构为 `ExplicitOverride` enum(只持久化用户意图),运行时 resolve 为 `ResolvedTarget` +- 抽取 `is_known_or_likely_bot(user_id, bot_settings, current_user_id)` 统一 bot 判定(合并 `known_bot_user_ids()` + `resolved_bot_user_id()` + `is_likely_bot_user_id()`) +- 修复 reply-to-human 误触发 bot targeting(在 resolver 中加 bot 判定) +- 新增 `TargetIndicator` widget(参考 `ReplyingPreview` 的 UI 模式),区分来源显示 +- **⚠️ TargetIndicator 与 ReplyingPreview 的所有权关系:** reply 预览已有独立的显示/取消/恢复状态机(`show_replying_to()` at `room_input_bar.rs:1214`、`clear_replying_to()` at `:1267`、`on_editing_pane_hidden()` at `:1307`)。必须先定义: + - 取消 reply 是否同时清掉 `ReplyBot` target → **应该是**,因为 `ReplyBot` 的真相来源就是 `replying_to` + - 清掉 target chip 是否保留 Matrix reply → **应该是**,reply 是 Matrix 原生关系,target 是 Robrix UX 层 + - `ReplyBot` 不应作为独立状态持久化,而应在 resolve 时从 `replying_to` + bot 判定实时推导(见上方持久化层设计) +- **定义切换入口(P1 必答题):** 必须包含"主动选择 bot"的入口(否则 `ExplicitOverride::Bot` 永远不会被设置)。最小完整集: + - 点 chip 弹出切换菜单 → 可选择 bot 或 room(产生 `ExplicitOverride::Bot` / `ExplicitOverride::Room`) + - reply bot 消息 → 自动 resolve 为 `ReplyBot`(临时,取消 reply 即消失) + - chip 上的 × → 清除 `ExplicitOverride` 回到 `None`(默认行为) +- 在 send path 中接入 explicit target(当前传 `None` 的地方) + +**P2(增量):** 命令分类 + menu button + `/command@bot` + +## 六、验证方式 + +- 运行 `cargo run`,进入有 bot 的房间 +- 确认 target chip 正确显示当前消息目标,且区分来源(默认 bot vs 显式选择) +- 测试切换 target(To room ↔ To configured bot)后发送消息,验证路由正确性 +- 测试 reply-to-bot 时 target 自动切换为 `ReplyBot` +- **测试 reply-to-human 时不触发 bot targeting**(当前是 bug) +- 验证跨导航时 `ExplicitOverride` 被恢复;`ReplyBot` 会随 `replying_to` 一起恢复(`replying_to` 已持久化在 `RoomInputBarState`,`ReplyBot` 从中实时推导——不独立持久化,但因 `replying_to` 恢复而间接恢复) diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md b/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md new file mode 100644 index 000000000..00ea23b3e --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md @@ -0,0 +1,635 @@ +# Telegram Bot Explicit Target Model Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement an explicit bot target model in the room input bar so users can see and control whether a message goes to the room, the bound bot, or a reply-to-bot target, while preserving normal Matrix reply semantics. + +**Architecture:** Keep the existing Matrix send pipeline and `target_user_id` transport field, but replace the sticky `active_target_user_id` model with persisted `ExplicitOverride` plus runtime `ResolvedTarget`. `RoomScreen` precomputes bot-classification context and passes it through `RoomScreenProps`; `RoomInputBar` owns target resolution, chip/menu UI, persistence, and send-path integration. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, `matrix-sdk`/`ruma`, serde-backed UI state, `cargo test`, `cargo build`, `agent-spec`. + +**Repo rules for this plan:** +- Do not run `cargo fmt`. +- Do not commit during implementation until the user has manually tested the feature. +- `agent-spec lifecycle` is a whole-spec gate, not a per-selector tool. Use targeted `cargo test ` commands during each task, then run full lifecycle verification at the end. + +--- + +## File Map + +- `src/home/room_screen.rs` + - Add the new `is_known_or_likely_bot()` helper. + - Expand `RoomScreenProps` with precomputed bot-classification context. + - Resolve `resolved_parent_bot_user_id` from `AppState` when building room props. + - Add unit tests to the existing `#[cfg(test)] mod tests`. + +- `src/room/room_input_bar.rs` + - Replace `active_target_user_id` with persisted `ExplicitOverride`. + - Add `ResolvedTarget` and pure target-resolution helpers. + - Update both send paths to resolve `target_user_id` from `ResolvedTarget`. + - Add `TargetIndicator` DSL, chip/menu interaction, and formatting helpers. + - Keep `replying_to` as the single source of truth for reply state. + - Add unit tests to the existing `#[cfg(test)] mod tests`. + +- `src/room/reply_preview.rs` + - Only adjust spacing/layout if `TargetIndicator` and `ReplyingPreview` do not stack cleanly. + - Do not change reply lifecycle logic here. + +- `resources/i18n/en.json` +- `resources/i18n/zh-CN.json` + - Add target-chip and target-menu strings under the existing `room_input_bar.*` namespace. + +--- + +## Task 1: Bot Classification Context in `RoomScreen` + +**Files:** +- Modify: `src/home/room_screen.rs` (`detected_bot_binding_for_members()` near lines 360-429, `is_likely_bot_user_id()` near lines 432-448, `RoomScreenProps` construction near lines 3522-3575, struct definition near lines 6471-6482, test module near lines 9035+) +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Add failing bot-detection tests in `room_screen.rs`** + +Add these exact spec-bound tests to the existing `#[cfg(test)] mod tests`: + +```rust +#[test] +fn test_bot_detection_configured_parent() { /* ... */ } + +#[test] +fn test_bot_detection_heuristic_fallback() { /* ... */ } + +#[test] +fn test_bot_detection_child_bot() { /* ... */ } + +#[test] +fn test_bot_detection_rejects_normal_user() { /* ... */ } +``` + +Focus them on the pure helper signature from the spec: + +```rust +is_known_or_likely_bot(user_id, resolved_parent_bot_user_id.as_deref(), &known_bot_user_ids) +``` + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_detection_ +``` + +Expected: +- FAIL because `is_known_or_likely_bot()` does not exist yet, or because current logic does not cover all three detection paths. + +- [ ] **Step 3: Implement `is_known_or_likely_bot()` without changing room-binding detection responsibilities** + +Add a new helper adjacent to `is_likely_bot_user_id()`: + +```rust +fn is_known_or_likely_bot( + user_id: &UserId, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> bool { + known_bot_user_ids.iter().any(|known| known.as_str() == user_id.as_str()) + || resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) + || is_likely_bot_user_id(user_id, resolved_parent_bot_user_id) +} +``` + +Keep `detected_bot_binding_for_members()` as the room-level binding detector. Do not collapse it into the new helper. + +- [ ] **Step 4: Extend `RoomScreenProps` with precomputed bot context** + +Add these fields: + +```rust +pub resolved_parent_bot_user_id: Option, +pub known_bot_user_ids: Vec, +``` + +Populate them when constructing `RoomScreenProps` from `AppState`, next to the existing `bound_bot_user_id` logic: + +```rust +let resolved_parent_bot_user_id = app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + .ok(); +let known_bot_user_ids = app_state.bot_settings.known_bot_user_ids(); +``` + +Dummy/fallback props should use `None` / `Vec::new()`. + +- [ ] **Step 5: Re-run the bot-detection tests** + +Run: + +```bash +cargo test test_bot_detection_ +``` + +Expected: +- PASS for all four detection tests. + +- [ ] **Step 6: Checkpoint the diff without committing** + +Run: + +```bash +git diff --stat -- src/home/room_screen.rs +``` + +Expected: +- Only `src/home/room_screen.rs` is touched for this task. + +--- + +## Task 2: Replace Sticky Target State with Explicit Model + +**Files:** +- Modify: `src/room/room_input_bar.rs` (`RoomInputBar` fields near lines 603-605, `resolve_target_user_id()` near lines 861-876, save/restore near lines 1588-1648, `RoomInputBarState` near lines 1770-1779, test module near lines 1797+) +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing pure-resolution tests** + +Add these spec-bound tests to `room_input_bar.rs`: + +```rust +#[test] +fn test_reply_to_human_no_bot_targeting() { /* ... */ } + +#[test] +fn test_reply_bot_overrides_explicit_room() { /* ... */ } + +#[test] +fn test_chip_dismiss_returns_to_room_default() { /* ... */ } + +#[test] +fn test_chip_dismiss_explicit_room_to_room_default() { /* ... */ } + +#[test] +fn test_chip_dismiss_no_bound_bot() { /* ... */ } +``` + +Implement them against pure helpers, not Makepad widget rendering. The goal is to lock the precedence chain before wiring UI. + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_reply_to_human_no_bot_targeting +cargo test test_reply_bot_overrides_explicit_room +cargo test test_chip_dismiss_ +``` + +Expected: +- FAIL because the current state model is still `active_target_user_id: Option`. + +- [ ] **Step 3: Introduce `ExplicitOverride` and `ResolvedTarget`** + +Add pure enums: + +```rust +#[derive(Clone, Debug, Default, PartialEq, Eq)] +enum ExplicitOverride { + #[default] + None, + Bot(OwnedUserId), + Room, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ResolvedTarget { + NoTarget, + RoomDefault(OwnedUserId), + ExplicitBot(OwnedUserId), + ExplicitRoom, + ReplyBot(OwnedUserId), +} +``` + +Also add helper functions: + +```rust +fn resolve_target( + explicit_override: &ExplicitOverride, + replying_to_sender: Option<&UserId>, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> ResolvedTarget + +fn resolved_target_user_id(target: &ResolvedTarget) -> Option + +fn clear_explicit_override_result( + bound_bot_user_id: Option<&UserId>, +) -> ResolvedTarget +``` + +Design rule: +- `ReplyBot` is derived, never persisted. +- `ExplicitRoom` means “skip fallback bot”, not “no explicit state”. + +- [ ] **Step 4: Replace the persisted state slot** + +Rename/replace: + +```rust +#[rust] active_target_user_id: Option +``` + +with: + +```rust +#[rust] explicit_override: ExplicitOverride +``` + +Do the same in `RoomInputBarState` save/restore. Keep `replying_to` exactly as-is; only the target intent slot changes. + +- [ ] **Step 5: Re-run the precedence tests** + +Run: + +```bash +cargo test test_reply_to_human_no_bot_targeting +cargo test test_reply_bot_overrides_explicit_room +cargo test test_chip_dismiss_ +``` + +Expected: +- PASS for the pure precedence/dismiss tests. + +- [ ] **Step 6: Checkpoint the diff without committing** + +Run: + +```bash +git diff --stat -- src/room/room_input_bar.rs +``` + +Expected: +- Only `src/room/room_input_bar.rs` changes in this task. + +--- + +## Task 3: Wire the Send Paths and Restore Semantics + +**Files:** +- Modify: `src/room/room_input_bar.rs` (location send near lines 1037-1083, text send near lines 1095-1159, reply clear near lines 887-894, restore logic near lines 1641-1648) +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing integration-oriented state tests** + +Add these tests: + +```rust +#[test] +fn test_explicit_bot_with_reply_to_human() { /* ... */ } + +#[test] +fn test_cancel_reply_clears_reply_bot() { /* ... */ } + +#[test] +fn test_explicit_override_persists_navigation() { /* ... */ } + +#[test] +fn test_reply_bot_restores_with_replying_to() { /* ... */ } +``` + +Keep them deterministic by testing helper/state transitions directly where possible. Do not wait for full widget-render assertions. + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_explicit_bot_with_reply_to_human +cargo test test_cancel_reply_clears_reply_bot +cargo test test_explicit_override_persists_navigation +cargo test test_reply_bot_restores_with_replying_to +``` + +Expected: +- FAIL because reply send paths still use `replying_to.sender()` directly and persistence still assumes sticky target state. + +- [ ] **Step 3: Add a bot-aware reply-target helper** + +Add a helper that derives reply targeting only when the replied-to sender is actually a bot: + +```rust +fn reply_bot_target_user_id( + &self, + room_screen_props: &RoomScreenProps, +) -> Option { + let reply_sender = self.replying_to + .as_ref() + .map(|(event_tl_item, _)| event_tl_item.sender()); + + match resolve_target( + &self.explicit_override, + reply_sender, + room_screen_props.bound_bot_user_id.as_deref(), + room_screen_props.resolved_parent_bot_user_id.as_deref(), + &room_screen_props.known_bot_user_ids, + ) { + ResolvedTarget::ReplyBot(user_id) => Some(user_id), + _ => None, + } +} +``` + +The pure resolver decides whether reply-to-human falls back to `ExplicitOverride` / `RoomDefault`. + +- [ ] **Step 4: Update both send paths** + +In both the location send path and text send path: +- compute `ResolvedTarget` once +- derive `target_user_id` from `resolved_target_user_id(&resolved_target)` +- keep Matrix `Reply` relation unchanged +- never set `target_user_id` to a human sender merely because the user clicked reply + +The current raw pattern: + +```rust +let reply_target_user_id = self.replying_to.as_ref().map(|(item, _)| item.sender().to_owned()); +``` + +should disappear from both send paths. + +- [ ] **Step 5: Keep reply and explicit target ownership separate** + +On cancel reply: +- `replying_to` becomes `None` +- `ReplyBot` disappears because it is derived +- `ExplicitOverride` remains unchanged + +On restore: +- `ExplicitOverride` comes from `RoomInputBarState` +- `ReplyBot` is re-derived if `replying_to` restores + +- [ ] **Step 6: Re-run the integration tests** + +Run: + +```bash +cargo test test_explicit_bot_with_reply_to_human +cargo test test_cancel_reply_clears_reply_bot +cargo test test_explicit_override_persists_navigation +cargo test test_reply_bot_restores_with_replying_to +``` + +Expected: +- PASS for send-path and restore semantics. + +- [ ] **Step 7: Build after send-path changes** + +Run: + +```bash +cargo build +``` + +Expected: +- PASS + +--- + +## Task 4: Add `TargetIndicator` UI, Menu Interaction, and i18n + +**Files:** +- Modify: `src/room/room_input_bar.rs` (DSL root near lines 175-220 and surrounding widget tree, event handling in `handle_actions()`, helper functions) +- Modify: `src/room/reply_preview.rs` only if vertical spacing becomes cramped +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing presentation tests** + +Add these spec-bound tests: + +```rust +#[test] +fn test_target_chip_room_default() { /* ... */ } + +#[test] +fn test_target_chip_hidden_no_bot() { /* ... */ } + +#[test] +fn test_explicit_bot_via_chip_menu() { /* ... */ } + +#[test] +fn test_explicit_room_via_chip_menu() { /* ... */ } +``` + +Do not make them depend on full Makepad rendering. Add a pure presentation helper so the tests can assert: +- visibility +- label text +- subdued-vs-normal style flag +- whether dismiss is shown + +- [ ] **Step 2: Run the presentation tests and confirm they fail** + +Run: + +```bash +cargo test test_target_chip_ +cargo test test_explicit_bot_via_chip_menu +cargo test test_explicit_room_via_chip_menu +``` + +Expected: +- FAIL because there is no target-chip presentation layer yet. + +- [ ] **Step 3: Add a pure presentation formatter** + +Introduce a helper such as: + +```rust +struct TargetChipPresentation { + visible: bool, + label: String, + subdued: bool, + dismissible: bool, +} + +fn format_target_chip_presentation( + app_language: AppLanguage, + resolved_target: &ResolvedTarget, + bot_display_name: Option<&str>, +) -> TargetChipPresentation +``` + +Formatting rules must match the spec exactly: +- `RoomDefault` → `"Default: {display_name}"` in subdued style +- `ExplicitBot` → `"To {display_name}"` +- `ExplicitRoom` → `"To room"` +- `ReplyBot` → `"Reply → {display_name}"` +- display name falls back to localpart if no room-member display name is available + +- [ ] **Step 4: Add `TargetIndicator` DSL to `RoomInputBar`** + +Insert the new UI above `replying_preview` in the widget tree so the target chip is the top-most context row: + +```rust +target_indicator := View { + visible: false + width: Fill + height: Fit + flow: Down + + target_chip_row := View { + flow: Right + align: Align{y: 0.5} + // chip label + dismiss + menu anchor + } + + target_menu_popup := RoundedView { + visible: false + // room option + bound bot option + } +} +``` + +Reuse the inline popup pattern already used by `emoji_picker_popup` / `translation_lang_wrapper`. Do not introduce a new global popup framework for this task. + +- [ ] **Step 5: Wire chip/menu actions in `handle_actions()`** + +Required interactions: +- clicking the chip toggles the target menu +- selecting the room option sets `ExplicitOverride::Room` +- selecting the bound bot option sets `ExplicitOverride::Bot(bound_bot_user_id.clone())` +- clicking `×` resets `ExplicitOverride::None` +- reply-to-bot does **not** mutate `ExplicitOverride`; it only changes runtime `ResolvedTarget` + +If the room has no bound bot, do not invent a multi-bot menu. Hide the bot option and fall back to the `NoTarget` / `To room` behavior from the resolver. + +- [ ] **Step 6: Add i18n keys** + +Add keys under the existing `room_input_bar.*` namespace, for example: + +```json +"room_input_bar.target.default": "Default: {display_name}", +"room_input_bar.target.to_bot": "To {display_name}", +"room_input_bar.target.to_room": "To room", +"room_input_bar.target.reply_bot": "Reply → {display_name}", +"room_input_bar.target.menu.bound_bot": "{display_name}", +"room_input_bar.target.menu.room": "To room" +``` + +Mirror them in `zh-CN.json`. + +- [ ] **Step 7: Adjust `reply_preview.rs` only if the stacked layout looks wrong** + +Allowed adjustment: +- padding / margin / spacing between the new `TargetIndicator` row and the existing `ReplyingPreview` + +Not allowed: +- changing the `ReplyingPreview` state machine +- moving reply state ownership into `reply_preview.rs` + +- [ ] **Step 8: Re-run the target-chip tests and build** + +Run: + +```bash +cargo test test_target_chip_ +cargo test test_explicit_bot_via_chip_menu +cargo test test_explicit_room_via_chip_menu +cargo build +``` + +Expected: +- PASS + +--- + +## Task 5: Full Verification and Manual Smoke Test + +**Files:** +- Modify: none expected unless verification uncovers defects in the files above +- Verify: `src/home/room_screen.rs`, `src/room/room_input_bar.rs`, `src/room/reply_preview.rs`, `resources/i18n/en.json`, `resources/i18n/zh-CN.json` + +- [ ] **Step 1: Run focused cargo test batches** + +Run: + +```bash +cargo test test_bot_detection_ +cargo test test_target_chip_ +cargo test test_reply_ +cargo test test_explicit_ +cargo test test_chip_dismiss_ +``` + +Expected: +- PASS across all new spec-bound unit tests. + +- [ ] **Step 2: Run a full build** + +Run: + +```bash +cargo build +``` + +Expected: +- PASS + +- [ ] **Step 3: Run full contract verification** + +Run: + +```bash +agent-spec lifecycle specs/task-tg-bot-explicit-targeting.spec.md --code . --format json +``` + +Expected: +- every scenario verdict is `pass` +- no boundary violations +- no spec quality regressions + +- [ ] **Step 4: Manual smoke test in the app** + +Run: + +```bash +cargo run +``` + +Manual checklist: +1. Open a room with a bound bot and confirm the chip shows `Default: `. +2. Click the chip and switch to `To room`; send a message and confirm `target_user_id` is absent. +3. Switch back to the bound bot; send a message and confirm `target_user_id` is the bot. +4. Reply to a human message and confirm the message is a Matrix reply but not targeted to that human. +5. Reply to a bot message and confirm the chip changes to `Reply → `. +6. Cancel the reply and confirm the chip falls back to the underlying explicit/default state. +7. Navigate away and back; confirm `ExplicitOverride` restores and `ReplyBot` only restores if `replying_to` restores. + +- [ ] **Step 5: Stop for user testing** + +Run: + +```bash +git status --short +git diff --stat +``` + +Expected: +- only the planned files changed +- no commit has been created yet + +Do not commit until the user tests the feature and explicitly approves the commit step. + +--- + +## Final Exit Criteria + +Before calling this implementation complete, all of the following must be true: + +- `cargo build` passes +- all spec-bound `cargo test` filters above pass +- `agent-spec lifecycle specs/task-tg-bot-explicit-targeting.spec.md --code . --format json` shows all scenarios as `pass` +- manual smoke test covers target chip visibility, chip menu switching, reply-to-human fallback, reply-to-bot temporary override, and navigation restore +- the worktree is ready for user testing, but **not yet committed** diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md b/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md new file mode 100644 index 000000000..1da2751fd --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md @@ -0,0 +1,363 @@ +# Telegram Bot UI Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add bot identity badges on message timeline and `/` slash command autocomplete menu to align Robrix bot UX with Telegram. + +**Architecture:** Two independent features that share no code: (1) a `bot_badge` Label widget added to the `Message` DSL template, controlled by `set_visible()` in `populate_message_view()`; (2) slash command autocomplete reusing the existing `CommandTextInput` trigger mechanism with a hardcoded command list. Both features are purely UI — no Matrix SDK or backend changes needed. + +**Tech Stack:** Makepad 2.0 `script_mod!` DSL, Rust, existing `CommandTextInput` widget, existing `is_likely_bot_user_id()` detection. + +**Spec:** `specs/task-tg-bot-ui-alignment.spec.md` + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|----------------| +| `src/home/room_screen.rs` | Modify | Add `bot_badge` Label to `Message` DSL template; set visibility in `populate_message_view()` | +| `src/shared/mentionable_text_input.rs` | Modify | Add slash command detection, hardcoded command list, command item template | +| `resources/i18n/en.json` | Modify | Add slash command description strings | +| `resources/i18n/zh-CN.json` | Modify | Add slash command description strings (Chinese) | + +--- + +### Task 1: Add Bot Badge Widget to Message DSL Template + +**Files:** +- Modify: `src/home/room_screen.rs:665-682` (username_view in Message template) + +This task adds a hidden-by-default `bot_badge` Label inside the `username_view` of the `Message` template. The badge is a small rounded label with blue background and white "bot" text. + +- [ ] **Step 1: Add bot_badge to the Message template DSL** + +In `src/home/room_screen.rs`, inside the `username_view` (line ~665), add a `bot_badge` Label after the `username` Label. The badge should be hidden by default (`visible: false`). + +Find the existing `username_view` block: +``` +username_view := View { + flow: Right, + width: Fill, + height: Fit, + username := Label { + width: Fill, + flow: Right, // do not wrap + ... + text: "" + } +} +``` + +Change `username` width from `Fill` to `Fit` (so the badge can sit next to it), and add the badge: +``` +username_view := View { + flow: Right, + width: Fill, + height: Fit, + align: Align{y: 0.5} + username := Label { + width: Fit, + flow: Right, + padding: 0, + margin: Inset{bottom: 9.0, top: 20.0, right: 4.0,} + max_lines: 1 + text_overflow: Ellipsis + draw_text +: { + text_style: USERNAME_TEXT_STYLE {}, + color: (USERNAME_TEXT_COLOR) + } + text: "" + } + bot_badge := RoundedView { + visible: false + width: Fit + height: 16 + margin: Inset{top: 18.0, right: 6.0} + padding: Inset{left: 5, right: 5, top: 1, bottom: 1} + show_bg: true + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + border_radius: 3.0 + } + bot_badge_label := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 8.0} + color: #fff + } + text: "bot" + } + } +} +``` + +- [ ] **Step 2: Build to verify DSL compiles** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 3: Commit** + +```bash +git add src/home/room_screen.rs +git commit -m "ui: add bot_badge widget to Message template (hidden by default)" +``` + +--- + +### Task 2: Show/Hide Bot Badge in populate_message_view() + +**Files:** +- Modify: `src/home/room_screen.rs:7360-7394` (username setting block in populate_message_view) + +This task adds bot detection logic that calls `set_visible()` on the badge after setting the username. + +- [ ] **Step 1: Add bot detection and badge visibility logic** + +In `src/home/room_screen.rs`, after the username is set (line ~7380 `username_label.set_text(cx, &username);`), add bot badge visibility logic. The detection uses the existing `is_likely_bot_user_id()` function (already defined in this file around line 383). + +Find the block: +```rust +username_label.set_text(cx, &username); +new_drawn_status.profile_drawn = profile_drawn; +``` + +Add after it: +```rust +// Show/hide the bot badge based on sender's user ID +let sender_is_bot = is_likely_bot_user_id(event_tl_item.sender()); +item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, sender_is_bot); +``` + +Also in the `else` branch (server notice, line ~7383), ensure the badge is hidden: +```rust +item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, false); +``` + +- [ ] **Step 2: Build to verify** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 3: Manual test** + +Run: `cargo run` +- Open a room with a bot (e.g., `@octosbot:127.0.0.1:8128`) +- Bot messages should show a blue "bot" badge next to the username +- Your own messages should NOT show the badge +- Condensed messages (consecutive from same sender) should NOT show the badge (username_view is hidden in CondensedMessage) + +- [ ] **Step 4: Commit** + +```bash +git add src/home/room_screen.rs +git commit -m "ui: show bot badge on messages from bot users" +``` + +--- + +### Task 3: Add i18n Keys for Slash Commands + +**Files:** +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` + +- [ ] **Step 1: Add English slash command descriptions** + +In `resources/i18n/en.json`, add these keys (in the appropriate alphabetical position): + +```json +"slash_command.createbot.description": "Create a new child bot", +"slash_command.deletebot.description": "Delete an existing bot", +"slash_command.listbots.description": "List all available bots", +"slash_command.bothelp.description": "Show bot management help", +"slash_command.header": "Bot Commands", +``` + +- [ ] **Step 2: Add Chinese slash command descriptions** + +In `resources/i18n/zh-CN.json`, add: + +```json +"slash_command.createbot.description": "创建一个新的子 Bot", +"slash_command.deletebot.description": "删除一个已有的 Bot", +"slash_command.listbots.description": "列出所有可用的 Bot", +"slash_command.bothelp.description": "显示 Bot 管理帮助", +"slash_command.header": "Bot 命令", +``` + +- [ ] **Step 3: Build to verify JSON is valid** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 4: Commit** + +```bash +git add resources/i18n/en.json resources/i18n/zh-CN.json +git commit -m "i18n: add slash command description strings" +``` + +--- + +### Task 4: Add Slash Command Detection to MentionableTextInput + +**Files:** +- Modify: `src/shared/mentionable_text_input.rs` + +The existing `MentionableTextInput` uses `CommandTextInput` with trigger `"@"`. We need to detect when the user types `/` at the **start of the input** (position 0 or after a newline) and show a hardcoded list of bot commands. + +The approach: instead of modifying `CommandTextInput`'s single-trigger mechanism, we handle `/` detection in `MentionableTextInput`'s own `handle_event` / `handle_actions` by checking the text content. When `/` is detected at position 0, we populate the popup with command items instead of user items. + +- [ ] **Step 1: Add slash command data struct and constant list** + +In `src/shared/mentionable_text_input.rs`, add a struct and constant list after the imports: + +```rust +/// A bot slash command entry for the command autocomplete popup. +pub struct SlashCommand { + pub command: &'static str, + pub description_key: &'static str, +} + +/// Hardcoded BotFather slash commands. +const SLASH_COMMANDS: &[SlashCommand] = &[ + SlashCommand { command: "/createbot", description_key: "slash_command.createbot.description" }, + SlashCommand { command: "/deletebot", description_key: "slash_command.deletebot.description" }, + SlashCommand { command: "/listbots", description_key: "slash_command.listbots.description" }, + SlashCommand { command: "/bothelp", description_key: "slash_command.bothelp.description" }, +]; +``` + +- [ ] **Step 2: Add a slash command list item DSL template** + +In the `script_mod!` block, add a template for slash command items (similar to `UserListItem` but simpler — command name + description): + +``` +mod.widgets.SlashCommandListItem = { + ..mod.widgets.View + width: Fill + height: Fit + padding: Inset{left: 12, right: 12, top: 8, bottom: 8} + spacing: 4 + flow: Down + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + } + + command_label := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 11.0} + color: (COLOR_ACTIVE_PRIMARY) + } + } + description_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 9.5} + color: #888 + } + } +} +``` + +Also add a `#[live]` field to `MentionableTextInput` for this template: +```rust +#[live] +slash_command_list_item: Option, +``` + +- [ ] **Step 3: Add slash command state tracking** + +Add fields to `MentionableTextInput`: +```rust +/// Whether slash command popup is currently active (instead of @mention) +#[rust(false)] +slash_command_active: bool, +``` + +- [ ] **Step 4: Implement slash command detection and popup** + +In the `handle_event` or `handle_actions` method of `MentionableTextInput`, add detection logic: + +When the text input content changes: +1. Check if the text starts with `/` +2. If yes and no `@mention` search is active, extract the prefix after `/` +3. Filter `SLASH_COMMANDS` by prefix match +4. Populate the popup list with matching commands +5. Show the popup + +When a command item is selected: +1. Replace the input text with the selected command +2. Close the popup + +When the text no longer starts with `/`: +1. If `slash_command_active`, hide the popup and reset + +This is the most complex step. The implementation should hook into the existing `changed()` action handler where text changes are detected. + +- [ ] **Step 5: Build to verify** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 6: Manual test** + +Run: `cargo run` +- Type `/` at the start of the message input +- A popup should appear with 4 commands: `/createbot`, `/deletebot`, `/listbots`, `/bothelp` +- Type `/list` — popup should filter to show only `/listbots` +- Type `/zzz` — popup should show empty or close +- Select a command — it should be inserted into the input +- Type a normal message (no `/`) — no popup should appear + +- [ ] **Step 7: Commit** + +```bash +git add src/shared/mentionable_text_input.rs +git commit -m "feat: add slash command autocomplete for bot commands" +``` + +--- + +### Task 5: Final Integration Test and Cleanup + +**Files:** +- All modified files + +- [ ] **Step 1: Full build** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 2: End-to-end manual test** + +Run: `cargo run` + +Verify all scenarios from the spec: +1. Bot messages show blue "bot" badge next to username +2. User messages do NOT show bot badge +3. Consecutive bot messages (condensed view) do NOT show badge +4. Typing `/` shows command popup with 4 commands +5. Filtering works (type `/list` to narrow) +6. Selecting a command inserts it +7. Non-matching prefix (`/zzz`) shows empty/closes +8. Normal typing (no `/`) does not trigger popup + +- [ ] **Step 3: Verify against spec** + +Run: `agent-spec parse specs/task-tg-bot-ui-alignment.spec.md` +Review each scenario and confirm it passes manually. + +- [ ] **Step 4: Final commit if any cleanup needed** + +```bash +git add -A +git commit -m "feat: telegram bot UI alignment — bot badge and slash commands" +``` diff --git a/palpo-and-octos-deploy/appservices/octos-registration.yaml b/palpo-and-octos-deploy/appservices/octos-registration.yaml index e99b83843..b30f485b4 100644 --- a/palpo-and-octos-deploy/appservices/octos-registration.yaml +++ b/palpo-and-octos-deploy/appservices/octos-registration.yaml @@ -9,8 +9,9 @@ id: octos-matrix-appservice # URL where Palpo pushes events to Octos. -# Uses Docker service name (not localhost) because both run in the same Docker network. -url: "http://octos:8009" +# When Octos runs on the host and Palpo stays in Docker, use Docker Desktop's +# host bridge so the container can reach the host listener. +url: "http://host.docker.internal:8009" # Tokens for mutual authentication between Palpo and Octos. # These are pre-filled for local development. Replace for production! diff --git a/palpo-and-octos-deploy/config/botfather.json b/palpo-and-octos-deploy/config/botfather.json index b96b35d45..6e5ef9d6b 100644 --- a/palpo-and-octos-deploy/config/botfather.json +++ b/palpo-and-octos-deploy/config/botfather.json @@ -6,6 +6,7 @@ "provider": "moonshot", "model": "kimi-k2.5", "api_key_env": "MOONSHOT_API_KEY", + "admin_mode": true, "channels": [ { "type": "matrix", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index aa81f2c30..2afed9749 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -299,6 +299,12 @@ "room_input_bar.translation.preview.idle": "Start typing to translate...", "room_input_bar.translation.preview.loading": "Translating...", "room_input_bar.translation.preview.error": "Error: {error}", + "room_input_bar.target.default": "Default: {display_name}", + "room_input_bar.target.to_bot": "To {display_name}", + "room_input_bar.target.to_room": "To room", + "room_input_bar.target.reply_bot": "Reply → {display_name}", + "room_input_bar.target.menu.bound_bot": "{display_name}", + "room_input_bar.target.menu.room": "To room", "invite_screen.message.invited_by": "has invited you to join:", "invite_screen.message.invited_generic": "You have been invited to join:", diff --git a/resources/i18n/zh-CN.json b/resources/i18n/zh-CN.json index 8fa8deee1..4b010ef3c 100644 --- a/resources/i18n/zh-CN.json +++ b/resources/i18n/zh-CN.json @@ -299,6 +299,12 @@ "room_input_bar.translation.preview.idle": "开始输入即可翻译...", "room_input_bar.translation.preview.loading": "翻译中...", "room_input_bar.translation.preview.error": "错误:{error}", + "room_input_bar.target.default": "默认:{display_name}", + "room_input_bar.target.to_bot": "发给 {display_name}", + "room_input_bar.target.to_room": "发到房间", + "room_input_bar.target.reply_bot": "回复 → {display_name}", + "room_input_bar.target.menu.bound_bot": "{display_name}", + "room_input_bar.target.menu.room": "发到房间", "invite_screen.message.invited_by": "邀请你加入:", "invite_screen.message.invited_generic": "你被邀请加入:", diff --git a/specs/task-tg-bot-explicit-room-no-fallback.spec.md b/specs/task-tg-bot-explicit-room-no-fallback.spec.md new file mode 100644 index 000000000..392acb221 --- /dev/null +++ b/specs/task-tg-bot-explicit-room-no-fallback.spec.md @@ -0,0 +1,103 @@ +spec: task +name: "Telegram Bot UI Alignment — Phase 2b: ExplicitRoom Suppresses Room Fallback" +inherits: project +tags: [bot, ui, telegram-parity, explicit-room, octos] +depends: [task-tg-bot-explicit-targeting] +estimate: 1d +--- + +## Intent + +修复 `To room` 的误导性行为。当前 Robrix2 在 `ExplicitOverride::Room` 状态下虽然不再附带 `target_user_id`,但 Octos 仍会对单 bot 房间执行 `route_by_room()` fallback,导致普通消息仍被 bot 接收并回复。这与用户对 `To room` 的直觉和 Telegram 风格显式 target 语义不符。 + +本任务要求 `To room` 成为真正的“发送给房间,不走默认 bot fallback”语义,同时保持现有显式 target、`@mention` 和 reply-to-bot 行为不变。 + +## Decisions + +- 新增消息内容标记: `org.octos.explicit_room: true` +- Robrix2 只在 `ExplicitOverride::Room` 解析为发送路径时写入该标记;`RoomDefault`、`ExplicitBot`、`ReplyBot`、无 target 的普通房间消息均不写入 +- `org.octos.explicit_room` 在 Matrix appservice 路径上表示“这条消息明确发给房间”;当消息同时不包含 `org.octos.target_user_id` 且不包含 bot `@mention` 时,Octos 不得把它 fallback 到主 profile,也不得继续生成 `InboundMessage` +- `org.octos.explicit_room` 不禁止显式 `org.octos.target_user_id`,也不禁止 Matrix `@mention` 路由 +- Octos 路由优先级调整为: `(1) explicit target` → `(2) matrix @mention` → `(3) explicit_room guard` → `(4) room fallback` +- Robrix2 的普通发送路径和 reply 发送路径都必须支持 `explicit_room` 标记,避免 “To room” 在 reply 场景下退化 +- `To room` 的 UI 文案不变;修复只改变其后端语义,使之与现有文案一致 + +## Boundaries + +### Allowed Changes +- specs/task-tg-bot-explicit-room-no-fallback.spec.md +- src/room/room_input_bar.rs +- src/sliding_sync.rs +- ../octos/crates/octos-bus/src/matrix_channel.rs + +### Forbidden +- 不要修改 `ExplicitOverride` / `ResolvedTarget` 的现有状态模型 +- 不要修改 target chip 的文案或布局 +- 不要实现多 bot room 切换菜单 +- 不要实现 `/command@bot` 语法 +- 不要改变 `@mention` 的既有路由优先级 +- 不要添加新的 cargo 依赖 + +## Out of Scope + +- 多 bot 房间的显式 bot 选择 UI +- Telegram 风格 menu button +- 动态命令注册 +- 重新设计 Octos 的整体 room routing 模型 + +## Completion Criteria + +Scenario: ExplicitRoom plain message carries explicit_room marker + Test: test_send_message_explicit_room_sets_octos_explicit_room_marker + Given a room whose input bar is in `ExplicitOverride::Room` + When the user sends a plain text message + Then the outgoing message content includes `org.octos.explicit_room = true` + And the outgoing message content does not include `org.octos.target_user_id` + +Scenario: ExplicitRoom reply message carries explicit_room marker + Test: test_send_reply_explicit_room_sets_octos_explicit_room_marker + Given a room whose input bar is in `ExplicitOverride::Room` + And the user is replying to an existing event + When the user sends the reply + Then the outgoing reply content includes `org.octos.explicit_room = true` + And the outgoing reply content does not include `org.octos.target_user_id` + +Scenario: RoomDefault message does not carry explicit_room marker + Test: test_send_message_room_default_does_not_set_octos_explicit_room_marker + Given a bot-bound room with no explicit override + When the user sends a plain text message + Then the outgoing message content does not include `org.octos.explicit_room` + +Scenario: Octos drops unaddressed explicit_room messages instead of dispatching to a bot + Test: test_handle_transaction_explicit_room_skips_room_fallback + Given Octos knows exactly one bot profile for room `"!room:localhost"` + And an incoming `m.room.message` event contains `org.octos.explicit_room = true` + And the event contains no `org.octos.target_user_id` + And the event body contains no bot mention + When Octos handles the transaction + Then Octos does not emit any `InboundMessage` for that event + +Scenario: Octos still routes by mention when explicit_room marker is present + Test: test_handle_transaction_explicit_room_preserves_mention_routing + Given Octos knows exactly one bot profile for room `"!room:localhost"` + And `"@bot_weather:localhost"` maps to profile `"profile-weather"` + And an incoming `m.room.message` event contains `org.octos.explicit_room = true` + And the event body explicitly mentions `"@bot_weather:localhost"` + When Octos handles the transaction + Then inbound message metadata contains `target_profile_id = "profile-weather"` + +Scenario: Octos still routes by explicit target when explicit_room marker is present + Test: test_handle_transaction_explicit_room_preserves_explicit_target_priority + Given `"@bot_weather:localhost"` maps to profile `"profile-weather"` + And an incoming `m.room.message` event contains both `org.octos.explicit_room = true` and `org.octos.target_user_id = "@bot_weather:localhost"` + When Octos handles the transaction + Then inbound message metadata contains `target_profile_id = "profile-weather"` + +Scenario: Invalid explicit_room marker does not suppress room fallback + Test: test_handle_transaction_invalid_explicit_room_marker_ignored + Given Octos knows exactly one bot profile for room `"!room:localhost"` + And an incoming `m.room.message` event contains `org.octos.explicit_room = "true"` as a string value + And the event contains no `org.octos.target_user_id` + And the event body contains no bot mention + When Octos handles the transaction + Then inbound message metadata contains `target_profile_id` diff --git a/specs/task-tg-bot-explicit-targeting.spec.md b/specs/task-tg-bot-explicit-targeting.spec.md new file mode 100644 index 000000000..e5a0e027c --- /dev/null +++ b/specs/task-tg-bot-explicit-targeting.spec.md @@ -0,0 +1,207 @@ +spec: task +name: "Telegram Bot UI Alignment — Phase 2: Explicit Target Model" +inherits: project +tags: [bot, ui, telegram-parity, target-model] +depends: [task-tg-bot-ui-alignment] +estimate: 3d +--- + +## Intent + +将 Robrix2 的 bot 消息路由从"implicit room-bound routing"转向"explicit bot targeting",对标 Telegram 的显式 bot 交互体验。当前用户无法看到消息会发给谁——路由由隐藏的 `target_user_id` fallback 静默决定(`room_input_bar.rs:861`)。本任务引入两层 target 状态模型(持久化用户意图 + 运行时推导),在输入框上方添加 target chip 显示当前消息目标,并修复 reply-to-human 误触发 bot targeting 的 bug。 + +架构原则:Telegram-style bot UX on top of Matrix semantics。底层保留 Matrix 的 room/user/message 模型,只在客户端 UX 层补上显式、低摩擦的 bot 交互。 + +## Decisions + +- 状态模型: 两层分离——持久化层 `ExplicitOverride { None, Bot(OwnedUserId), Room }` 存入 `RoomInputBarState`,运行时层 `ResolvedTarget { NoTarget, RoomDefault(OwnedUserId), ExplicitBot(OwnedUserId), ExplicitRoom, ReplyBot(OwnedUserId) }` 实时推导 +- 持久化策略: 只持久化 `ExplicitOverride`。`RoomDefault` 从 `bound_bot_user_id` 推导,`ReplyBot` 从 `replying_to` + bot 判定推导,均不独立持久化 +- Resolve 优先级: (1) `replying_to` 且被回复者是 bot → `ReplyBot`;(2) `ExplicitOverride::Bot` → `ExplicitBot`,`::Room` → `ExplicitRoom`;(3) `ExplicitOverride::None` + 有 `bound_bot_user_id` → `RoomDefault`;(4) 否则 → `NoTarget` +- Bot 判定: 新建 `is_known_or_likely_bot(user_id: &UserId, resolved_parent_bot_user_id: Option<&UserId>, known_bot_user_ids: &[OwnedUserId]) -> bool`,三条检测路径:(1) `known_bot_user_ids` 精确匹配;(2) `resolved_parent_bot_user_id` 精确匹配;(3) `is_likely_bot_user_id()` 启发式。函数只吃预计算后的最小上下文,不依赖 `BotSettingsState` 或 `current_user_id` +- Target chip UI: 参考 `ReplyingPreview`(`reply_preview.rs:77-123`)的模式,在输入框上方添加 `TargetIndicator` widget +- Chip × 行为: 清除 `ExplicitOverride` 回到 `None`,resolve 自动回退到 `RoomDefault`(有绑定 bot 时)或 `NoTarget`(无 bot 时) +- Reply/Target 所有权: 取消 reply 清掉 `ReplyBot`(真相来源是 `replying_to`);清掉 target chip 保留 Matrix reply(reply 是协议层,target 是 UX 层) +- 混合场景: `ExplicitOverride::Bot` + reply-to-human → resolve 为 `ExplicitBot`,同时保留 Matrix reply 关系,两者独立 +- 切换入口: 点 chip 弹出切换菜单(可选 bot 或 room),reply bot 自动 resolve 为 `ReplyBot`,chip × 清除 override +- Chip 文案规则: chip 中 bot 标识统一使用 display name(如 "BotFather"),不使用 MXID 或 localpart。具体格式:`RoomDefault` → "Default: {display_name}"(subdued style);`ExplicitBot` → "To {display_name}";`ExplicitRoom` → "To room";`ReplyBot` → "Reply → {display_name}"。display name 取自 room member 的 `display_name()`,如果为空则 fallback 到 localpart + +## Boundaries + +### Allowed Changes +- src/room/room_input_bar.rs +- src/home/room_screen.rs +- src/room/reply_preview.rs +- resources/i18n/en.json +- resources/i18n/zh-CN.json + +What to change in each file: room_input_bar.rs gets ExplicitOverride enum, ResolvedTarget enum, refactored resolve_target_user_id(), TargetIndicator widget DSL, save/restore logic. room_screen.rs gets is_known_or_likely_bot() new function, expanded RoomScreenProps with resolved_parent_bot_user_id and known_bot_user_ids fields, bot detection context passing. reply_preview.rs may need layout adjustments for TargetIndicator. en.json and zh-CN.json get target chip display strings. + +### Forbidden +- 不要修改 `detected_bot_binding_for_members()` 的职责边界——它是房间级绑定发现,不是通用 bot 判定 +- 不要修改 `ReplyingPreview` 的现有显示/取消/恢复状态机逻辑(`show_replying_to()`、`clear_replying_to()`、`on_editing_pane_hidden()`) +- 不要修改 `DEFAULT_BOTFATHER_LOCALPART` 的全局默认值 +- 不要添加新的 cargo 依赖 +- 不要实现动态命令注册(独立未来方向,需 Matrix-side 协议设计) +- 不要实现 `/command@bot` 语法解析(P2 范围) +- 不要实现多 bot room 切换菜单(P2 范围,当前只需支持单个绑定 bot) + +## Out of Scope + +- Menu button 替代 `/bot`(P2) +- 命令行为分类(send-on-select vs insert)(P2) +- `/command@bot` 显式寻址语法(P2) +- 多 bot room 场景下的 target 切换(P2) +- 动态命令注册协议设计 + +## Completion Criteria + +Scenario: Target chip shows RoomDefault in bot-bound room + Test: test_target_chip_room_default + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the user has no `ExplicitOverride` set for this room + When the user enters the room + Then the target chip displays "Default: {display_name}" in subdued style where display_name is the bot's room member display name + And the `ResolvedTarget` is `RoomDefault` with the bound bot's user ID + +Scenario: Target chip hidden in normal room + Test: test_target_chip_hidden_no_bot + Given a room with no bound bot + When the user enters the room + Then no target chip is displayed + And the `ResolvedTarget` is `NoTarget` + +Scenario: User switches to ExplicitBot via chip menu + Test: test_explicit_bot_via_chip_menu + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the target chip shows "Default: {display_name}" where display_name is the bot's room member display name + When the user clicks the chip and selects the bound bot from the menu + Then the target chip displays "To {display_name}" in normal style + And the `ExplicitOverride` is `Bot` with the bound bot's user ID + And a message sent from this state has `target_user_id` set to the bot + +Scenario: User switches to ExplicitRoom via chip menu + Test: test_explicit_room_via_chip_menu + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the target chip shows "Default: {display_name}" where display_name is the bot's room member display name + When the user clicks the chip and selects "To room" from the menu + Then the target chip displays "To room" + And the `ExplicitOverride` is `Room` + And a message sent from this state has `target_user_id` set to `None` + +Scenario: Chip dismiss clears ExplicitOverride to RoomDefault + Test: test_chip_dismiss_returns_to_room_default + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the `ExplicitOverride` is `Bot` with the bound bot's user ID + When the user clicks the × button on the target chip + Then the `ExplicitOverride` resets to `None` + And the target chip displays "Default: {display_name}" in subdued style where display_name is the bot's room member display name + +Scenario: Reply-to-bot triggers ReplyBot target + Test: test_reply_to_bot_triggers_reply_bot + Given a room with a message from bot "@octosbot:127.0.0.1:8128" + When the user clicks reply on the bot's message + Then the `ResolvedTarget` is `ReplyBot` with the bot's user ID + And the target chip displays "Reply → {display_name}" + And the reply preview shows the bot's message content + +Scenario: Reply-to-human does NOT trigger bot targeting + Test: test_reply_to_human_no_bot_targeting + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And no `ExplicitOverride` set (defaults to `None`) + And a message from regular user "@alice:127.0.0.1:8128" + When the user clicks reply on alice's message + Then the `ResolvedTarget` is `RoomDefault` with the bound bot's user ID (NOT `ReplyBot`) + And the message is sent with Matrix reply relation to alice's event + And `target_user_id` is set to the bound bot (via RoomDefault fallback) + But `target_user_id` is NOT set to alice's user ID + +Scenario: Cancel reply clears ReplyBot but preserves ExplicitOverride + Test: test_cancel_reply_clears_reply_bot + Given a room where the user has `ExplicitOverride::Bot` set to "@octosbot:127.0.0.1:8128" + And the user is replying to a bot message (ResolvedTarget is ReplyBot) + When the user cancels the reply via the reply preview's cancel button + Then the `ResolvedTarget` changes from `ReplyBot` to `ExplicitBot` + And the target chip changes from "Reply → {display_name}" to "To {display_name}" + And the reply preview is hidden + +Scenario: ExplicitBot persists with reply-to-human + Test: test_explicit_bot_with_reply_to_human + Given a room with `ExplicitOverride::Bot` set to "@octosbot:127.0.0.1:8128" + And the user replies to a message from regular user "@alice:127.0.0.1:8128" + When the user sends the message + Then the message has `target_user_id` set to the bot's user ID + And the message has Matrix reply relation to alice's event + And the target chip continues to show "To {display_name}" + +Scenario: ExplicitOverride persists across room navigation + Test: test_explicit_override_persists_navigation + Given a room with `ExplicitOverride::Room` set + When the user navigates away from the room and returns + Then the `ExplicitOverride` is still `Room` + And the target chip displays "To room" + +Scenario: ReplyBot restores when replying_to restores + Test: test_reply_bot_restores_with_replying_to + Given a room where the user is replying to a bot message + When the user navigates away and returns + Then `replying_to` is restored from `RoomInputBarState` + And the `ResolvedTarget` re-resolves to `ReplyBot` with the bot's user ID + And the target chip displays "Reply → {display_name}" + +Scenario: ReplyBot overrides ExplicitRoom when replying to bot + Test: test_reply_bot_overrides_explicit_room + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the user has `ExplicitOverride::Room` set + And the target chip displays "To room" + When the user clicks reply on the bot's message + Then the `ResolvedTarget` is `ReplyBot` with the bot's user ID + And the target chip displays "Reply → {display_name}" + But the `ExplicitOverride` remains `Room` + And when the user cancels the reply, the target reverts to `ExplicitRoom` + +Scenario: Chip dismiss clears ExplicitRoom back to RoomDefault + Test: test_chip_dismiss_explicit_room_to_room_default + Given a room with bound bot "@octosbot:127.0.0.1:8128" + And the `ExplicitOverride` is `Room` + And the target chip displays "To room" + When the user clicks the × button on the target chip + Then the `ExplicitOverride` resets to `None` + And the `ResolvedTarget` is `RoomDefault` with the bound bot's user ID + And the target chip displays "Default: {display_name}" in subdued style + +Scenario: Chip dismiss in room without bound bot returns NoTarget + Test: test_chip_dismiss_no_bound_bot + Given a room with no bound bot + And the user has somehow set `ExplicitOverride::Bot` with a user ID + When the user clicks the × button on the target chip + Then the `ExplicitOverride` resets to `None` + And the `ResolvedTarget` is `NoTarget` + And no target chip is displayed + +Scenario: is_known_or_likely_bot detects configured parent bot via resolved_parent_bot_user_id + Test: test_bot_detection_configured_parent + Given `resolved_parent_bot_user_id` is "@octosbot:127.0.0.1:8128" + And `known_bot_user_ids` is empty + When `is_known_or_likely_bot` is called with user ID "@octosbot:127.0.0.1:8128" + Then the function returns `true` via `resolved_parent_bot_user_id` matching + +Scenario: is_known_or_likely_bot detects bot by heuristic when not in known list + Test: test_bot_detection_heuristic_fallback + Given `resolved_parent_bot_user_id` is `None` + And `known_bot_user_ids` is empty + When `is_known_or_likely_bot` is called with user ID "@myservice_bot:other.server" + Then the function returns `true` via localpart heuristic (ends with "_bot") + +Scenario: is_known_or_likely_bot detects child bots via known list + Test: test_bot_detection_child_bot + Given `known_bot_user_ids` containing "@octosbot_weather:127.0.0.1:8128" + And `resolved_parent_bot_user_id` is `None` + When `is_known_or_likely_bot` is called with user ID "@octosbot_weather:127.0.0.1:8128" + Then the function returns `true` via `known_bot_user_ids` matching + +Scenario: is_known_or_likely_bot rejects normal users + Test: test_bot_detection_rejects_normal_user + Given `resolved_parent_bot_user_id` is `None` + And `known_bot_user_ids` is empty + When `is_known_or_likely_bot` is called with user ID "@alice:127.0.0.1:8128" + Then the function returns `false` diff --git a/specs/task-tg-bot-ui-alignment.spec.md b/specs/task-tg-bot-ui-alignment.spec.md new file mode 100644 index 000000000..016cc1b38 --- /dev/null +++ b/specs/task-tg-bot-ui-alignment.spec.md @@ -0,0 +1,114 @@ +spec: task +name: "Telegram Bot UI Alignment — Phase 1: Bot Badge & Message Identity" +inherits: project +tags: [bot, ui, telegram-parity] +depends: [] +estimate: 1d +--- + +## Intent + +在 Robrix 的消息流中对齐 Telegram 的 bot 对话体验。当前 Robrix 的 bot 消息与普通用户消息在视觉上完全相同,用户无法快速识别哪些消息来自 bot。Telegram 在 bot 消息的用户名旁显示一个蓝色 "bot" 标签,这是 bot 对话体验的基础视觉元素。 + +本 spec 覆盖 Phase 1(Bot 身份标识)和 Phase 2(`/` 命令菜单),这两项是让 bot 对话"感觉像 Telegram"的最小可行改动。 + +## Decisions + +- Bot 徽章样式: 在消息 username Label 右侧添加一个小号 RoundedView,内含 "bot" 文本,背景色使用 `COLOR_ACTIVE_PRIMARY`,文字白色,圆角 3px,高度 16px +- Bot 检测逻辑: 复用现有 `is_likely_bot_user_id()` 和 `is_likely_bot_member()` 函数(`room_screen.rs`),不引入新的检测机制 +- Bot 徽章数据传递: 在 `populate_message_view()` 中检测发送者是否为 bot,通过 `set_visible()` 控制徽章显示/隐藏 +- `/` 命令菜单: 复用现有 `CommandTextInput` 组件(已用于 `@mention`),添加 `/` 作为第二个 trigger character +- 命令来源: 硬编码一组 BotFather 基础命令(`/createbot`, `/deletebot`, `/listbots`, `/bothelp`),不实现动态命令注册 +- 命令列表 UI: 复用 `@mention` 弹出列表的样式,每项显示命令名和描述 + +## Boundaries + +### Allowed Changes +- src/home/room_screen.rs — 消息模板添加 bot 徽章 widget,`populate_message_view()` 添加 bot 检测逻辑 +- src/shared/mentionable_text_input.rs — 添加 `/` trigger 和命令列表数据源 +- src/room/room_input_bar.rs — 必要时调整输入栏与命令补全的集成 +- resources/i18n/en.json — 添加命令描述的 i18n 键 +- resources/i18n/zh-CN.json — 添加命令描述的 i18n 键 + +### Forbidden +- 不要修改 bot 检测逻辑(`is_likely_bot_user_id` / `is_likely_bot_member`)的判断规则 +- 不要添加新的 cargo 依赖 +- 不要修改消息的整体布局结构(avatar 位置、消息气泡宽度等) +- 不要实现 Inline Keyboard 按钮(Phase 3 范围) +- 不要实现 Reply Keyboard(Phase 3 范围) +- 不要实现动态命令注册协议(超出当前范围) + +## Out of Scope + +- Inline Keyboard 按钮(消息下方的可点击按钮) +- Reply Keyboard(替换用户键盘的预设选项) +- Bot Profile 页面(专属的 bot 信息/描述页面) +- Menu Button(输入框旁的 bot 菜单按钮) +- Bot 欢迎消息 / "What can this bot do?" 描述框 +- 动态命令注册(bot 向客户端声明自己支持的命令) +- Bot-to-Bot 通信 + +## Completion Criteria + +Scenario: Bot badge visible on bot messages + Test: test_bot_badge_visible + Given a room with user "alice" and bot "@octosbot:127.0.0.1:8128" + When the bot sends a message to the room + Then the bot's message displays username "BotFather" with a "bot" badge label visible next to it + And the badge has a distinct background color (not the same as username text) + +Scenario: Bot badge hidden on regular user messages + Test: test_bot_badge_hidden_for_users + Given a room with user "alice" and bot "@octosbot:127.0.0.1:8128" + When user "alice" sends a message + Then "alice"'s message does NOT display a "bot" badge label + +Scenario: Bot badge visible on condensed messages + Test: test_bot_badge_condensed + Given a bot sends multiple consecutive messages + When the messages are rendered as CondensedMessage (no avatar/profile) + Then the condensed messages do NOT show the bot badge (since username row is hidden) + +Scenario: Bot badge detects known bot patterns + Test: test_bot_badge_detection + Given a user with ID "@mybot:server" or "@octosbot_translator:server" + When their message is rendered + Then the bot badge is visible because `is_likely_bot_user_id()` returns true + +Scenario: Slash command menu appears on "/" input + Test: test_slash_command_trigger + Given the user is in a room with bot features enabled + When the user types "/" in the message input field + Then a popup list appears showing available bot commands with descriptions + +Scenario: Slash command menu absent without "/" + Test: test_slash_command_no_trigger + Given the user is in a room + When the user types a regular message without "/" + Then no command popup list appears + +Scenario: Selecting a command from the menu inserts it + Test: test_slash_command_selection + Given the slash command popup is visible + When the user selects "/listbots" from the list + Then "/listbots" is inserted into the message input field + And the popup closes + +Scenario: Slash command menu shows BotFather commands with descriptions + Test: test_slash_command_list_content + Given the slash command popup is visible + Then the list contains at least: "/createbot", "/deletebot", "/listbots", "/bothelp" + And each command entry displays both a command name and a description label in the popup list style matching the @mention popup + +Scenario: Slash command menu handles empty prefix gracefully + Test: test_slash_command_empty_prefix + Given the user is in a room + When the user types "/" followed by a non-matching string "zzzznotacommand" + Then the command popup shows an empty list or closes + And no error is displayed + +Scenario: Bot badge not shown for user with "bot" in display name but normal user ID + Test: test_bot_badge_false_positive + Given a regular user with ID "@roberto:server" and display name "Roberto" + When their message is rendered + Then the bot badge is NOT visible because `is_likely_bot_user_id()` returns false for "roberto" diff --git a/src/app.rs b/src/app.rs index 63671252f..a729bb2eb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1094,6 +1094,7 @@ impl MatchEvent for App { message: RoomMessageEventContent::notice_plain(format!("[App Service] {message}")), replied_to: None, target_user_id: None, + explicit_room: false, #[cfg(feature = "tsp")] sign_with_tsp: false, }); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 5808f2f37..a5e8d98ca 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -447,6 +447,61 @@ fn is_likely_bot_user_id( || (localpart.ends_with("bot") && localpart.len() > 3) } +pub(crate) fn is_known_or_likely_bot( + user_id: &UserId, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> bool { + known_bot_user_ids + .iter() + .any(|known_bot_user_id| known_bot_user_id.as_str() == user_id.as_str()) + || resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) + || is_likely_bot_user_id(user_id, resolved_parent_bot_user_id) +} + +fn collect_room_bot_user_ids( + room_members: &[RoomMember], + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], + persisted_room_bot_user_ids: &[OwnedUserId], +) -> Vec { + let own_user_id = current_user_id(); + let mut room_bot_user_ids = Vec::::new(); + + for persisted_room_bot_user_id in persisted_room_bot_user_ids { + if room_bot_user_ids + .iter() + .all(|existing_user_id| existing_user_id.as_str() != persisted_room_bot_user_id.as_str()) + { + room_bot_user_ids.push(persisted_room_bot_user_id.clone()); + } + } + + for room_member in room_members.iter().filter(|room_member| + own_user_id + .as_deref() + .is_none_or(|own_user_id| room_member.user_id() != own_user_id) + ) { + if is_known_or_likely_bot( + room_member.user_id(), + resolved_parent_bot_user_id, + known_bot_user_ids, + ) || is_likely_bot_member(room_member, resolved_parent_bot_user_id) + { + let user_id = room_member.user_id().to_owned(); + if room_bot_user_ids + .iter() + .all(|existing_user_id| existing_user_id.as_str() != user_id.as_str()) + { + room_bot_user_ids.push(user_id); + } + } + } + + room_bot_user_ids.sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + room_bot_user_ids +} + fn is_likely_bot_member( room_member: &RoomMember, resolved_parent_bot_user_id: Option<&UserId>, @@ -3522,13 +3577,49 @@ impl Widget for RoomScreen { let room_props = if let Some(tl) = self.tl_state.as_ref() { let room_id = tl.kind.room_id().clone(); let room_members = tl.room_members.clone(); - let (app_service_enabled, app_service_room_bound, bound_bot_user_id) = scope + let ( + app_service_enabled, + app_service_room_bound, + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) = scope .data .get::() .map(|app_state| { let app_service_enabled = app_state.bot_settings.enabled; let persisted_bound_bot_user_id = app_state.bot_settings.bound_bot_user_id(&room_id).map(ToOwned::to_owned); + let persisted_room_bot_user_ids = if app_service_enabled { + app_state.bot_settings.bound_bot_user_ids(&room_id) + } else { + Vec::new() + }; + let resolved_parent_bot_user_id = if app_service_enabled { + app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + .ok() + } else { + None + }; + let known_bot_user_ids = if app_service_enabled { + app_state.bot_settings.known_bot_user_ids() + } else { + Vec::new() + }; + let room_bot_user_ids = room_members + .as_ref() + .map(|members| + collect_room_bot_user_ids( + members.as_ref(), + resolved_parent_bot_user_id.as_deref(), + &known_bot_user_ids, + &persisted_room_bot_user_ids, + ) + ) + .unwrap_or(persisted_room_bot_user_ids); let detected_bound_bot_user_id = room_members .as_ref() .and_then(|members| @@ -3557,21 +3648,27 @@ impl Widget for RoomScreen { app_service_enabled, app_service_room_bound, bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, ) }) - .unwrap_or((false, false, None)); + .unwrap_or((false, false, None, None, Vec::new(), Vec::new())); RoomScreenProps { room_screen_widget_uid, room_name_id: self.room_name_id.clone().unwrap_or_else(|| RoomNameId::empty(room_id)), timeline_kind: tl.kind.clone(), room_members, + room_bot_user_ids, room_members_sync_pending: tl.room_members_sync_pending, room_members_sort: tl.room_members_sort.clone(), room_avatar_url: self.room_avatar_url.clone(), app_service_enabled, app_service_room_bound, bound_bot_user_id, + resolved_parent_bot_user_id, + known_bot_user_ids, } } else if let Some(room_name) = &self.room_name_id { // Fallback case: we have a room_name but no tl_state yet @@ -3581,12 +3678,15 @@ impl Widget for RoomScreen { timeline_kind: self.timeline_kind.clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, + room_bot_user_ids: Vec::new(), room_members_sort: None, room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, bound_bot_user_id: None, + resolved_parent_bot_user_id: None, + known_bot_user_ids: Vec::new(), } } else { // No room selected yet, skip event handling that requires room context @@ -3601,12 +3701,15 @@ impl Widget for RoomScreen { room_name_id: RoomNameId::empty(room_id.clone()), timeline_kind: TimelineKind::MainRoom { room_id }, room_members: None, + room_bot_user_ids: Vec::new(), room_members_sort: None, room_members_sync_pending: false, room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, bound_bot_user_id: None, + resolved_parent_bot_user_id: None, + known_bot_user_ids: Vec::new(), } }; let mut room_scope = Scope::with_props(&room_props); @@ -4406,6 +4509,7 @@ impl RoomScreen { message: RoomMessageEventContent::notice_plain(message), replied_to: None, target_user_id: None, + explicit_room: false, #[cfg(feature = "tsp")] sign_with_tsp: false, }); @@ -4452,6 +4556,7 @@ impl RoomScreen { .bot_settings .bound_bot_user_id(room_id.as_ref()) .map(ToOwned::to_owned), + explicit_room: false, #[cfg(feature = "tsp")] sign_with_tsp: false, }); @@ -6473,6 +6578,7 @@ pub struct RoomScreenProps { pub room_name_id: RoomNameId, pub timeline_kind: TimelineKind, pub room_members: Option>>, + pub room_bot_user_ids: Vec, pub room_members_sync_pending: bool, /// Pre-computed sort order for room members (for mention search optimization). pub room_members_sort: Option>, @@ -6480,6 +6586,8 @@ pub struct RoomScreenProps { pub app_service_enabled: bool, pub app_service_room_bound: bool, pub bound_bot_user_id: Option, + pub resolved_parent_bot_user_id: Option, + pub known_bot_user_ids: Vec, } @@ -9210,4 +9318,53 @@ mod tests { fn bot_badge_text_is_centered_within_badge() { assert!(bot_badge_label_center_y() < (BOT_BADGE_HEIGHT * 0.5)); } + + #[test] + fn test_bot_detection_configured_parent() { + let user_id: OwnedUserId = "@octosbot:127.0.0.1:8128".try_into().unwrap(); + let resolved_parent_bot_user_id = Some(user_id.clone()); + let known_bot_user_ids = Vec::new(); + + assert!(is_known_or_likely_bot( + user_id.as_ref(), + resolved_parent_bot_user_id.as_deref(), + &known_bot_user_ids, + )); + } + + #[test] + fn test_bot_detection_heuristic_fallback() { + let user_id: OwnedUserId = "@myservice_bot:other.server".try_into().unwrap(); + let known_bot_user_ids = Vec::new(); + + assert!(is_known_or_likely_bot( + user_id.as_ref(), + None, + &known_bot_user_ids, + )); + } + + #[test] + fn test_bot_detection_child_bot() { + let user_id: OwnedUserId = "@octosbot_weather:127.0.0.1:8128".try_into().unwrap(); + let known_bot_user_ids = vec![user_id.clone()]; + + assert!(is_known_or_likely_bot( + user_id.as_ref(), + None, + &known_bot_user_ids, + )); + } + + #[test] + fn test_bot_detection_rejects_normal_user() { + let user_id: OwnedUserId = "@alice:127.0.0.1:8128".try_into().unwrap(); + let known_bot_user_ids = Vec::new(); + + assert!(!is_known_or_likely_bot( + user_id.as_ref(), + None, + &known_bot_user_ids, + )); + } } diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 391282cad..8abdac22b 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -20,8 +20,8 @@ use makepad_widgets::*; use matrix_sdk::room::reply::{EnforceThread, Reply}; use ruma::events::room::message::AddMentions; use matrix_sdk_ui::timeline::{EmbeddedEvent, EventTimelineItem, TimelineEventItemId}; -use ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId}; -use crate::{app::AppState, 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}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, shared::{avatar::AvatarWidgetRefExt, file_upload_modal::{FileData, FileLoadedData, FilePreviewerAction}, 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 ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId, UserId}; +use crate::{app::AppState, home::{editing_pane::{EditingPaneState, EditingPaneWidgetExt, EditingPaneWidgetRefExt}, location_preview::{LocationPreviewWidgetExt, LocationPreviewWidgetRefExt}, room_screen::{MessageAction, RoomScreenProps, is_known_or_likely_bot, populate_preview_of_timeline_item}, tombstone_footer::{SuccessorRoomDetails, TombstoneFooterWidgetExt}, upload_progress::UploadProgressViewWidgetRefExt}, i18n::{AppLanguage, tr_fmt, tr_key}, location::init_location_subscriber, room::translation::{self, TRANSLATION_REQUEST_ID}, shared::{avatar::AvatarWidgetRefExt, file_upload_modal::{FileData, FileLoadedData, FilePreviewerAction}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, mentionable_text_input::MentionableTextInputWidgetExt, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{MatrixRequest, TimelineKind, UserPowerLevels, submit_async_request}, utils}; #[cfg(not(any(target_os = "ios", target_os = "android")))] use crate::shared::file_upload_modal::{FilePreviewerMetaData, ThumbnailData}; @@ -83,6 +83,353 @@ fn compute_translation_apply_outcome(translated_text: &str) -> TranslationApplyO } } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +enum ExplicitOverride { + #[default] + None, + Bot(OwnedUserId), + Room, +} + +impl ExplicitOverride { + fn cleared(&self) -> Self { + Self::None + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ResolvedTarget { + NoTarget, + RoomDefault(OwnedUserId), + ExplicitBot(OwnedUserId), + ExplicitRoom, + ReplyBot(OwnedUserId), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct TargetChipPresentation { + visible: bool, + label: String, + subdued: bool, + dismissible: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum TargetMenuSelection { + Room, + BoundBot, +} + +fn known_bot_candidates<'a>( + bound_bot_user_id: Option<&'a UserId>, + resolved_parent_bot_user_id: Option<&'a UserId>, + room_bot_user_ids: &'a [OwnedUserId], + known_bot_user_ids: &'a [OwnedUserId], +) -> Vec<&'a UserId> { + let mut candidates = Vec::with_capacity(room_bot_user_ids.len() + known_bot_user_ids.len() + 2); + if let Some(bound_bot_user_id) = bound_bot_user_id { + candidates.push(bound_bot_user_id); + } + if let Some(resolved_parent_bot_user_id) = resolved_parent_bot_user_id + && candidates + .iter() + .all(|candidate| *candidate != resolved_parent_bot_user_id) + { + candidates.push(resolved_parent_bot_user_id); + } + for room_bot_user_id in room_bot_user_ids { + let room_bot_user_id_ref: &UserId = room_bot_user_id.as_ref(); + if candidates + .iter() + .all(|candidate| *candidate != room_bot_user_id_ref) + { + candidates.push(room_bot_user_id_ref); + } + } + for known_bot_user_id in known_bot_user_ids { + let known_bot_user_id_ref: &UserId = known_bot_user_id.as_ref(); + if candidates + .iter() + .all(|candidate| *candidate != known_bot_user_id_ref) + { + candidates.push(known_bot_user_id_ref); + } + } + candidates +} + +fn is_matrix_localpart_mention_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | '=' | '/' | '+') +} + +fn contains_matrix_localpart_mention(text: &str, user_id: &UserId) -> bool { + let mention = format!("@{}", user_id.localpart()); + for (idx, _) in text.match_indices(&mention) { + let start_ok = text[..idx] + .chars() + .next_back() + .is_none_or(|c| !is_matrix_localpart_mention_char(c)); + let end_idx = idx + mention.len(); + let end_ok = text[end_idx..] + .chars() + .next() + .is_none_or(|c| !is_matrix_localpart_mention_char(c)); + if start_ok && end_ok { + return true; + } + } + false +} + +fn text_mentions_known_bot( + text: &str, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + room_bot_user_ids: &[OwnedUserId], + known_bot_user_ids: &[OwnedUserId], +) -> bool { + known_bot_candidates( + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) + .into_iter() + .any(|candidate| { + text.contains(candidate.as_str()) || contains_matrix_localpart_mention(text, candidate) + }) +} + +fn message_mentions_known_bot( + message: &RoomMessageEventContent, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + room_bot_user_ids: &[OwnedUserId], + known_bot_user_ids: &[OwnedUserId], +) -> bool { + if message.mentions.as_ref().is_some_and(|mentions| { + mentions.user_ids.iter().any(|user_id| { + room_bot_user_ids + .iter() + .any(|room_bot_user_id| room_bot_user_id.as_str() == user_id.as_str()) + || is_known_or_likely_bot( + user_id.as_ref(), + resolved_parent_bot_user_id, + known_bot_user_ids, + ) + }) + }) { + return true; + } + + if text_mentions_known_bot( + message.body(), + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) { + return true; + } + + match &message.msgtype { + MessageType::Text(content) => content.formatted.as_ref().is_some_and(|formatted| { + text_mentions_known_bot( + &formatted.body, + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) + }), + MessageType::Notice(content) => content.formatted.as_ref().is_some_and(|formatted| { + text_mentions_known_bot( + &formatted.body, + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) + }), + MessageType::Emote(content) => content.formatted.as_ref().is_some_and(|formatted| { + text_mentions_known_bot( + &formatted.body, + bound_bot_user_id, + resolved_parent_bot_user_id, + room_bot_user_ids, + known_bot_user_ids, + ) + }), + _ => false, + } +} + +fn routing_directives_for_message( + resolved_target: &ResolvedTarget, + message_mentions_bot: bool, +) -> (Option, bool) { + let explicit_room = matches!(resolved_target, ResolvedTarget::ExplicitRoom); + let target_user_id = if message_mentions_bot { + None + } else { + resolved_target_user_id(resolved_target) + }; + (target_user_id, explicit_room) +} + +fn resolve_target( + explicit_override: &ExplicitOverride, + replying_to_sender: Option<&UserId>, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> ResolvedTarget { + if let Some(replying_to_sender) = replying_to_sender + && is_known_or_likely_bot( + replying_to_sender, + resolved_parent_bot_user_id, + known_bot_user_ids, + ) + { + return ResolvedTarget::ReplyBot(replying_to_sender.to_owned()); + } + + match explicit_override { + ExplicitOverride::Bot(bot_user_id) => ResolvedTarget::ExplicitBot(bot_user_id.clone()), + ExplicitOverride::Room => ResolvedTarget::ExplicitRoom, + ExplicitOverride::None => bound_bot_user_id + .map(|bot_user_id| ResolvedTarget::RoomDefault(bot_user_id.to_owned())) + .unwrap_or(ResolvedTarget::NoTarget), + } +} + +fn resolved_target_user_id(target: &ResolvedTarget) -> Option { + match target { + ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => None, + ResolvedTarget::RoomDefault(bot_user_id) + | ResolvedTarget::ExplicitBot(bot_user_id) + | ResolvedTarget::ReplyBot(bot_user_id) => Some(bot_user_id.clone()), + } +} + +#[cfg(test)] +fn clear_explicit_override_result(bound_bot_user_id: Option<&UserId>) -> ResolvedTarget { + bound_bot_user_id + .map(|bot_user_id| ResolvedTarget::RoomDefault(bot_user_id.to_owned())) + .unwrap_or(ResolvedTarget::NoTarget) +} + +fn format_target_chip_presentation( + app_language: AppLanguage, + resolved_target: &ResolvedTarget, + bot_display_name: Option<&str>, +) -> TargetChipPresentation { + let target_label = match resolved_target { + ResolvedTarget::RoomDefault(bot_user_id) + | ResolvedTarget::ExplicitBot(bot_user_id) + | ResolvedTarget::ReplyBot(bot_user_id) => bot_display_name + .map(str::trim) + .filter(|display_name| !display_name.is_empty()) + .unwrap_or_else(|| bot_user_id.localpart()) + .to_string(), + ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => String::new(), + }; + + match resolved_target { + ResolvedTarget::NoTarget => TargetChipPresentation { + visible: false, + label: String::new(), + subdued: false, + dismissible: false, + }, + ResolvedTarget::RoomDefault(_) => TargetChipPresentation { + visible: true, + label: tr_fmt( + app_language, + "room_input_bar.target.default", + &[("display_name", &target_label)], + ), + subdued: true, + dismissible: false, + }, + ResolvedTarget::ExplicitBot(_) => TargetChipPresentation { + visible: true, + label: tr_fmt( + app_language, + "room_input_bar.target.to_bot", + &[("display_name", &target_label)], + ), + subdued: false, + dismissible: true, + }, + ResolvedTarget::ExplicitRoom => TargetChipPresentation { + visible: true, + label: tr_key(app_language, "room_input_bar.target.to_room").to_string(), + subdued: false, + dismissible: true, + }, + ResolvedTarget::ReplyBot(_) => TargetChipPresentation { + visible: true, + label: tr_fmt( + app_language, + "room_input_bar.target.reply_bot", + &[("display_name", &target_label)], + ), + subdued: false, + dismissible: false, + }, + } +} + +fn apply_target_menu_selection( + selection: TargetMenuSelection, + bound_bot_user_id: Option<&UserId>, +) -> ExplicitOverride { + match selection { + TargetMenuSelection::Room => ExplicitOverride::Room, + TargetMenuSelection::BoundBot => bound_bot_user_id + .map(|bot_user_id| ExplicitOverride::Bot(bot_user_id.to_owned())) + .unwrap_or_default(), + } +} + +fn resolve_send_target( + explicit_override: &ExplicitOverride, + replying_to_sender: Option<&UserId>, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> ResolvedTarget { + resolve_target( + explicit_override, + replying_to_sender, + bound_bot_user_id, + resolved_parent_bot_user_id, + known_bot_user_ids, + ) +} + +#[cfg(test)] +fn resolve_restored_target( + explicit_override: &ExplicitOverride, + replying_to_sender: Option<&UserId>, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> ResolvedTarget { + resolve_target( + explicit_override, + replying_to_sender, + bound_bot_user_id, + resolved_parent_bot_user_id, + known_bot_user_ids, + ) +} + +fn restored_explicit_override(saved_state: &RoomInputBarState) -> ExplicitOverride { + saved_state.explicit_override.clone() +} + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -171,6 +518,46 @@ script_mod! { } } + mod.widgets.TargetChipButton = Button { + width: Fit, height: Fit + padding: Inset{left: 12, right: 12, top: 6, bottom: 6} + draw_bg +: { + color: #xF0F4FA + color_hover: #xE0E8F0 + color_down: #xD0D8E8 + border_radius: 13.0 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + text: "Target" + } + + mod.widgets.TargetMenuButton = Button { + width: Fit, height: Fit + padding: Inset{left: 12, right: 12, top: 7, bottom: 7} + draw_bg +: { + color: (COLOR_PRIMARY) + color_hover: #F4F7FC + color_down: #E8EEF8 + border_radius: 7.0 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } + } + text: "Option" + } + mod.widgets.RoomInputBar = set_type_default() do #(RoomInputBar::register_widget(vm)) { ..mod.widgets.RoundedView @@ -197,6 +584,56 @@ script_mod! { // shadow_offset: vec2(0.0,0.0) } + target_indicator := View { + visible: false + width: Fill, height: Fit + flow: Down + padding: Inset{left: 6, right: 6, top: 6, bottom: 2} + spacing: 4 + + target_chip_row := View { + width: Fit, height: Fit + flow: Right + align: Align{y: 0.5} + spacing: 6 + + target_chip_button := mod.widgets.TargetChipButton { text: "Target" } + + target_chip_dismiss_button := RobrixNeutralIconButton { + visible: false + width: Fit, height: Fit + spacing: 0 + text: "" + padding: Inset{left: 6, right: 6, top: 6, bottom: 6} + draw_icon +: { + svg: (ICON_CLOSE) + } + icon_walk: Walk{width: 10, height: 10} + } + } + + target_menu_popup := RoundedView { + visible: false + width: Fit, height: Fit + flow: Down + padding: 4 + spacing: 4 + show_bg: true + draw_bg +: { + color: #xF7F9FD + border_radius: 8.0 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + + target_menu_room_button := mod.widgets.TargetMenuButton { text: "To room" } + target_menu_bound_bot_button := mod.widgets.TargetMenuButton { + visible: false + text: "Bot" + } + } + } + // The top-most element is a preview of the message that the user is replying to, if any. replying_preview := ReplyingPreview { } @@ -601,8 +1038,8 @@ 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 most recently selected explicit bot target for this room. - #[rust] active_target_user_id: Option, + /// The user's explicit target override for this room. + #[rust] explicit_override: ExplicitOverride, /// Whether the location card is currently expanded. #[rust] is_location_card_expanded: bool, /// Whether the emoji picker popup is currently expanded. @@ -623,6 +1060,8 @@ pub struct RoomInputBar { #[rust] translation_preview_text: Option, /// Whether a translation HTTP request is currently in flight. #[rust] translation_request_pending: bool, + /// Whether the target selection menu is currently expanded. + #[rust] is_target_menu_visible: bool, /// Debounce timer for translation requests. #[rust] translation_debounce_timer: Timer, /// The last source text that was sent for translation. @@ -766,6 +1205,7 @@ impl Widget for RoomInputBar { } } + self.sync_target_indicator(cx, room_screen_props); self.view.handle_event(cx, event, scope); } @@ -814,6 +1254,16 @@ impl Widget for RoomInputBar { } impl RoomInputBar { + fn current_resolved_target(&self, room_screen_props: &RoomScreenProps) -> ResolvedTarget { + resolve_send_target( + &self.explicit_override, + self.replying_to_sender(), + room_screen_props.bound_bot_user_id.as_deref(), + room_screen_props.resolved_parent_bot_user_id.as_deref(), + &room_screen_props.known_bot_user_ids, + ) + } + fn set_app_language(&mut self, cx: &mut Cx, app_language: AppLanguage) { self.app_language = app_language; self.app_language_initialized = true; @@ -858,20 +1308,123 @@ impl RoomInputBar { self.redraw(cx); } - fn resolve_target_user_id( - &mut self, - explicit_target_user_id: Option, - reply_target_user_id: Option, - fallback_target_user_id: Option, - ) -> Option { - if let Some(explicit_target_user_id) = explicit_target_user_id { - self.active_target_user_id = Some(explicit_target_user_id.clone()); - Some(explicit_target_user_id) - } else if let Some(reply_target_user_id) = reply_target_user_id { - self.active_target_user_id = Some(reply_target_user_id.clone()); - Some(reply_target_user_id) + fn replying_to_sender(&self) -> Option<&UserId> { + self.replying_to + .as_ref() + .map(|(event_tl_item, _embedded_event)| event_tl_item.sender()) + } + + fn target_display_name_for_user( + &self, + room_screen_props: &RoomScreenProps, + user_id: &UserId, + ) -> Option { + room_screen_props + .room_members + .as_ref() + .and_then(|members| { + members + .iter() + .find(|member| member.user_id() == user_id) + .and_then(|member| { + member + .display_name() + .map(str::trim) + .filter(|display_name| !display_name.is_empty()) + .map(ToOwned::to_owned) + }) + }) + } + + fn resolved_target_display_name( + &self, + room_screen_props: &RoomScreenProps, + resolved_target: &ResolvedTarget, + ) -> Option { + match resolved_target { + ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => None, + ResolvedTarget::RoomDefault(user_id) + | ResolvedTarget::ExplicitBot(user_id) + | ResolvedTarget::ReplyBot(user_id) => { + self.target_display_name_for_user(room_screen_props, user_id.as_ref()) + } + } + } + + fn sync_target_indicator(&mut self, cx: &mut Cx, room_screen_props: &RoomScreenProps) { + let resolved_target = self.current_resolved_target(room_screen_props); + let bot_display_name = self.resolved_target_display_name(room_screen_props, &resolved_target); + let presentation = format_target_chip_presentation( + self.app_language, + &resolved_target, + bot_display_name.as_deref(), + ); + + self.view + .view(cx, ids!(target_indicator)) + .set_visible(cx, presentation.visible); + self.view + .view(cx, ids!(target_menu_popup)) + .set_visible(cx, presentation.visible && self.is_target_menu_visible); + + if !presentation.visible { + self.is_target_menu_visible = false; + return; + } + + let mut target_chip_button = self.button(cx, ids!(target_chip_button)); + target_chip_button.set_text(cx, &presentation.label); + let (text_color, border_color, bg_color, hover_color, down_color): (Vec4, Vec4, Vec4, Vec4, Vec4) = if presentation.subdued { + ( + COLOR_MESSAGE_NOTICE_TEXT, + COLOR_BG_DISABLED, + vec4(0.89, 0.89, 0.89, 1.0), + vec4(0.84, 0.84, 0.84, 1.0), + vec4(0.78, 0.78, 0.78, 1.0), + ) } else { - self.active_target_user_id.clone().or(fallback_target_user_id) + ( + COLOR_ACTIVE_PRIMARY_DARKER, + COLOR_BG_DISABLED, + COLOR_BG_PREVIEW, + COLOR_BG_PREVIEW_HOVER, + vec4(0.77, 0.86, 0.96, 1.0), + ) + }; + script_apply_eval!(cx, target_chip_button, { + draw_bg +: { + border_color: #(border_color), + color: #(bg_color), + color_hover: #(hover_color), + color_down: #(down_color), + } + draw_text +: { + color: #(text_color), + color_hover: #(text_color), + color_down: #(text_color), + } + }); + + self.button(cx, ids!(target_chip_dismiss_button)) + .set_visible(cx, presentation.dismissible); + self.button(cx, ids!(target_menu_room_button)) + .set_text(cx, tr_key(self.app_language, "room_input_bar.target.menu.room")); + + let bound_bot_option = room_screen_props.bound_bot_user_id.as_deref().map(|bound_bot_user_id| { + self.target_display_name_for_user(room_screen_props, bound_bot_user_id) + .unwrap_or_else(|| bound_bot_user_id.localpart().to_string()) + }); + self.button(cx, ids!(target_menu_bound_bot_button)) + .set_visible(cx, bound_bot_option.is_some()); + if let Some(bound_bot_option) = bound_bot_option { + self.button(cx, ids!(target_menu_bound_bot_button)).set_text( + cx, + &tr_fmt( + self.app_language, + "room_input_bar.target.menu.bound_bot", + &[("display_name", &bound_bot_option)], + ), + ); } } @@ -884,12 +1437,42 @@ impl RoomInputBar { let mentionable_text_input = self.mentionable_text_input(cx, ids!(mentionable_text_input)); let text_input = mentionable_text_input.text_input_ref(); + if self.button(cx, ids!(target_chip_button)).clicked(actions) { + self.is_target_menu_visible = !self.is_target_menu_visible; + self.redraw(cx); + } + + if self.button(cx, ids!(target_chip_dismiss_button)).clicked(actions) { + self.explicit_override = self.explicit_override.cleared(); + self.is_target_menu_visible = false; + self.redraw(cx); + } + + if self.button(cx, ids!(target_menu_room_button)).clicked(actions) { + self.explicit_override = apply_target_menu_selection( + TargetMenuSelection::Room, + room_screen_props.bound_bot_user_id.as_deref(), + ); + self.is_target_menu_visible = false; + self.redraw(cx); + } + + if self.button(cx, ids!(target_menu_bound_bot_button)).clicked(actions) { + self.explicit_override = apply_target_menu_selection( + TargetMenuSelection::BoundBot, + room_screen_props.bound_bot_user_id.as_deref(), + ); + self.is_target_menu_visible = false; + self.redraw(cx); + } + // Clear the replying-to preview pane if the "cancel reply" button was clicked // or if the `Escape` key was pressed within the message input box. if self.button(cx, ids!(cancel_reply_button)).clicked(actions) || text_input.escaped(actions) { self.clear_replying_to(cx); + self.is_target_menu_visible = false; self.redraw(cx); } @@ -897,6 +1480,7 @@ impl RoomInputBar { if self.button(cx, ids!(more_actions_button)).clicked(actions) { self.is_location_card_expanded = !self.is_location_card_expanded; self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, self.is_location_card_expanded); + self.is_target_menu_visible = false; self.redraw(cx); } @@ -904,12 +1488,14 @@ impl RoomInputBar { if self.button(cx, ids!(emoji_picker_button)).clicked(actions) { self.is_emoji_picker_expanded = !self.is_emoji_picker_expanded; self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, self.is_emoji_picker_expanded); + self.is_target_menu_visible = false; 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.is_target_menu_visible = false; self.open_file_picker(cx); } @@ -945,6 +1531,7 @@ impl RoomInputBar { self.is_emoji_picker_expanded = false; self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); + self.is_target_menu_visible = false; self.redraw(cx); } @@ -959,10 +1546,12 @@ impl RoomInputBar { self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; + self.is_target_menu_visible = false; self.redraw(cx); } else { self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; + self.is_target_menu_visible = false; let button_rect = self.button(cx, ids!(translate_button)).area().clipped_rect(cx); if button_rect.size.x > 0.0 { cx.widget_action( @@ -984,6 +1573,7 @@ impl RoomInputBar { self.view.label(cx, ids!(translation_preview_text)).set_text(cx, &outcome.preserved_preview_text); self.view.view(cx, ids!(translation_preview)).set_visible(cx, outcome.keep_preview_visible); self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); + self.is_target_menu_visible = false; self.redraw(cx); } } @@ -997,6 +1587,7 @@ impl RoomInputBar { self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; + self.is_target_menu_visible = false; self.redraw(cx); } @@ -1005,6 +1596,7 @@ impl RoomInputBar { log!("Location card clicked; requesting current location..."); self.is_location_card_expanded = false; self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); + self.is_target_menu_visible = false; if let Err(_e) = init_location_subscriber(cx) { error!("Failed to initialize location subscriber"); enqueue_popup_notification( @@ -1022,6 +1614,7 @@ impl RoomInputBar { room_screen_props.room_screen_widget_uid, MessageAction::ShowThreadsPane, ); + self.is_target_menu_visible = false; self.redraw(cx); } @@ -1030,6 +1623,7 @@ impl RoomInputBar { room_screen_props.room_screen_widget_uid, MessageAction::ShowRoomInfoPane, ); + self.is_target_menu_visible = false; self.redraw(cx); } @@ -1043,10 +1637,9 @@ impl RoomInputBar { LocationMessageEventContent::new(geo_uri.clone(), geo_uri) ) ); - let reply_target_user_id = self - .replying_to - .as_ref() - .map(|(event_tl_item, _emb)| event_tl_item.sender().to_owned()); + let resolved_target = self.current_resolved_target(room_screen_props); + let target_user_id = resolved_target_user_id(&resolved_target); + let explicit_room = matches!(resolved_target, ResolvedTarget::ExplicitRoom); let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| event_tl_item.event_id().map(|event_id| { let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { @@ -1073,11 +1666,8 @@ impl RoomInputBar { timeline_kind: room_screen_props.timeline_kind.clone(), message, replied_to, - target_user_id: self.resolve_target_user_id( - None, - reply_target_user_id, - room_screen_props.bound_bot_user_id.clone(), - ), + target_user_id, + explicit_room, #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); @@ -1089,6 +1679,7 @@ impl RoomInputBar { self.clear_replying_to(cx); location_preview.clear(); location_preview.redraw(cx); + self.is_target_menu_visible = false; } } @@ -1106,14 +1697,21 @@ impl RoomInputBar { typing: false, }); self.enable_send_message_button(cx, false); + self.is_target_menu_visible = false; self.redraw(cx); return; } - let reply_target_user_id = self - .replying_to - .as_ref() - .map(|(event_tl_item, _emb)| event_tl_item.sender().to_owned()); + let resolved_target = self.current_resolved_target(room_screen_props); let message = mentionable_text_input.create_message_with_mentions(&entered_text); + let message_mentions_bot = message_mentions_known_bot( + &message, + room_screen_props.bound_bot_user_id.as_deref(), + room_screen_props.resolved_parent_bot_user_id.as_deref(), + &room_screen_props.room_bot_user_ids, + &room_screen_props.known_bot_user_ids, + ); + let (target_user_id, explicit_room) = + routing_directives_for_message(&resolved_target, message_mentions_bot); let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| event_tl_item.event_id().map(|event_id| { let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { @@ -1140,11 +1738,8 @@ impl RoomInputBar { timeline_kind: room_screen_props.timeline_kind.clone(), message, replied_to, - target_user_id: self.resolve_target_user_id( - None, - reply_target_user_id, - room_screen_props.bound_bot_user_id.clone(), - ), + target_user_id, + explicit_room, #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); @@ -1156,6 +1751,7 @@ impl RoomInputBar { self.clear_replying_to(cx); mentionable_text_input.set_text(cx, ""); self.enable_send_message_button(cx, false); + self.is_target_menu_visible = false; } } @@ -1247,6 +1843,8 @@ impl RoomInputBar { replying_preview.set_visible(cx, true); self.replying_to = Some(replying_to); + self.is_target_menu_visible = false; + self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); // 2. Hide other views that are irrelevant to a reply, e.g., // the `EditingPane` would improperly cover up the ReplyPreview. @@ -1267,6 +1865,8 @@ impl RoomInputBar { fn clear_replying_to(&mut self, cx: &mut Cx) { self.view(cx, ids!(replying_preview)).set_visible(cx, false); self.replying_to = None; + self.is_target_menu_visible = false; + self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); } /// Shows the editing pane to allow the user to edit the given event. @@ -1291,6 +1891,8 @@ impl RoomInputBar { self.view.location_preview(cx, ids!(location_preview)).clear(); let editing_pane = self.view.editing_pane(cx, ids!(editing_pane)); + self.is_target_menu_visible = false; + self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); match behavior { ShowEditingPaneBehavior::ShowNew { event_tl_item } => { editing_pane.show(cx, event_tl_item, timeline_kind); @@ -1593,7 +2195,7 @@ impl RoomInputBarRef { RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), - active_target_user_id: inner.active_target_user_id.clone(), + explicit_override: inner.explicit_override.clone(), editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), text_input_state: inner.child_by_path(ids!(input_bar.input_row.mentionable_text_input.text_input)).as_text_input().save_state(), } @@ -1609,11 +2211,12 @@ impl RoomInputBarRef { tombstone_info: Option<&SuccessorRoomDetails>, ) { let Some(mut inner) = self.borrow_mut() else { return }; + let explicit_override = restored_explicit_override(&saved_state); let RoomInputBarState { was_replying_preview_visible, text_input_state, replying_to, - active_target_user_id, + explicit_override: _, editing_pane_state, } = saved_state; @@ -1635,6 +2238,8 @@ impl RoomInputBarRef { inner.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); inner.is_emoji_picker_expanded = false; inner.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); + inner.is_target_menu_visible = false; + inner.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); inner.is_lang_popup_visible = false; inner.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); @@ -1645,7 +2250,7 @@ impl RoomInputBarRef { inner.clear_replying_to(cx); } inner.was_replying_preview_visible = was_replying_preview_visible; - inner.active_target_user_id = active_target_user_id; + inner.explicit_override = explicit_override; // 3. Restore the state of the editing pane. if let Some(editing_pane_state) = editing_pane_state { @@ -1774,8 +2379,8 @@ pub struct RoomInputBarState { text_input_state: TextInputState, /// The event that the user is currently replying to, if any. replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, - /// The most recently selected explicit bot target for this room. - active_target_user_id: Option, + /// The user's explicit target override for this room. + explicit_override: ExplicitOverride, /// The state of the `EditingPane`, if any message was being edited. editing_pane_state: Option, } @@ -1797,6 +2402,10 @@ enum ShowEditingPaneBehavior { mod tests { use super::*; + fn test_user_id(user_id: &str) -> OwnedUserId { + user_id.try_into().unwrap() + } + #[test] fn translation_popup_position_prefers_above_button() { let button_rect = Rect { @@ -1877,4 +2486,308 @@ mod tests { assert_eq!(outcome.next_last_source, "Hola mundo"); assert!(outcome.keep_preview_visible); } + + #[test] + fn test_reply_to_human_no_bot_targeting() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let reply_sender = test_user_id("@alice:127.0.0.1:8128"); + + assert_eq!( + resolve_target( + &ExplicitOverride::None, + Some(reply_sender.as_ref()), + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::RoomDefault(bound_bot_user_id), + ); + } + + #[test] + fn test_reply_bot_overrides_explicit_room() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + resolve_target( + &ExplicitOverride::Room, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::ReplyBot(bound_bot_user_id), + ); + } + + #[test] + fn test_chip_dismiss_returns_to_room_default() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + clear_explicit_override_result(Some(bound_bot_user_id.as_ref())), + ResolvedTarget::RoomDefault(bound_bot_user_id), + ); + } + + #[test] + fn test_chip_dismiss_explicit_room_to_room_default() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + let explicit_override = ExplicitOverride::Room; + let cleared_override = explicit_override.cleared(); + + assert_eq!(cleared_override, ExplicitOverride::None); + assert_eq!( + resolve_target( + &cleared_override, + None, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::RoomDefault(bound_bot_user_id), + ); + } + + #[test] + fn test_chip_dismiss_no_bound_bot() { + assert_eq!(clear_explicit_override_result(None), ResolvedTarget::NoTarget); + } + + #[test] + fn test_explicit_bot_with_reply_to_human() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let reply_sender = test_user_id("@alice:127.0.0.1:8128"); + + assert_eq!( + resolved_target_user_id(&resolve_send_target( + &ExplicitOverride::Bot(bot_user_id.clone()), + Some(reply_sender.as_ref()), + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + )), + Some(bot_user_id), + ); + } + + #[test] + fn test_cancel_reply_clears_reply_bot() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + resolve_restored_target( + &ExplicitOverride::Bot(bot_user_id.clone()), + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::ReplyBot(bot_user_id.clone()), + ); + assert_eq!( + resolve_restored_target( + &ExplicitOverride::Bot(bot_user_id.clone()), + None, + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::ExplicitBot(bot_user_id), + ); + } + + #[test] + fn test_explicit_override_persists_navigation() { + let saved_state = RoomInputBarState { + explicit_override: ExplicitOverride::Room, + ..Default::default() + }; + + assert_eq!( + restored_explicit_override(&saved_state), + ExplicitOverride::Room, + ); + } + + #[test] + fn test_reply_bot_restores_with_replying_to() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + resolve_restored_target( + &ExplicitOverride::None, + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + ), + ResolvedTarget::ReplyBot(bot_user_id), + ); + } + + #[test] + fn test_target_chip_room_default() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + format_target_chip_presentation( + AppLanguage::English, + &ResolvedTarget::RoomDefault(bot_user_id), + Some("BotFather"), + ), + TargetChipPresentation { + visible: true, + label: "Default: BotFather".to_string(), + subdued: true, + dismissible: false, + }, + ); + } + + #[test] + fn test_target_chip_hidden_no_bot() { + assert_eq!( + format_target_chip_presentation( + AppLanguage::English, + &ResolvedTarget::NoTarget, + None, + ), + TargetChipPresentation { + visible: false, + label: String::new(), + subdued: false, + dismissible: false, + }, + ); + } + + #[test] + fn test_explicit_bot_via_chip_menu() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let explicit_override = apply_target_menu_selection( + TargetMenuSelection::BoundBot, + Some(bot_user_id.as_ref()), + ); + + assert_eq!(explicit_override, ExplicitOverride::Bot(bot_user_id.clone())); + assert_eq!( + format_target_chip_presentation( + AppLanguage::English, + &resolve_target( + &explicit_override, + None, + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + ), + Some("BotFather"), + ), + TargetChipPresentation { + visible: true, + label: "To BotFather".to_string(), + subdued: false, + dismissible: true, + }, + ); + } + + #[test] + fn test_explicit_room_via_chip_menu() { + let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let explicit_override = apply_target_menu_selection( + TargetMenuSelection::Room, + Some(bot_user_id.as_ref()), + ); + + assert_eq!(explicit_override, ExplicitOverride::Room); + assert_eq!( + format_target_chip_presentation( + AppLanguage::English, + &resolve_target( + &explicit_override, + None, + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], + ), + Some("BotFather"), + ), + TargetChipPresentation { + visible: true, + label: "To room".to_string(), + subdued: false, + dismissible: true, + }, + ); + } + + #[test] + fn test_text_mentions_known_bot_matches_localpart() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let child_bot_user_id = test_user_id("@octosbot_alexbot:127.0.0.1:8128"); + + assert!(text_mentions_known_bot( + "@octosbot_alexbot 你是谁", + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + std::slice::from_ref(&child_bot_user_id), + std::slice::from_ref(&child_bot_user_id), + )); + } + + #[test] + fn test_message_mentions_room_member_bot_with_empty_known_bot_list() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let room_member_bot_user_id = test_user_id("@octosbot_alexbot:127.0.0.1:8128"); + let message = RoomMessageEventContent::text_plain("@octosbot_alexbot 你是谁"); + + assert!(message_mentions_known_bot( + &message, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + std::slice::from_ref(&room_member_bot_user_id), + &[], + )); + } + + #[test] + fn test_message_mentions_known_bot_prefers_structured_mentions() { + use ruma::events::Mentions; + + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let child_bot_user_id = test_user_id("@octosbot_alexbot:127.0.0.1:8128"); + let message = RoomMessageEventContent::text_plain("你好") + .add_mentions(Mentions::with_user_ids([child_bot_user_id.clone()])); + + assert!(message_mentions_known_bot( + &message, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + std::slice::from_ref(&child_bot_user_id), + std::slice::from_ref(&child_bot_user_id), + )); + } + + #[test] + fn test_message_bot_mention_suppresses_explicit_bot_target() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + + assert_eq!( + routing_directives_for_message( + &ResolvedTarget::ExplicitBot(bound_bot_user_id), + true, + ), + (None, false), + ); + } + + #[test] + fn test_message_bot_mention_keeps_explicit_room_marker() { + assert_eq!( + routing_directives_for_message(&ResolvedTarget::ExplicitRoom, true), + (None, true), + ); + } } diff --git a/src/shared/html_or_plaintext.rs b/src/shared/html_or_plaintext.rs index dee4772dc..70a6ef564 100644 --- a/src/shared/html_or_plaintext.rs +++ b/src/shared/html_or_plaintext.rs @@ -268,17 +268,34 @@ impl Widget for RobrixHtmlLink { impl RobrixHtmlLink { #[allow(unused)] fn draw_matrix_pill(&mut self, cx: &mut Cx, matrix_id: &MatrixId, via: &[OwnedServerName]) { - if let Some(mut pill) = self.matrix_link_pill(cx, ids!(matrix_link)).borrow_mut() { + { + let matrix_link_pill = self.matrix_link_pill(cx, ids!(matrix_link)); + let Some(mut pill) = matrix_link_pill.borrow_mut() else { + self.draw_html_link(cx); + return; + }; pill.populate_pill(cx, self.url.clone(), matrix_id, via); } - self.view(cx, ids!(matrix_link_view)).set_visible(cx, true); - self.view(cx, ids!(html_link_view)).set_visible(cx, false); + let matrix_link_view = self.view(cx, ids!(matrix_link_view)); + let html_link_view = self.view(cx, ids!(html_link_view)); + let visibility_changed = !matrix_link_view.visible() || html_link_view.visible(); + matrix_link_view.set_visible(cx, true); + html_link_view.set_visible(cx, false); + if visibility_changed { + self.redraw(cx); + } } /// Shows the inner plain HTML link and hides the Matrix link pill view. fn draw_html_link(&mut self, cx: &mut Cx) { - self.view(cx, ids!(html_link_view)).set_visible(cx, true); - self.view(cx, ids!(matrix_link_view)).set_visible(cx, false); + let html_link_view = self.view(cx, ids!(html_link_view)); + let matrix_link_view = self.view(cx, ids!(matrix_link_view)); + let visibility_changed = !html_link_view.visible() || matrix_link_view.visible(); + html_link_view.set_visible(cx, true); + matrix_link_view.set_visible(cx, false); + if visibility_changed { + self.redraw(cx); + } let mut html_link = self.html_link(cx, ids!(html_link)); html_link.set_url(&self.url); html_link.set_text(cx, self.text.as_ref()); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 456f5d586..bee2f19b4 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -927,6 +927,7 @@ pub enum MatrixRequest { message: RoomMessageEventContent, replied_to: Option, target_user_id: Option, + explicit_room: bool, #[cfg(feature = "tsp")] sign_with_tsp: bool, }, @@ -1049,6 +1050,34 @@ fn add_octos_target_user_id( content } +fn add_octos_explicit_room_marker( + mut content: serde_json::Value, + explicit_room: bool, +) -> serde_json::Value { + if explicit_room + && let Some(content_obj) = content.as_object_mut() + { + content_obj.insert( + "org.octos.explicit_room".to_string(), + serde_json::Value::Bool(true), + ); + } + content +} + +fn add_octos_routing_metadata( + content: serde_json::Value, + target_user_id: Option<&UserId>, + explicit_room: bool, +) -> serde_json::Value { + let content = add_octos_explicit_room_marker(content, explicit_room); + if let Some(target_user_id) = target_user_id { + add_octos_target_user_id(content, target_user_id) + } else { + content + } +} + async fn ensure_target_user_joined_room( room: &Room, target_user_id: &UserId, @@ -1103,6 +1132,68 @@ mod matrix_request_tests { Some("@bot_weather:example.com") ); } + + #[test] + fn test_send_message_explicit_room_sets_octos_explicit_room_marker() { + let content = serde_json::json!({ + "msgtype": "m.text", + "body": "hello room", + }); + + let content = add_octos_explicit_room_marker(content, true); + + assert_eq!( + content + .get("org.octos.explicit_room") + .and_then(|value| value.as_bool()), + Some(true) + ); + assert!( + content.get("org.octos.target_user_id").is_none(), + "ExplicitRoom should not also set a targeted bot MXID", + ); + } + + #[test] + fn test_send_reply_explicit_room_sets_octos_explicit_room_marker() { + let content = serde_json::json!({ + "msgtype": "m.text", + "body": "reply body", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$reply" + } + } + }); + + let content = add_octos_explicit_room_marker(content, true); + + assert_eq!( + content + .get("org.octos.explicit_room") + .and_then(|value| value.as_bool()), + Some(true) + ); + assert!( + content.get("org.octos.target_user_id").is_none(), + "ExplicitRoom replies should suppress room fallback without setting target_user_id", + ); + } + + #[test] + fn test_send_message_room_default_does_not_set_octos_explicit_room_marker() { + let content = serde_json::json!({ + "msgtype": "m.text", + "body": "hello bot", + }); + + let content = add_octos_explicit_room_marker(content, false); + + assert!( + content.get("org.octos.explicit_room").is_none(), + "RoomDefault should not suppress Octos room fallback", + ); + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -2605,6 +2696,7 @@ async fn matrix_worker_task( message, replied_to, target_user_id, + explicit_room, #[cfg(feature = "tsp")] sign_with_tsp, } => { @@ -2679,12 +2771,14 @@ async fn matrix_worker_task( } }; - if let Some(target_user_id) = target_user_id.as_ref() { - if let Err(_e) = ensure_target_user_joined_room( - timeline.room(), - target_user_id.as_ref(), - ) - .await + if target_user_id.is_some() || explicit_room { + let target_user_id = target_user_id.as_ref(); + if let Some(target_user_id) = target_user_id + && let Err(_e) = ensure_target_user_joined_room( + timeline.room(), + target_user_id.as_ref(), + ) + .await { error!("Failed to ensure targeted bot {target_user_id} joined {timeline_kind}: {_e:?}"); enqueue_popup_notification( @@ -2696,7 +2790,11 @@ async fn matrix_worker_task( } let raw_content = match serde_json::to_value(&reply_content) { - Ok(content) => add_octos_target_user_id(content, target_user_id.as_ref()), + Ok(content) => add_octos_routing_metadata( + content, + target_user_id.map(|user_id| user_id.as_ref()), + explicit_room, + ), Err(_e) => { error!("Failed to serialize reply content for {timeline_kind}: {_e:?}"); enqueue_popup_notification( @@ -2708,9 +2806,15 @@ async fn matrix_worker_task( } }; match timeline.room().send_raw("m.room.message", raw_content).await { - Ok(_response) => log!("Sent targeted reply message to {timeline_kind}."), + Ok(_response) => { + if target_user_id.is_some() { + log!("Sent targeted reply message to {timeline_kind}."); + } else { + log!("Sent explicit-room reply message to {timeline_kind}."); + } + } Err(_e) => { - error!("Failed to send targeted reply message to {timeline_kind}: {_e:?}"); + error!("Failed to send reply message to {timeline_kind}: {_e:?}"); enqueue_popup_notification(format!("Failed to send reply: {_e}"), PopupKind::Error, None); } } @@ -2723,12 +2827,14 @@ async fn matrix_worker_task( } } } - } else if let Some(target_user_id) = target_user_id.as_ref() { - if let Err(_e) = ensure_target_user_joined_room( - timeline.room(), - target_user_id.as_ref(), - ) - .await + } else if target_user_id.is_some() || explicit_room { + let target_user_id = target_user_id.as_ref(); + if let Some(target_user_id) = target_user_id + && let Err(_e) = ensure_target_user_joined_room( + timeline.room(), + target_user_id.as_ref(), + ) + .await { error!("Failed to ensure targeted bot {target_user_id} joined {timeline_kind}: {_e:?}"); enqueue_popup_notification( @@ -2740,7 +2846,11 @@ async fn matrix_worker_task( } let raw_content = match serde_json::to_value(&message) { - Ok(content) => add_octos_target_user_id(content, target_user_id.as_ref()), + Ok(content) => add_octos_routing_metadata( + content, + target_user_id.map(|user_id| user_id.as_ref()), + explicit_room, + ), Err(_e) => { error!("Failed to serialize message content for {timeline_kind}: {_e:?}"); enqueue_popup_notification( @@ -2752,9 +2862,15 @@ async fn matrix_worker_task( } }; match timeline.room().send_raw("m.room.message", raw_content).await { - Ok(_response) => log!("Sent targeted message to {timeline_kind}."), + Ok(_response) => { + if target_user_id.is_some() { + log!("Sent targeted message to {timeline_kind}."); + } else { + log!("Sent explicit-room message to {timeline_kind}."); + } + } Err(_e) => { - error!("Failed to send targeted message to {timeline_kind}: {_e:?}"); + error!("Failed to send message to {timeline_kind}: {_e:?}"); enqueue_popup_notification(format!("Failed to send message: {_e}"), PopupKind::Error, None); } } From 10302d8340d8d81f061eb78c72bb5bed1fd037fe Mon Sep 17 00:00:00 2001 From: Alvin <48358093+TigerInYourDream@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:44:27 +0800 Subject: [PATCH 153/283] ui: align language hint text and normalize proxy toggle style (#88) Align the language hint label with the section title by removing its left margin, and replace the 18-line custom proxy toggle styling with the standard 4-line App Service toggle pattern for visual consistency. --- src/settings/settings_screen.rs | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 91dd4b3cf..63827f775 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -226,7 +226,7 @@ script_mod! { preferences_language_hint_label := Label { width: Fill height: Fit - margin: Inset{left: (SPACE_XS), right: (SPACE_SM), top: (SPACE_XS), bottom: (SPACE_XS)} + margin: Inset{right: (SPACE_SM), top: (SPACE_XS), bottom: (SPACE_XS)} draw_text +: { color: (MESSAGE_TEXT_COLOR) text_style: REGULAR_TEXT { font_size: 10.5 } @@ -268,28 +268,16 @@ script_mod! { } preferences_proxy_use_toggle := Toggle { - width: 52, height: 28 + width: Fit + height: Fit + padding: Inset{top: (SPACE_SM), right: (SPACE_SM), bottom: (SPACE_SM), left: (SPACE_SM)} text: "" active: false - icon_walk: Walk{width: 0, height: 0, margin: 0} - label_walk: Walk{width: 0, height: 0, margin: 0} draw_bg +: { - size: 18.0 - color: #E3E7EF - color_hover: #E3E7EF - color_down: #D5DBE6 + size: 20.0 color_active: (COLOR_ACTIVE_PRIMARY) - border_radius: 14.0 - border_size: 1.5 - border_color: #7E879A - border_color_hover: #7E879A - border_color_down: #6F788D - border_color_active: (COLOR_ACTIVE_PRIMARY_DARKER) - mark_color: #2D3A57 - mark_color_hover: #2D3A57 - mark_color_down: #2D3A57 - mark_color_active: #FFFFFF - mark_color_active_hover: #FFFFFF + border_color_active: (COLOR_ACTIVE_PRIMARY) + mark_color_active: #fff } } } From bbaa4ef013fa1764e0607a892883d5716ab8f4e8 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sun, 12 Apr 2026 04:47:48 +0800 Subject: [PATCH 154/283] Switch Matrix bot UX to mention-first routing --- ...6-04-12-tg-bot-mention-reply-first-plan.md | 136 +++++ specs/task-tg-bot-mention-reply-first.spec.md | 111 ++++ src/home/room_screen.rs | 3 - src/room/room_input_bar.rs | 570 +++++------------- 4 files changed, 388 insertions(+), 432 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md create mode 100644 specs/task-tg-bot-mention-reply-first.spec.md diff --git a/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md b/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md new file mode 100644 index 000000000..00c6b33fb --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md @@ -0,0 +1,136 @@ +# Telegram Bot Mention/Reply-First Targeting Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove the always-visible target chip/popup and make bot-bound rooms default to room-first message routing, with bot interaction driven by `@mention` and reply-to-bot. + +**Architecture:** Keep the existing mention routing, reply-to-bot targeting, and `explicit_room` marker pipeline. Change the input bar’s default resolved target from `RoomDefault` to `ExplicitRoom` in bot-bound rooms, clear any persisted explicit target overrides from older builds, and hide the target chip/popup UI entirely so the main interaction path is text-first rather than mode-switch-first. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `src/room/room_input_bar.rs` + - Change default target resolution to room-first in bot-bound rooms. + - Remove visible target-chip behavior from the input bar. + - Stop restoring persisted explicit overrides from previous builds. + - Update unit tests from chip/menu semantics to mention/reply-first semantics. + +- `src/home/room_screen.rs` + - Remove or neutralize the now-unused target popup trigger path if needed for compile cleanliness. + +- `specs/task-tg-bot-mention-reply-first.spec.md` + - Verification contract for the new direction. + +--- + +### Task 1: Lock the New Routing Semantics with Failing Tests + +**Files:** +- Modify: `src/room/room_input_bar.rs` +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing tests for the new default behavior** + +Add tests for: +- `test_bot_bound_room_defaults_to_explicit_room` +- `test_reply_to_human_in_bot_bound_room_stays_explicit_room` +- `test_reply_to_bot_still_targets_bot` +- `test_persisted_explicit_override_is_ignored_on_restore` + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_bound_room_defaults_to_explicit_room +cargo test test_reply_to_human_in_bot_bound_room_stays_explicit_room +cargo test test_reply_to_bot_still_targets_bot +cargo test test_persisted_explicit_override_is_ignored_on_restore +``` + +- [ ] **Step 3: Update the target resolution helpers** + +Implement the minimal logic so that: +- `ExplicitOverride::None + bound_bot_user_id` resolves to `ExplicitRoom` +- `reply-to-bot` still resolves to `ReplyBot` +- persisted explicit overrides restore as `None` + +- [ ] **Step 4: Re-run the routing tests** + +Run the same four tests and confirm they pass. + +--- + +### Task 2: Remove the Visible Target UI from the Input Bar + +**Files:** +- Modify: `src/room/room_input_bar.rs` +- Modify: `src/home/room_screen.rs` + +- [ ] **Step 1: Add a failing UI-state test** + +Add: +- `test_target_chip_hidden_in_bot_bound_room` + +- [ ] **Step 2: Run the test and confirm it fails** + +Run: + +```bash +cargo test test_target_chip_hidden_in_bot_bound_room +``` + +- [ ] **Step 3: Hide the target indicator and stop exposing the popup** + +Make the minimal code changes so: +- `sync_target_indicator()` always hides the target chip +- the target chip no longer opens a popup in normal use +- any stale popup path in `RoomScreen` is neutralized or left dormant but unreachable + +- [ ] **Step 4: Re-run the UI-state test** + +Run: + +```bash +cargo test test_target_chip_hidden_in_bot_bound_room +``` + +--- + +### Task 3: Verify Mention/Reply Routing Still Works End-to-End + +**Files:** +- Modify: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Keep and adapt the mention/reply regression tests** + +Ensure these tests still cover the final behavior: +- `test_message_bot_mention_keeps_explicit_room_marker` +- `test_text_mentions_known_bot_matches_localpart` +- `test_message_mentions_room_member_bot_with_empty_known_bot_list` +- `test_message_mentions_known_bot_prefers_structured_mentions` + +- [ ] **Step 2: Run the focused regression suite** + +Run: + +```bash +cargo test test_message_bot_mention_keeps_explicit_room_marker +cargo test test_text_mentions_known_bot_matches_localpart +cargo test test_message_mentions_room_member_bot_with_empty_known_bot_list +cargo test test_message_mentions_known_bot_prefers_structured_mentions +``` + +- [ ] **Step 3: Run the final verification gates** + +Run: + +```bash +cargo build +agent-spec parse specs/task-tg-bot-mention-reply-first.spec.md +agent-spec lint specs/task-tg-bot-mention-reply-first.spec.md --min-score 0.7 +``` + diff --git a/specs/task-tg-bot-mention-reply-first.spec.md b/specs/task-tg-bot-mention-reply-first.spec.md new file mode 100644 index 000000000..8052b8e0d --- /dev/null +++ b/specs/task-tg-bot-mention-reply-first.spec.md @@ -0,0 +1,111 @@ +spec: task +name: "Telegram Bot UI Alignment — Phase 3: Mention/Reply-First Targeting" +inherits: project +tags: [bot, ui, telegram-parity, mention, reply] +depends: [task-tg-bot-explicit-targeting, task-tg-bot-explicit-room-no-fallback] +estimate: 1d +--- + +## Intent + +将 Robrix2 的群聊 bot 交互进一步收敛到 Telegram 风格:默认普通消息发给房间,bot 交互优先通过 `@bot` mention 和 reply-to-bot 完成,而不是依赖常驻 target chip 或手工切换菜单。当前 Phase 2/3 方向把“显式 target”暴露成了主 UI,导致多人房间里心智偏重、界面噪音大,而且 popup 复杂度明显高于它带来的价值。 + +本任务要求移除输入框上的常驻 target chip / target popup 作为主交互入口,并把 bot-bound 房间的默认输入行为改成 room-first:普通消息默认携带 `explicit_room` 语义来抑制 Octos fallback,只有 reply-to-bot 才继续走显式 target。 + +## Decisions + +- 默认输入模型: `RoomInputBar` 不再暴露手工 target 切换 UI;普通输入默认是 room-first +- 默认解析: 当 `ExplicitOverride::None` 且存在 `bound_bot_user_id` 时,输入栏运行时目标解析为 `ResolvedTarget::ExplicitRoom`,不再回落到 `RoomDefault` +- 普通房间保持不变: 当 `ExplicitOverride::None` 且不存在 `bound_bot_user_id` 时,解析结果仍为 `ResolvedTarget::NoTarget` +- Reply-to-bot 解析: 当默认结果原本会是 room-first 时,reply-to-bot 仍解析为 `ReplyBot(bot_user_id)` +- Reply-to-human 保持 room-first: reply-to-human 不得触发 bot target +- Mention 规则保持: 文本或结构化 `@mention` 命中 bot 时,不得继续附带 `target_user_id`;如果当前消息同时具备 room-first 语义,仍可保留 `explicit_room` +- 迁移策略: 旧版本持久化下来的 `ExplicitOverride::Bot` / `::Room` 在 restore 时一律丢弃,避免隐藏 target 状态在 UI 被移除后继续生效 +- UI 策略: `target_indicator` 和 target popup 不再作为可见主 UI;reply 状态仍由现有 `ReplyingPreview` 承担 + +## Boundaries + +### Allowed Changes +- src/room/room_input_bar.rs +- src/home/room_screen.rs +- specs/task-tg-bot-mention-reply-first.spec.md +- docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md + +### Forbidden +- 不要修改 Octos 后端 +- 不要重新设计 `ReplyingPreview` +- 不要实现新的 bot menu button +- 不要实现 `/command@bot` +- 不要新增 cargo 依赖 + +## Out of Scope + +- Telegram 风格 bot command menu +- BotFather 面板改版 +- 显示名式 `@octos` mention 解析 +- 单 bot 私聊的特殊自动路由策略 + +## Completion Criteria + +Scenario: Bot-bound room defaults to room-first routing + Test: test_bot_bound_room_defaults_to_explicit_room + Given a room with `bound_bot_user_id = "@octosbot:127.0.0.1:8128"` + And the user has no `ExplicitOverride` + When the input bar resolves the current target without replying + Then the `ResolvedTarget` is `ExplicitRoom` + And a plain message sent from this state has `target_user_id = None` + And a plain message sent from this state has `explicit_room = true` + +Scenario: Replying to a human in a bot-bound room stays room-first + Test: test_reply_to_human_in_bot_bound_room_stays_explicit_room + Given a room with `bound_bot_user_id = "@octosbot:127.0.0.1:8128"` + And no `ExplicitOverride` + And the user is replying to a message from "@alice:127.0.0.1:8128" + When the input bar resolves the current target + Then the `ResolvedTarget` is `ExplicitRoom` + And the outgoing message does not set `target_user_id` + And the outgoing message keeps `explicit_room = true` + +Scenario: Replying to a bot still targets that bot + Test: test_reply_to_bot_still_targets_bot + Given a room with `bound_bot_user_id = "@octosbot:127.0.0.1:8128"` + And the user is replying to a message from "@octosbot:127.0.0.1:8128" + When the input bar resolves the current target + Then the `ResolvedTarget` is `ReplyBot("@octosbot:127.0.0.1:8128")` + And the outgoing message sets `target_user_id = "@octosbot:127.0.0.1:8128"` + And the outgoing message does not set `explicit_room` + +Scenario: Reply-to-bot overrides the room-first default + Test: test_reply_to_bot_overrides_room_first_default + Given a room with `bound_bot_user_id = "@octosbot:127.0.0.1:8128"` + And without replying the input bar would resolve to `ExplicitRoom` + When the user replies to a message from "@octosbot:127.0.0.1:8128" + Then the resolved target is `ReplyBot("@octosbot:127.0.0.1:8128")` + And the resolved target is not `ExplicitRoom` + +Scenario: Mentioning a bot in a bot-bound room does not attach target_user_id + Test: test_message_bot_mention_keeps_explicit_room_marker + Given a bot-bound room whose default resolved target is `ExplicitRoom` + When the user sends the message "@octosbot_alexbot 你好" + Then the outgoing message does not set `target_user_id` + And the outgoing message keeps `explicit_room = true` + And the message body still contains "@octosbot_alexbot" + +Scenario: Target chip is hidden in bot-bound room + Test: test_target_chip_hidden_in_bot_bound_room + Given a room with `bound_bot_user_id = "@octosbot:127.0.0.1:8128"` + When the room input bar syncs its target UI + Then no target chip is shown + +Scenario: Persisted explicit override is ignored on restore + Test: test_persisted_explicit_override_is_ignored_on_restore + Given a saved `RoomInputBarState` containing `ExplicitOverride::Bot("@octosbot:127.0.0.1:8128")` + When the room input bar restores state + Then the restored explicit override is `None` + +Scenario: Invalid stale explicit bot selection is ignored after removing the target menu + Test: test_stale_explicit_bot_selection_is_ignored_without_available_bots + Given the current explicit override is `None` + And there are no available bot targets in the current room + When invalid stale code tries to apply `TargetMenuSelection::Bot("@octosbot:127.0.0.1:8128")` + Then the explicit override remains `None` diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index a5e8d98ca..83f97e472 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2042,7 +2042,6 @@ script_mod! { } } - mod.widgets.RoomScreen = #(RoomScreen::register_widget(vm)) { width: Fill, height: Fill, cursor: MouseCursor.Default, @@ -8665,8 +8664,6 @@ pub enum ReportRoomResultAction { }, } - -/// Actions related to a specific message within a room timeline. #[derive(Clone, Default, Debug)] pub enum MessageAction { /// The user clicked the "react" button on a message diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 8abdac22b..89503833f 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -83,6 +83,7 @@ fn compute_translation_apply_outcome(translated_text: &str) -> TranslationApplyO } } +#[allow(dead_code)] #[derive(Clone, Debug, Default, PartialEq, Eq)] enum ExplicitOverride { #[default] @@ -92,6 +93,7 @@ enum ExplicitOverride { } impl ExplicitOverride { + #[allow(dead_code)] fn cleared(&self) -> Self { Self::None } @@ -100,12 +102,12 @@ impl ExplicitOverride { #[derive(Clone, Debug, PartialEq, Eq)] enum ResolvedTarget { NoTarget, - RoomDefault(OwnedUserId), ExplicitBot(OwnedUserId), ExplicitRoom, ReplyBot(OwnedUserId), } +#[cfg(test)] #[derive(Clone, Debug, PartialEq, Eq)] struct TargetChipPresentation { visible: bool, @@ -114,10 +116,32 @@ struct TargetChipPresentation { dismissible: bool, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum TargetMenuSelection { +#[cfg(test)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TargetMenuSelection { Room, - BoundBot, + Bot(OwnedUserId), +} + +#[cfg(test)] +fn apply_multi_bot_target_menu_selection( + selection: TargetMenuSelection, + available_bot_user_ids: &[OwnedUserId], + explicit_override: &ExplicitOverride, +) -> ExplicitOverride { + match selection { + TargetMenuSelection::Room => ExplicitOverride::Room, + TargetMenuSelection::Bot(bot_user_id) => { + if available_bot_user_ids + .iter() + .any(|available_bot_user_id| available_bot_user_id == &bot_user_id) + { + ExplicitOverride::Bot(bot_user_id) + } else { + explicit_override.clone() + } + } + } } fn known_bot_candidates<'a>( @@ -268,12 +292,12 @@ fn routing_directives_for_message( resolved_target: &ResolvedTarget, message_mentions_bot: bool, ) -> (Option, bool) { - let explicit_room = matches!(resolved_target, ResolvedTarget::ExplicitRoom); let target_user_id = if message_mentions_bot { None } else { resolved_target_user_id(resolved_target) }; + let explicit_room = target_user_id.is_none(); (target_user_id, explicit_room) } @@ -297,99 +321,35 @@ fn resolve_target( match explicit_override { ExplicitOverride::Bot(bot_user_id) => ResolvedTarget::ExplicitBot(bot_user_id.clone()), ExplicitOverride::Room => ResolvedTarget::ExplicitRoom, - ExplicitOverride::None => bound_bot_user_id - .map(|bot_user_id| ResolvedTarget::RoomDefault(bot_user_id.to_owned())) - .unwrap_or(ResolvedTarget::NoTarget), + ExplicitOverride::None => { + if bound_bot_user_id.is_some() { + ResolvedTarget::ExplicitRoom + } else { + ResolvedTarget::NoTarget + } + } } } fn resolved_target_user_id(target: &ResolvedTarget) -> Option { match target { ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => None, - ResolvedTarget::RoomDefault(bot_user_id) - | ResolvedTarget::ExplicitBot(bot_user_id) + ResolvedTarget::ExplicitBot(bot_user_id) | ResolvedTarget::ReplyBot(bot_user_id) => Some(bot_user_id.clone()), } } #[cfg(test)] -fn clear_explicit_override_result(bound_bot_user_id: Option<&UserId>) -> ResolvedTarget { - bound_bot_user_id - .map(|bot_user_id| ResolvedTarget::RoomDefault(bot_user_id.to_owned())) - .unwrap_or(ResolvedTarget::NoTarget) -} - fn format_target_chip_presentation( - app_language: AppLanguage, - resolved_target: &ResolvedTarget, - bot_display_name: Option<&str>, + _app_language: AppLanguage, + _resolved_target: &ResolvedTarget, + _bot_display_name: Option<&str>, ) -> TargetChipPresentation { - let target_label = match resolved_target { - ResolvedTarget::RoomDefault(bot_user_id) - | ResolvedTarget::ExplicitBot(bot_user_id) - | ResolvedTarget::ReplyBot(bot_user_id) => bot_display_name - .map(str::trim) - .filter(|display_name| !display_name.is_empty()) - .unwrap_or_else(|| bot_user_id.localpart()) - .to_string(), - ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => String::new(), - }; - - match resolved_target { - ResolvedTarget::NoTarget => TargetChipPresentation { - visible: false, - label: String::new(), - subdued: false, - dismissible: false, - }, - ResolvedTarget::RoomDefault(_) => TargetChipPresentation { - visible: true, - label: tr_fmt( - app_language, - "room_input_bar.target.default", - &[("display_name", &target_label)], - ), - subdued: true, - dismissible: false, - }, - ResolvedTarget::ExplicitBot(_) => TargetChipPresentation { - visible: true, - label: tr_fmt( - app_language, - "room_input_bar.target.to_bot", - &[("display_name", &target_label)], - ), - subdued: false, - dismissible: true, - }, - ResolvedTarget::ExplicitRoom => TargetChipPresentation { - visible: true, - label: tr_key(app_language, "room_input_bar.target.to_room").to_string(), - subdued: false, - dismissible: true, - }, - ResolvedTarget::ReplyBot(_) => TargetChipPresentation { - visible: true, - label: tr_fmt( - app_language, - "room_input_bar.target.reply_bot", - &[("display_name", &target_label)], - ), - subdued: false, - dismissible: false, - }, - } -} - -fn apply_target_menu_selection( - selection: TargetMenuSelection, - bound_bot_user_id: Option<&UserId>, -) -> ExplicitOverride { - match selection { - TargetMenuSelection::Room => ExplicitOverride::Room, - TargetMenuSelection::BoundBot => bound_bot_user_id - .map(|bot_user_id| ExplicitOverride::Bot(bot_user_id.to_owned())) - .unwrap_or_default(), + TargetChipPresentation { + visible: false, + label: String::new(), + subdued: false, + dismissible: false, } } @@ -426,8 +386,10 @@ fn resolve_restored_target( ) } +#[cfg(test)] fn restored_explicit_override(saved_state: &RoomInputBarState) -> ExplicitOverride { - saved_state.explicit_override.clone() + let _ = saved_state; + ExplicitOverride::None } script_mod! { @@ -520,14 +482,14 @@ script_mod! { mod.widgets.TargetChipButton = Button { width: Fit, height: Fit - padding: Inset{left: 12, right: 12, top: 6, bottom: 6} + padding: Inset{left: 10, right: 10, top: 5, bottom: 5} draw_bg +: { - color: #xF0F4FA - color_hover: #xE0E8F0 - color_down: #xD0D8E8 - border_radius: 13.0 + color: #xF7F9FD + color_hover: #xEEF3F9 + color_down: #xE5ECF5 + border_radius: 9.0 border_size: 1.0 - border_color: (COLOR_SECONDARY) + border_color: #xD7DFEA } draw_text +: { color: (COLOR_TEXT) @@ -588,14 +550,13 @@ script_mod! { visible: false width: Fill, height: Fit flow: Down - padding: Inset{left: 6, right: 6, top: 6, bottom: 2} - spacing: 4 + padding: Inset{left: 6, right: 6, top: 6, bottom: 3} target_chip_row := View { width: Fit, height: Fit flow: Right align: Align{y: 0.5} - spacing: 6 + spacing: 4 target_chip_button := mod.widgets.TargetChipButton { text: "Target" } @@ -604,32 +565,20 @@ script_mod! { width: Fit, height: Fit spacing: 0 text: "" - padding: Inset{left: 6, right: 6, top: 6, bottom: 6} + padding: Inset{left: 5, right: 5, top: 5, bottom: 5} + draw_bg +: { + color: #xF7F9FD + color_hover: #xEEF3F9 + color_down: #xE5ECF5 + border_radius: 9.0 + border_size: 1.0 + border_color: #xD7DFEA + } draw_icon +: { svg: (ICON_CLOSE) + color: (COLOR_MESSAGE_NOTICE_TEXT) } - icon_walk: Walk{width: 10, height: 10} - } - } - - target_menu_popup := RoundedView { - visible: false - width: Fit, height: Fit - flow: Down - padding: 4 - spacing: 4 - show_bg: true - draw_bg +: { - color: #xF7F9FD - border_radius: 8.0 - border_size: 1.0 - border_color: (COLOR_SECONDARY) - } - - target_menu_room_button := mod.widgets.TargetMenuButton { text: "To room" } - target_menu_bound_bot_button := mod.widgets.TargetMenuButton { - visible: false - text: "Bot" + icon_walk: Walk{width: 9, height: 9} } } } @@ -1060,8 +1009,6 @@ pub struct RoomInputBar { #[rust] translation_preview_text: Option, /// Whether a translation HTTP request is currently in flight. #[rust] translation_request_pending: bool, - /// Whether the target selection menu is currently expanded. - #[rust] is_target_menu_visible: bool, /// Debounce timer for translation requests. #[rust] translation_debounce_timer: Timer, /// The last source text that was sent for translation. @@ -1314,118 +1261,11 @@ impl RoomInputBar { .map(|(event_tl_item, _embedded_event)| event_tl_item.sender()) } - fn target_display_name_for_user( - &self, - room_screen_props: &RoomScreenProps, - user_id: &UserId, - ) -> Option { - room_screen_props - .room_members - .as_ref() - .and_then(|members| { - members - .iter() - .find(|member| member.user_id() == user_id) - .and_then(|member| { - member - .display_name() - .map(str::trim) - .filter(|display_name| !display_name.is_empty()) - .map(ToOwned::to_owned) - }) - }) - } - - fn resolved_target_display_name( - &self, - room_screen_props: &RoomScreenProps, - resolved_target: &ResolvedTarget, - ) -> Option { - match resolved_target { - ResolvedTarget::NoTarget | ResolvedTarget::ExplicitRoom => None, - ResolvedTarget::RoomDefault(user_id) - | ResolvedTarget::ExplicitBot(user_id) - | ResolvedTarget::ReplyBot(user_id) => { - self.target_display_name_for_user(room_screen_props, user_id.as_ref()) - } - } - } - fn sync_target_indicator(&mut self, cx: &mut Cx, room_screen_props: &RoomScreenProps) { - let resolved_target = self.current_resolved_target(room_screen_props); - let bot_display_name = self.resolved_target_display_name(room_screen_props, &resolved_target); - let presentation = format_target_chip_presentation( - self.app_language, - &resolved_target, - bot_display_name.as_deref(), - ); - self.view .view(cx, ids!(target_indicator)) - .set_visible(cx, presentation.visible); - self.view - .view(cx, ids!(target_menu_popup)) - .set_visible(cx, presentation.visible && self.is_target_menu_visible); - - if !presentation.visible { - self.is_target_menu_visible = false; - return; - } - - let mut target_chip_button = self.button(cx, ids!(target_chip_button)); - target_chip_button.set_text(cx, &presentation.label); - let (text_color, border_color, bg_color, hover_color, down_color): (Vec4, Vec4, Vec4, Vec4, Vec4) = if presentation.subdued { - ( - COLOR_MESSAGE_NOTICE_TEXT, - COLOR_BG_DISABLED, - vec4(0.89, 0.89, 0.89, 1.0), - vec4(0.84, 0.84, 0.84, 1.0), - vec4(0.78, 0.78, 0.78, 1.0), - ) - } else { - ( - COLOR_ACTIVE_PRIMARY_DARKER, - COLOR_BG_DISABLED, - COLOR_BG_PREVIEW, - COLOR_BG_PREVIEW_HOVER, - vec4(0.77, 0.86, 0.96, 1.0), - ) - }; - script_apply_eval!(cx, target_chip_button, { - draw_bg +: { - border_color: #(border_color), - color: #(bg_color), - color_hover: #(hover_color), - color_down: #(down_color), - } - draw_text +: { - color: #(text_color), - color_hover: #(text_color), - color_down: #(text_color), - } - }); - - self.button(cx, ids!(target_chip_dismiss_button)) - .set_visible(cx, presentation.dismissible); - self.button(cx, ids!(target_menu_room_button)) - .set_text(cx, tr_key(self.app_language, "room_input_bar.target.menu.room")); - - let bound_bot_option = room_screen_props.bound_bot_user_id.as_deref().map(|bound_bot_user_id| { - self.target_display_name_for_user(room_screen_props, bound_bot_user_id) - .unwrap_or_else(|| bound_bot_user_id.localpart().to_string()) - }); - self.button(cx, ids!(target_menu_bound_bot_button)) - .set_visible(cx, bound_bot_option.is_some()); - if let Some(bound_bot_option) = bound_bot_option { - self.button(cx, ids!(target_menu_bound_bot_button)).set_text( - cx, - &tr_fmt( - self.app_language, - "room_input_bar.target.menu.bound_bot", - &[("display_name", &bound_bot_option)], - ), - ); - } + .set_visible(cx, false); + let _ = room_screen_props; } fn handle_actions( @@ -1437,42 +1277,12 @@ impl RoomInputBar { let mentionable_text_input = self.mentionable_text_input(cx, ids!(mentionable_text_input)); let text_input = mentionable_text_input.text_input_ref(); - if self.button(cx, ids!(target_chip_button)).clicked(actions) { - self.is_target_menu_visible = !self.is_target_menu_visible; - self.redraw(cx); - } - - if self.button(cx, ids!(target_chip_dismiss_button)).clicked(actions) { - self.explicit_override = self.explicit_override.cleared(); - self.is_target_menu_visible = false; - self.redraw(cx); - } - - if self.button(cx, ids!(target_menu_room_button)).clicked(actions) { - self.explicit_override = apply_target_menu_selection( - TargetMenuSelection::Room, - room_screen_props.bound_bot_user_id.as_deref(), - ); - self.is_target_menu_visible = false; - self.redraw(cx); - } - - if self.button(cx, ids!(target_menu_bound_bot_button)).clicked(actions) { - self.explicit_override = apply_target_menu_selection( - TargetMenuSelection::BoundBot, - room_screen_props.bound_bot_user_id.as_deref(), - ); - self.is_target_menu_visible = false; - self.redraw(cx); - } - // Clear the replying-to preview pane if the "cancel reply" button was clicked // or if the `Escape` key was pressed within the message input box. if self.button(cx, ids!(cancel_reply_button)).clicked(actions) || text_input.escaped(actions) { self.clear_replying_to(cx); - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1480,7 +1290,6 @@ impl RoomInputBar { if self.button(cx, ids!(more_actions_button)).clicked(actions) { self.is_location_card_expanded = !self.is_location_card_expanded; self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, self.is_location_card_expanded); - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1488,14 +1297,12 @@ impl RoomInputBar { if self.button(cx, ids!(emoji_picker_button)).clicked(actions) { self.is_emoji_picker_expanded = !self.is_emoji_picker_expanded; self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, self.is_emoji_picker_expanded); - self.is_target_menu_visible = false; 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.is_target_menu_visible = false; self.open_file_picker(cx); } @@ -1531,7 +1338,6 @@ impl RoomInputBar { self.is_emoji_picker_expanded = false; self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1546,12 +1352,10 @@ impl RoomInputBar { self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; - self.is_target_menu_visible = false; self.redraw(cx); } else { self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; - self.is_target_menu_visible = false; let button_rect = self.button(cx, ids!(translate_button)).area().clipped_rect(cx); if button_rect.size.x > 0.0 { cx.widget_action( @@ -1573,7 +1377,6 @@ impl RoomInputBar { self.view.label(cx, ids!(translation_preview_text)).set_text(cx, &outcome.preserved_preview_text); self.view.view(cx, ids!(translation_preview)).set_visible(cx, outcome.keep_preview_visible); self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); - self.is_target_menu_visible = false; self.redraw(cx); } } @@ -1587,7 +1390,6 @@ impl RoomInputBar { self.view.view(cx, ids!(translation_preview)).set_visible(cx, false); self.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); self.is_lang_popup_visible = false; - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1596,7 +1398,6 @@ impl RoomInputBar { log!("Location card clicked; requesting current location..."); self.is_location_card_expanded = false; self.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); - self.is_target_menu_visible = false; if let Err(_e) = init_location_subscriber(cx) { error!("Failed to initialize location subscriber"); enqueue_popup_notification( @@ -1614,7 +1415,6 @@ impl RoomInputBar { room_screen_props.room_screen_widget_uid, MessageAction::ShowThreadsPane, ); - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1623,7 +1423,6 @@ impl RoomInputBar { room_screen_props.room_screen_widget_uid, MessageAction::ShowRoomInfoPane, ); - self.is_target_menu_visible = false; self.redraw(cx); } @@ -1639,7 +1438,7 @@ impl RoomInputBar { ); let resolved_target = self.current_resolved_target(room_screen_props); let target_user_id = resolved_target_user_id(&resolved_target); - let explicit_room = matches!(resolved_target, ResolvedTarget::ExplicitRoom); + let explicit_room = target_user_id.is_none(); let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| event_tl_item.event_id().map(|event_id| { let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { @@ -1679,7 +1478,6 @@ impl RoomInputBar { self.clear_replying_to(cx); location_preview.clear(); location_preview.redraw(cx); - self.is_target_menu_visible = false; } } @@ -1697,7 +1495,6 @@ impl RoomInputBar { typing: false, }); self.enable_send_message_button(cx, false); - self.is_target_menu_visible = false; self.redraw(cx); return; } @@ -1751,7 +1548,6 @@ impl RoomInputBar { self.clear_replying_to(cx); mentionable_text_input.set_text(cx, ""); self.enable_send_message_button(cx, false); - self.is_target_menu_visible = false; } } @@ -1843,8 +1639,6 @@ impl RoomInputBar { replying_preview.set_visible(cx, true); self.replying_to = Some(replying_to); - self.is_target_menu_visible = false; - self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); // 2. Hide other views that are irrelevant to a reply, e.g., // the `EditingPane` would improperly cover up the ReplyPreview. @@ -1865,8 +1659,6 @@ impl RoomInputBar { fn clear_replying_to(&mut self, cx: &mut Cx) { self.view(cx, ids!(replying_preview)).set_visible(cx, false); self.replying_to = None; - self.is_target_menu_visible = false; - self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); } /// Shows the editing pane to allow the user to edit the given event. @@ -1891,8 +1683,6 @@ impl RoomInputBar { self.view.location_preview(cx, ids!(location_preview)).clear(); let editing_pane = self.view.editing_pane(cx, ids!(editing_pane)); - self.is_target_menu_visible = false; - self.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); match behavior { ShowEditingPaneBehavior::ShowNew { event_tl_item } => { editing_pane.show(cx, event_tl_item, timeline_kind); @@ -2195,7 +1985,6 @@ impl RoomInputBarRef { RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), - explicit_override: inner.explicit_override.clone(), editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), text_input_state: inner.child_by_path(ids!(input_bar.input_row.mentionable_text_input.text_input)).as_text_input().save_state(), } @@ -2211,12 +2000,10 @@ impl RoomInputBarRef { tombstone_info: Option<&SuccessorRoomDetails>, ) { let Some(mut inner) = self.borrow_mut() else { return }; - let explicit_override = restored_explicit_override(&saved_state); let RoomInputBarState { was_replying_preview_visible, text_input_state, replying_to, - explicit_override: _, editing_pane_state, } = saved_state; @@ -2238,8 +2025,6 @@ impl RoomInputBarRef { inner.view.view(cx, ids!(more_actions_popup)).set_visible(cx, false); inner.is_emoji_picker_expanded = false; inner.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); - inner.is_target_menu_visible = false; - inner.view.view(cx, ids!(target_menu_popup)).set_visible(cx, false); inner.is_lang_popup_visible = false; inner.view.view(cx, ids!(translation_lang_wrapper)).set_visible(cx, false); @@ -2250,7 +2035,7 @@ impl RoomInputBarRef { inner.clear_replying_to(cx); } inner.was_replying_preview_visible = was_replying_preview_visible; - inner.explicit_override = explicit_override; + inner.explicit_override = ExplicitOverride::None; // 3. Restore the state of the editing pane. if let Some(editing_pane_state) = editing_pane_state { @@ -2379,8 +2164,6 @@ pub struct RoomInputBarState { text_input_state: TextInputState, /// The event that the user is currently replying to, if any. replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, - /// The user's explicit target override for this room. - explicit_override: ExplicitOverride, /// The state of the `EditingPane`, if any message was being edited. editing_pane_state: Option, } @@ -2488,171 +2271,142 @@ mod tests { } #[test] - fn test_reply_to_human_no_bot_targeting() { + fn test_bot_bound_room_defaults_to_explicit_room() { let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - let reply_sender = test_user_id("@alice:127.0.0.1:8128"); assert_eq!( resolve_target( &ExplicitOverride::None, - Some(reply_sender.as_ref()), + None, Some(bound_bot_user_id.as_ref()), Some(bound_bot_user_id.as_ref()), &[], ), - ResolvedTarget::RoomDefault(bound_bot_user_id), + ResolvedTarget::ExplicitRoom, + ); + assert_eq!( + routing_directives_for_message(&ResolvedTarget::ExplicitRoom, false), + (None, true), ); } #[test] - fn test_reply_bot_overrides_explicit_room() { + fn test_reply_to_human_in_bot_bound_room_stays_explicit_room() { let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let reply_sender = test_user_id("@alice:127.0.0.1:8128"); assert_eq!( resolve_target( - &ExplicitOverride::Room, - Some(bound_bot_user_id.as_ref()), + &ExplicitOverride::None, + Some(reply_sender.as_ref()), Some(bound_bot_user_id.as_ref()), Some(bound_bot_user_id.as_ref()), &[], ), - ResolvedTarget::ReplyBot(bound_bot_user_id), + ResolvedTarget::ExplicitRoom, ); - } - - #[test] - fn test_chip_dismiss_returns_to_room_default() { - let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - assert_eq!( - clear_explicit_override_result(Some(bound_bot_user_id.as_ref())), - ResolvedTarget::RoomDefault(bound_bot_user_id), + routing_directives_for_message(&ResolvedTarget::ExplicitRoom, false), + (None, true), ); } #[test] - fn test_chip_dismiss_explicit_room_to_room_default() { + fn test_reply_to_bot_still_targets_bot() { let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - let explicit_override = ExplicitOverride::Room; - let cleared_override = explicit_override.cleared(); - - assert_eq!(cleared_override, ExplicitOverride::None); assert_eq!( resolve_target( - &cleared_override, - None, + &ExplicitOverride::None, + Some(bound_bot_user_id.as_ref()), Some(bound_bot_user_id.as_ref()), Some(bound_bot_user_id.as_ref()), &[], ), - ResolvedTarget::RoomDefault(bound_bot_user_id), + ResolvedTarget::ReplyBot(bound_bot_user_id.clone()), ); - } - - #[test] - fn test_chip_dismiss_no_bound_bot() { - assert_eq!(clear_explicit_override_result(None), ResolvedTarget::NoTarget); - } - - #[test] - fn test_explicit_bot_with_reply_to_human() { - let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - let reply_sender = test_user_id("@alice:127.0.0.1:8128"); - assert_eq!( - resolved_target_user_id(&resolve_send_target( - &ExplicitOverride::Bot(bot_user_id.clone()), - Some(reply_sender.as_ref()), - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), - &[], - )), - Some(bot_user_id), + routing_directives_for_message( + &ResolvedTarget::ReplyBot(bound_bot_user_id.clone()), + false, + ), + (Some(bound_bot_user_id), false), ); } #[test] - fn test_cancel_reply_clears_reply_bot() { - let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + fn test_reply_to_bot_overrides_room_first_default() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); assert_eq!( - resolve_restored_target( - &ExplicitOverride::Bot(bot_user_id.clone()), - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), + resolve_target( + &ExplicitOverride::None, + None, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), &[], ), - ResolvedTarget::ReplyBot(bot_user_id.clone()), + ResolvedTarget::ExplicitRoom, ); assert_eq!( - resolve_restored_target( - &ExplicitOverride::Bot(bot_user_id.clone()), - None, - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), + resolve_target( + &ExplicitOverride::None, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), &[], ), - ResolvedTarget::ExplicitBot(bot_user_id), + ResolvedTarget::ReplyBot(bound_bot_user_id), ); } #[test] - fn test_explicit_override_persists_navigation() { - let saved_state = RoomInputBarState { - explicit_override: ExplicitOverride::Room, - ..Default::default() - }; + fn test_persisted_explicit_override_is_ignored_on_restore() { + let saved_state = RoomInputBarState::default(); assert_eq!( restored_explicit_override(&saved_state), - ExplicitOverride::Room, + ExplicitOverride::None, ); } #[test] - fn test_reply_bot_restores_with_replying_to() { + fn test_stale_explicit_bot_selection_is_ignored_without_available_bots() { let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); assert_eq!( - resolve_restored_target( - &ExplicitOverride::None, - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), + apply_multi_bot_target_menu_selection( + TargetMenuSelection::Bot(bot_user_id), &[], + &ExplicitOverride::None, ), - ResolvedTarget::ReplyBot(bot_user_id), + ExplicitOverride::None, ); } #[test] - fn test_target_chip_room_default() { + fn test_reply_bot_restores_with_replying_to() { let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); assert_eq!( - format_target_chip_presentation( - AppLanguage::English, - &ResolvedTarget::RoomDefault(bot_user_id), - Some("BotFather"), + resolve_restored_target( + &ExplicitOverride::None, + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + Some(bot_user_id.as_ref()), + &[], ), - TargetChipPresentation { - visible: true, - label: "Default: BotFather".to_string(), - subdued: true, - dismissible: false, - }, + ResolvedTarget::ReplyBot(bot_user_id), ); } #[test] - fn test_target_chip_hidden_no_bot() { + fn test_target_chip_hidden_in_bot_bound_room() { assert_eq!( format_target_chip_presentation( AppLanguage::English, - &ResolvedTarget::NoTarget, - None, + &ResolvedTarget::ExplicitRoom, + Some("BotFather"), ), TargetChipPresentation { visible: false, @@ -2664,63 +2418,21 @@ mod tests { } #[test] - fn test_explicit_bot_via_chip_menu() { - let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - let explicit_override = apply_target_menu_selection( - TargetMenuSelection::BoundBot, - Some(bot_user_id.as_ref()), - ); - - assert_eq!(explicit_override, ExplicitOverride::Bot(bot_user_id.clone())); - assert_eq!( - format_target_chip_presentation( - AppLanguage::English, - &resolve_target( - &explicit_override, - None, - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), - &[], - ), - Some("BotFather"), - ), - TargetChipPresentation { - visible: true, - label: "To BotFather".to_string(), - subdued: false, - dismissible: true, - }, - ); - } - - #[test] - fn test_explicit_room_via_chip_menu() { - let bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); - let explicit_override = apply_target_menu_selection( - TargetMenuSelection::Room, - Some(bot_user_id.as_ref()), - ); + fn test_room_bot_mention_overrides_selected_explicit_bot() { + let bound_bot_user_id = test_user_id("@octosbot:127.0.0.1:8128"); + let bob_bot_user_id = test_user_id("@octosbot_bob:127.0.0.1:8128"); + let message = RoomMessageEventContent::text_plain("@octosbot_bob 你好"); + let resolved_target = ResolvedTarget::ExplicitBot(bound_bot_user_id.clone()); - assert_eq!(explicit_override, ExplicitOverride::Room); - assert_eq!( - format_target_chip_presentation( - AppLanguage::English, - &resolve_target( - &explicit_override, - None, - Some(bot_user_id.as_ref()), - Some(bot_user_id.as_ref()), - &[], - ), - Some("BotFather"), - ), - TargetChipPresentation { - visible: true, - label: "To room".to_string(), - subdued: false, - dismissible: true, - }, - ); + assert!(message_mentions_known_bot( + &message, + Some(bound_bot_user_id.as_ref()), + Some(bound_bot_user_id.as_ref()), + std::slice::from_ref(&bob_bot_user_id), + &[], + )); + assert_eq!(routing_directives_for_message(&resolved_target, true), (None, false)); + assert!(message.body().contains("@octosbot_bob")); } #[test] From 1a3399858268f76673a35d396bad6a63e307b240 Mon Sep 17 00:00:00 2001 From: AlexZ Date: Sun, 12 Apr 2026 07:05:53 +0800 Subject: [PATCH 155/283] Polish bot timeline card rendering --- .../2026-04-12-tg-bot-timeline-cards-plan.md | 184 +++ .../2026-04-12-bot-timeline-card-design.md | 192 +++ resources/fonts/LXGWWenKaiRegular.ttf | Bin 0 -> 19073964 bytes resources/fonts/LiberationMono-Regular.ttf | Bin 0 -> 108168 bytes resources/fonts/NotoColorEmoji.ttf | Bin 0 -> 10643852 bytes specs/task-tg-bot-timeline-cards.spec.md | 145 +++ src/home/room_screen.rs | 1096 ++++++++++++++++- src/home/streaming_animation.rs | 33 +- src/shared/html_or_plaintext.rs | 6 +- src/shared/styles.rs | 25 + 10 files changed, 1652 insertions(+), 29 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md create mode 100644 docs/superpowers/specs/2026-04-12-bot-timeline-card-design.md create mode 100644 resources/fonts/LXGWWenKaiRegular.ttf create mode 100755 resources/fonts/LiberationMono-Regular.ttf create mode 100644 resources/fonts/NotoColorEmoji.ttf create mode 100644 specs/task-tg-bot-timeline-cards.spec.md diff --git a/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md b/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md new file mode 100644 index 000000000..5ae1b6664 --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md @@ -0,0 +1,184 @@ +# Telegram Bot Timeline Cards Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restyle bot-authored text messages in the room timeline into Telegram-inspired reply cards with a clear body card, lightweight status strip, and subdued metadata footer. + +**Architecture:** Keep the current mention/reply-first routing and current Matrix payloads untouched. Add a small bot-message parsing layer in `room_screen.rs` that recognizes Octos' existing status/provider/footer text format, then render bot text messages through a dedicated timeline card sub-structure while preserving `HtmlOrPlaintext` for the extracted main body. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, existing `HtmlOrPlaintext`, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `src/home/room_screen.rs` + - Add pure helpers for parsing bot timeline layers from existing Octos text output. + - Add bot card widgets / styling to `Message` and `CondensedMessage`. + - Wire parsed bot layers into timeline population for text and notice messages. + +- `src/shared/html_or_plaintext.rs` + - Only touch if the bot card body needs a small style or spacing hook that cannot live entirely in `room_screen.rs`. + +- `src/home/edited_indicator.rs` + - Only touch if edited indicator placement or visual weight needs a small adjustment to fit the new footer hierarchy. + +- `specs/task-tg-bot-timeline-cards.spec.md` + - Verification contract for the work. + +--- + +### Task 1: Lock Bot Timeline Parsing with Pure Failing Tests + +**Files:** +- Modify: `src/home/room_screen.rs` +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Add parsing tests for the happy path and fallback cases** + +Add tests for: +- `test_parse_bot_timeline_layers_extracts_status_provider_body_and_footer` +- `test_parse_bot_timeline_layers_falls_back_for_unmatched_bot_text` +- `test_parse_bot_timeline_layers_ignores_regular_user_messages` +- `test_parse_bot_timeline_layers_prefers_safe_fallback_for_malformed_metadata` +- `test_parse_bot_timeline_layers_invalid_metadata_does_not_panic` + +- [ ] **Step 2: Run the focused parsing tests and confirm they fail** + +Run: + +```bash +cargo test test_parse_bot_timeline_layers_extracts_status_provider_body_and_footer +cargo test test_parse_bot_timeline_layers_falls_back_for_unmatched_bot_text +cargo test test_parse_bot_timeline_layers_ignores_regular_user_messages +cargo test test_parse_bot_timeline_layers_prefers_safe_fallback_for_malformed_metadata +cargo test test_parse_bot_timeline_layers_invalid_metadata_does_not_panic +``` + +- [ ] **Step 3: Add a minimal bot timeline layer parser** + +Implement small pure helpers in `src/home/room_screen.rs`: +- one type to hold parsed bot layers +- one function that only activates for bot senders +- conservative parsing for: + - optional top status line + - optional `via provider (model)` line + - optional trailing `_model · X in · Y out · Zs_` footer +- safe fallback to full-body rendering when the format is unmatched or malformed + +- [ ] **Step 4: Re-run the parsing tests** + +Run the same five tests and confirm they pass. + +--- + +### Task 2: Add the Bot Reply Card Structure to Timeline Widgets + +**Files:** +- Modify: `src/home/room_screen.rs` + +- [ ] **Step 1: Add rendering-state tests for card visibility and hierarchy** + +Add tests for: +- `test_bot_timeline_card_visible_for_bot_text_message` +- `test_bot_timeline_card_hidden_for_regular_user_message` +- `test_bot_status_strip_renders_above_body_and_not_inside_body` +- `test_bot_metadata_footer_renders_below_body` + +- [ ] **Step 2: Run the rendering-state tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_timeline_card_visible_for_bot_text_message +cargo test test_bot_timeline_card_hidden_for_regular_user_message +cargo test test_bot_status_strip_renders_above_body_and_not_inside_body +cargo test test_bot_metadata_footer_renders_below_body +``` + +- [ ] **Step 3: Add bot-specific card widgets to the message templates** + +In `src/home/room_screen.rs`: +- extend `Message` with a bot-only card container around the message body +- add optional `status strip` and `metadata footer` regions +- keep the existing username row and `bot` badge +- keep the main reply body rendered through `HtmlOrPlaintext` +- make sure ordinary user messages still use the plain timeline path + +- [ ] **Step 4: Populate the new bot card subviews** + +Update the timeline population path so that: +- bot-authored text/notice messages use the parsed layers +- main body text is sent to `HtmlOrPlaintext` +- provider/footer text is routed to the lighter metadata views +- unmatched bot messages fall back cleanly without partial junk UI + +- [ ] **Step 5: Re-run the rendering-state tests** + +Run the same four tests and confirm they pass. + +--- + +### Task 3: Preserve Reply Preview, Condensed Layout, and Final Rendering Semantics + +**Files:** +- Modify: `src/home/room_screen.rs` +- Modify if needed: `src/shared/html_or_plaintext.rs` +- Modify if needed: `src/home/edited_indicator.rs` + +- [ ] **Step 1: Add regression tests for shared timeline behavior** + +Add tests for: +- `test_bot_timeline_card_body_uses_html_or_plaintext_rendering` +- `test_bot_timeline_card_preserves_reply_preview_and_condensed_layout` + +- [ ] **Step 2: Run the regression tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_timeline_card_body_uses_html_or_plaintext_rendering +cargo test test_bot_timeline_card_preserves_reply_preview_and_condensed_layout +``` + +- [ ] **Step 3: Adjust spacing and supporting widget hooks only where needed** + +Make the minimal changes needed so that: +- reply preview still sits correctly above the bot card +- condensed bot messages still render a readable card body without restoring a full profile row +- edited indicator and footer do not visually compete +- `HtmlOrPlaintext` behavior remains unchanged for links, emphasis, and line breaks + +- [ ] **Step 4: Run the targeted regression suite** + +Run: + +```bash +cargo test test_bot_timeline_card_body_uses_html_or_plaintext_rendering +cargo test test_bot_timeline_card_preserves_reply_preview_and_condensed_layout +``` + +- [ ] **Step 5: Run the final verification gates** + +Run: + +```bash +cargo build +agent-spec parse specs/task-tg-bot-timeline-cards.spec.md +agent-spec lint specs/task-tg-bot-timeline-cards.spec.md --min-score 0.7 +``` + +- [ ] **Step 6: Manual GUI validation** + +Run: + +```bash +cargo run +``` + +Verify: +- bot replies read as distinct cards in mixed human/bot rooms +- status text is visually above the main reply, not inside it +- provider/model and token/latency text are visibly weaker than the answer +- long bot replies stay readable +- reply previews and condensed messages still align correctly diff --git a/docs/superpowers/specs/2026-04-12-bot-timeline-card-design.md b/docs/superpowers/specs/2026-04-12-bot-timeline-card-design.md new file mode 100644 index 000000000..977d1b37b --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-bot-timeline-card-design.md @@ -0,0 +1,192 @@ +# Bot Timeline Card Design + +Date: 2026-04-12 +Status: Draft for review +Scope: Robrix2 bot conversation timeline styling only + +## Goal + +Make bot replies in the room timeline feel closer to Telegram bot chats without changing the current mention-first / reply-first interaction model. + +This design only covers the visual treatment of bot messages already rendered in the timeline. It does not change routing, slash commands, reply semantics, or bot action menus. + +## Context + +Current bot replies in Robrix have the correct functional behavior, but the reading experience is still flat: + +- bot replies look too similar to ordinary Matrix text messages +- bot status text, provider text, and token/latency stats compete with the actual reply +- the username row does too much work to communicate bot identity +- there is no strong visual center for the bot response itself + +The Telegram references the user supplied and the official bot features page point to a different hierarchy: + +- the reply itself is the focal surface +- secondary metadata is visually quiet +- bot-specific affordances live with the message, not as a permanent input-state control + +Reference: +- Telegram Bot Features: + +## Design Direction + +Adopt a Telegram-style card treatment for bot replies inside the existing Robrix timeline. + +The timeline should read in this order: + +1. sender identity +2. bot reply card +3. quiet metadata footer + +The key shift is that the bot response body becomes a clear card surface, while generation status and model/provider details become secondary layers. + +## Chosen Approach + +Use a **carded bot reply layout** inside the existing `Message` timeline widget. + +Why this approach: + +- it improves readability without reopening the routing model +- it fits Makepad's existing `room_screen.rs` ownership model +- it stays local to timeline rendering and avoids input-bar churn +- it matches the Telegram references more closely than a light typography-only cleanup + +## Message Anatomy + +### 1. Identity Row + +Keep the current username row, including the `bot` badge, but reduce its visual weight relative to the reply card. + +Rules: + +- username remains on top of the message content +- `bot` badge stays compact and close to the username +- identity row should not become the primary visual anchor + +### 2. Reply Card + +The actual bot response body becomes the main card. + +Rules: + +- use a soft rounded background behind the bot reply body +- keep padding noticeably larger than ordinary text messages +- preserve Markdown / HTML rendering through `HtmlOrPlaintext` +- avoid making the card look like a desktop form panel or debug container + +Visual intent: + +- lighter than Telegram's mobile bubbles, but clearly a separate response surface +- warmer and more intentional than the current plain timeline flow + +### 3. Status Strip + +Streaming or generation status should not appear as if it were the first line of the assistant's reply. + +Rules: + +- move transient status text such as "thinking", "generating", or tool phase summaries into a small status strip above the main reply body +- status strip should read as operational context, not content +- status strip can share the card family but should be visually lighter than the main body + +### 4. Metadata Footer + +Provider, model, token usage, duration, and related diagnostic text become a subdued footer. + +Rules: + +- provider/model line should sit below the body, not above it +- token/latency stats should be the weakest text layer in the message +- footer should remain selectable/readable, but never compete with the response + +## Visual Principles + +### Hierarchy + +- Primary: bot reply body +- Secondary: sender name and reply preview +- Tertiary: provider/model line +- Quaternary: token/latency stats and edited markers + +### Shape + +- rounded corners should be consistent across the bot card family +- avoid mixing one radius for the card and another unrelated radius for adjacent bot-specific surfaces + +### Spacing + +- bot replies should breathe more than plain user text +- footer spacing must be tighter than body spacing +- reply preview to bot card spacing should feel intentional rather than inherited from generic message layout + +### Color + +- use restrained surfaces, not loud brand blocks +- bot responses should feel distinct without reading like warnings, notices, or selected rows +- keep contrast strong enough for long-form reading + +## Component Boundaries + +### Keep As-Is + +- message routing and target selection +- input bar behavior +- mention parsing +- `HtmlOrPlaintext` content rendering model + +### Change + +- bot-specific visual treatment in `src/home/room_screen.rs` +- bot message sub-structure around message body and metadata +- optional small supporting style hooks in shared widgets only if required by the card layout + +## File Impact + +Primary: + +- `src/home/room_screen.rs` + +Possible supporting changes if needed: + +- `src/shared/html_or_plaintext.rs` +- `src/home/edited_indicator.rs` + +No planned changes: + +- `src/room/room_input_bar.rs` +- Matrix routing code +- Octos backend + +## Out of Scope + +- inline keyboard / action button UI +- menu button near the input field +- slash command redesign +- bot profile pages +- non-bot message restyling across the entire timeline +- protocol or backend metadata changes + +## Implementation Notes + +- Prefer adapting existing `Message` / `CondensedMessage` structure rather than inventing a second message system. +- Bot card ownership should remain in `room_screen.rs`, which already decides sender identity, badge visibility, and message-body population. +- If dynamic styling is needed for bot-only surfaces, follow the project's Makepad rule: avoid relying on `script_apply_eval!` for dynamic widgets created from live pointers. + +## Validation Plan + +Manual validation should check: + +- bot replies are distinguishable at a glance from user replies +- the main answer is visually stronger than provider/model/stats +- long bot responses remain comfortable to read +- streamed replies still look coherent while updating +- condensed messages and reply previews still align correctly + +## Success Criteria + +The redesign succeeds if: + +- a user can scan a mixed room and immediately spot bot responses +- the assistant's actual answer becomes the visual center of each bot message +- operational metadata stays available without dominating the timeline +- the UI feels closer to Telegram bot chat references while still fitting Robrix's Matrix timeline diff --git a/resources/fonts/LXGWWenKaiRegular.ttf b/resources/fonts/LXGWWenKaiRegular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..de26379a07d2fedf2d9ea1eaffc7d785d89b160a GIT binary patch literal 19073964 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
9fnx1n;`;LpyRqBi)BUU9wGI*yD~2bp?tm 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®1l=Cs`n@10Mne^1ilw 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

`Zf@*} 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

}@@6S>H6l_4-bHjWa8Br_-& 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�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

^S-Yu>DEQtR92z(K|XmLrPjS4u!Kz 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�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

n=LP?b`7fv< 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 z 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

I72_J9%~5j(}$+e0jQ?@qkv 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 z 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 (3bE%m|TQVH6jP5To-QycQ^f>j55PR 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

KF@dgSa%`w~9ZZ%WHbo!y~Sj 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 u{f;(?cC|Y@+Ii;r^Lakc=jpn= z>$8HUNTt#VK3ovKdAVBBl1zqbk~Ct=V1ewX!&o3c;mQq47$O^%JVx$Nc^C1BxFc6A z>3wp!Q4*C)oZRR{4)F_;vL~4`*^dlhBm`o74#+ufMfMx$NlleNV{9eICP=MBlTIQM zV0{vTFvi3xwC}W{G0P3i%-s3mp`elKHQjqR4WYam^lw``fj|EO8L9o#?AqDp!-c#% zsP3w^DrUZQA)|wyCzt7L=r7*QdTCZfgrC>$*}=+>^gH7qjNx=Hs&8CsCIqn^+9Z&6 zSSmj0+*VAxm1rW9@8=!H|2kjEyDD@xR2!q2%7<#|#!XIi*G83nT1CzswJ2oTCSyLL ztbhN#>XSQuhW>KJ$u5NetKl*GR+RChHBdV861T7Ehx1-^`m&#T&5tKLe1<0q#qkpD z@QcGWHD&wx?tF@XiWD-u@U`#ms4b1P_1m$BO9kt)dd^dE<-nibM-A8*FT{yfSqh!~ z4;l{{V<;_cqMka^b)4DBFL!-SV{sgXk|@qGv;liau9T4|f^wf1<8X_Rh`235t&A8_ zkx11IO@W)m+^;wc;4dOTM-)_^$x4=s#%}`6@j5}fY940Obq>>WdA?9j>-vc@vMvHgvO`&_ru-r1iaF;!kOehPiP{=IX^i;iyA zReM)H<1{Se>N$HqQl@5!CG$|!Q5|Y823vFWKtm-vY}(slG~J+RYJSGc z_zSuh20B@P;ieRw0^9XX!!g3hPFt_F^JPa%4|f;ay;=56*|mX+(c0-)esM4?3>0G~ zCJeWl3(BdrKU&b+EbdvgP;O#A73^5g*8DGB=X&fB#QdvVIJ45vGvNu6xMB@9!#p$BFE&zF`U1Rwp}?MjQg*=(rmK0A0dw0BdU zZcm=w$TPN<4RqTzztih7&dDyd|EAuYDdZ!*i)vO|DdmBvt-Q-XuO5V^#nk<2BlIlK zTu29|r8IS;#^_+318SEU`%Q(esVqbD!i?>k+b(RX&6U##0DFzmA!8BdR=&DxsDFvw z-G0XIne7=mw#hM1o2}GdcT(M`*50HkQ?F&v4mPZNm4WBsXUptQms5^Q+jOhJe6F4? zcuXW)_nW5D^RdM>dv(tCkLii^F%>pTjc|x4aSr{c|B=K93CvP?0#!)CK zPXvFHB36Jji04Vo06vl=6GI&kJxM|nRF;%eAiJalmTX4eLqb8>LaI@eTr&B+JRtud zLrFe%C_(8h(%f<3s`Cd1XQdMi(&ocwV&;Q?B5WS1?uF&+PA`b@69BI`zEPz{bgucc51|Oke)$5 zfgPOvm}>PewYuuRR$a5D{R~~z4Koc&ceDS^Uu{RKFxs2Cz`=q=Z{!B}?=i>S*n3D| zywEYSGXsyD);6^^)cuv_!1%?K(KbAo?4jwrhR8XkXGmQyNxA)I`PyGpv z0&gR=$m6GTtO{q*7wWv-%$}QGDzNUM@9(D5`8HE74pR_rVdD@P*6tln?D)&=*|V|n znx~Ha`i)O(jXo8!^DuQ48H(A}|MQoOZzMcv+1>C7nQ}<90C25@acqLU>Nqzf@%tBaXtGIVbvk*JsW?7fJAu2kGg{J#lRh5yVA;IANGj3k=gCh=H&3z|+&Z}? z&)|TQ17nV2eBw1ZnAE>Kvwl3zN75szVhCU&+*=3}GWAqK@DdL%i7Wq=!~Ms-siTfN zU|Y@698KxNwG-*sPerx[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

>jyE)0 z4jvBlJ^bd$P2XLEwmGD1EC`2)e zjFXNSH4SB(BKT0F6lQS+`zEt${V%#&czL9qNL1kBMCXED@cn8A)`brz_nibSWt>>P4I)wUx6WpojXcdDX@it%Hz%d&0Y(wO4pW` zGwe*MJb0B+;DPAW#q$MYQs(**kniN@({C{2w`-^4D_5;KcQSYEk8|wU!bC=<2Ph<5c;{|Hn@>q za8Eq6EXCeLUj?%unIRuM^Ah7H_D@Gb*^nn&%DQ87xOGt0uqKry5X6%H2>KTb{M-ie5eo3hPhpOl`ql}| z3@vTj*mf^%cOuleifg?FUjfR0$s9Ol10#8r?bH&i5#Tfz@sS41L zbqJuQwgqTD0mlCRE!He!WbC}uL#I8qX8`KavgI~^^akE1iSD$w4{?V5bDuH11z3+} zG|!@X$S_@&mwn^k9Xk?K{0Fc0F4v8YtMRxuOK$V%W!@g;h0op<+)!MN#oVDAyA$k0 z*^#d4ie~-@B>VfY(9@cT#6RM57mRsuCOz~P(%w89R6vd`Ftrg2zLk-IjA(`O50-(U z^8}`%EoMMpD;?Mh2wHcnLEoAI6dcmcIW-oOfk`u5#)TvT;0K9PgTifz#gCE_GDo?eEqrtuHYG5%e3Q(xLXA<>?>`PZTqkn=+U-II8C}_ z@ry;SkHcw2V?7XW2_JSKBl`)^GB{`*tUZ10chR4+6~o5W9EOgzw6n1G^f1o=6_m~28If#!?K}1 zXQZJHJi#=3o3(q`QnbI@3LU}GFoX!P3T$~2`lS=ud}D^of4B|$NW?jL7Pupa{?)`{ zY{$ij>qo}*h-|NhL!b%~y<^l08m`CSrYa$46^f5mmjfE%Y%;pQ)m39#v(%Gk{ZJ`yx;|D&{a<~%OP zp3&xSl-D1$Mn_+|CBc3$tUY%?-PiB+#vAKDmOjvBBSLore)Od8Yzu^UYlHQ5spt3! zHWEyNX}b)2@0mg<(tue(zo{f`kHyUlR$)g|kTzTq;LfJh1^nh;y$Z77+MpbQZg)m; zyFt8mP3YGEbVQ1p#uyh-=`9mnVDUh|%Cn``nDm*;m1a20{25QXEf|Gi_ zj_ch>W=;QbcFX;vfSb5a>Dz;Kiu3M!UupMm?MeA3hNRMdD;zsDni(zsP|=jNmtJL~ ziH;8eMF?DP7XrbBiS{LYUyR+j%BX%R+_-0F>?Y6j`@5hGngXTrrnT0d_CBc{tHsQ7 z6TM%YP>rMI*mkVEW%_*(P=Kb{P_R(l8IK&9VQ4_QkM?3_SSz0>PntUwW^8u7w0T&{ z^Tru15IX+0BQSrVm1?S6UidVXSK3nL$lsKs(qMe(AVf%%k<`R4=o{ zL2V49##Z6Ds~g6ddRV;?0eoXum|>@U1Su8iwdfUt*{jtc^~OzP!D=v+c$tm1`rBRdFJ4;3xZrG95o#+ zu1p*+u3Q?QgN6zj2@Xv7YmsgVCk~fG=ZLEq*aFT9XO6rO2S^Yy%3Hh+sieTIEIJBd zZFIfEqNu8bqk!p=6G5R!o$8h#2h|#AZY@TA;1ObE9JF#>hO=j2C+-px1Q(jrK_avpFz5B9jLI(gIr79DQj4~ z$$2m(IYs8U)}O^7{u{w|levbza00V1zdSOoq$dx-i*WY*Kh2F?waZn4-Wfa88H!~$ zr9XwnLC$qPpSJQ<Gw}mB`rv9160r9Xl8aa+c}jhwQNpYa*W@BCr1PU6`KfKO*;zR_KP@s z3zsOaJk zoji&d(Q@cNB>|1LUEM}BB|pBBCSu$KH^gS|}hqdAT8pa6a6)S6`A^vMi+ z8R#MQ6=9H1(+9Z2S6aMq#2lFB`W?6Q2wl@J%U_don3pZ!M1Kr#Ul6T6oGIcgjZAtZ ztlt`-M6%XeE{XGnIQ7aL^homaoL^8* z%}D$5Syz612pd%k?19x`x5A8Af5{w%Y>nnjgFcF>$RS&6hs-SEy;b*0xCS>QRcAO4 zQhteDHJxS$Mi`{OtJcaTRK`&w3HU*Ym!>dj2_T0n?B3z%K;Ch?)}UJfEL+d0yDmJb2ls3v6(?f4IpE9 zuFw(bPFRrW#PYv~KU@$QFd(Qx@!+Nzq__p!#_wTZV|sJu+c)64k7TF*`+jCUHoDj2n6ZBf{vn7O!hubd z+O7j%<<+*oAZ9UtAP1y+&ObQliQsaa&p;AZs3;qTXno}(ocMh1tnb2sx|8dqR;mGu27 z{(@iHS2llaq+jT3kEVE`b$f|&U;JqlFfuV}>qbTvevVA?uFa9Tz&!*wfvqczfe)6|En0W$U6IX&q-UcDCE_&k@`YCqam$p zUJ8V}*P^8PI@_6q7Kt(n6Z58}U0^XT@cM1$5$6%cCqRuj+3Sa#FAHQ(vvQZ__4iq` zhOVUgNBBrE5S&MNx^2t~L&E-0SWS<*X($d$U2-Os(>mItp|M$B@`JpATZ8Vz4@;ou z!P-G`leLm&$-9kvlDF>nz?lG6??>E>D{`Q}yQ^?VGzP-H4K2SJmUztM?$*_~dcdSJ zS^g+&X=Ax^SUqN(T8P11P{lU><9?BshT7uTCEtTOW(82mcc_ZNiBO9JzwH+KNT-aP zg)VT|2noB<(V`I+P$0gZ_ed!cQLb7fUxjWMYyhUA(3}MvSt|j-o)khdL=2>$MU+UG zj;NelE(~s~CcY;~3pc@+5t)jN3vUm8mGB*89{2zdFo1A7oEoJ{o%jsH4a7P@>O~k^ zw3REYT6~Tw;Ejh!uTX-eK$_3=Df+bm3SyQ1_@`6W%5Wzt+9?! z=c={qeSup-ERnTNG#i@_7<03l(ca;Wf|)&z0ygAV>bG~7eMxg;Agq0|`+QC|bLpqk zW>Kr&dxD$FVXUhTT^mhER~ec=0m?ma&80gEOY*7crex+28!6;;Hl+!EpH)KQ``vCYJi0}tgy#c28Dhy_fV%8NSAWv$q?glGKyop?#!=54qJ@&5(Y}-L! zwHqDwyU}6)7Ay$?0su650;*)-8))JT?f?cOxZ22>;-Fy&?;x;jVRo%Bg#*SBUm;{H z?k<9B5Y0GI{Rr!#>PT$kgt1nICu#W*vKg$AqxXjY!=sR6!-UBtQL#kn(PB#%pFp@p zfxQR|#{;O;lKV7!Nx$}{_our`ku0l5!cHxxH2E4UPb3kbcrA4NdTlVa)ybC*Z!kT712fAfi{zlA8pMxNO zAgUcUwYfQy-MKU*?blFmzgdcYaWV}>=I#v;l3&`vANp&1r*VR%qQ~a@eYsIBwS*m} zwVe?9RgX=CBM=g^jR!efcJ0Z)3M5jK-W1{(_<(BDcFWS&OcZY-9rzXAos@ zORUyaYcU-Fhr>6(%g_}a2MkW8LuX0BD34cZ-A#5yhvl^qm!|jUFTOQS;^5lKqi=}KrUDuOk zP=|pNpASUUPrKS%m>VBW%RdR{UqMcxRLjS(xbGIKh|>#}_klP&tGq4UZpVCQ+@gA0 zDAjR&M$f5BZ^9lF*E39TB{RW?$6np|TP1@1i=nV8UE|L~;1?jf=|)1ljLB-poXdcL zZJkV?0C!%`ghPIrn`0Qnk(?!%8kl7dIH2g)jd*)m^%=1Y{Bv)UwsO@SBZCd1l-#D+Wr>TH-58}T!3DSBNT8Ft{(_SR6W2} zbfL9eIhg<|PbzUl=bdmZ(ec9Z(VRtHH0BWwsOKO;3I4znU=Gyi5`!Dnx)+D=8J;QL z3Vwyf#Z(VD1e(QLL@8h+_*4j0l(r&W75S)Woa1H@wjf!sh_;ZdiRi%-=VCFnB`3Am z>jFeCT|d8PsgM0a`Zk7apt##vS>Vb6l{y@3Vv_s%F`rhl%!V6OrB#r3bz@iyVkpg%C3ItYqZol(ayRSplpKE8TMhUXvZg01xLfK;5FczMxwzR|2LCXN6KM8NgYH@QZII4SC za=9WSYe&iG+W`q2P{2?xYDT&Qy^3A;jFen#y^y=d#u??SQ@}^4Ursu**R(x}c!A2N zbPw-%sP{{rYlEGZd)I67}oQ`F`L-i zvECQ;(J+mxeI}QUjaBt93ty#~qlcwJ23?)UdQmlfSz+5E!F+JWk^Lu@gf2_hkECBJ zV$|%?aK3QLEiBMn&d|yNJH*Xh6FBJ9ZPpp*3{>MED~xJ9-Tck!gTB-!>(~w zs~J;O_lD4I5S}@62&{|A=Ic?l#DC#*uBdcC=^S7vWYyj6tGT<}7wppby&1p*t~ZMx zQJYK~%)Y5h*Jh;2LUv^^!F4yd^r?UERw0@d){`lV$pb}CDo>v(tCD|>4mLW%o{Al~ zVa=(>BiD!7JsEan?{*~(*3S&=>VCNhoW=&a_u3!JaN;wRCR-Hbv+EZ>Ik7(*VFk1%iVnE zH&s9uMC&{#nz`^Pn7*Cg_$l1T9$<&x`wk!2uJP&U)xJHa>_f(0j2t{LpJ}qENah}X zBYS`YrM+c{rBP5{-H#W`(OzT>%9q^}cdavdFz^-($=`g}c5*y@&MLgl%Z{VRw?P1( zndj6@@E$eL&%N=T&|%z!iBopD{M-~*`4MeZ`#e{a;{|?Rq$r7O;xKdQ1xFBsZ3pv*0t9@kOeW1uk{u^|=%hv>+2idNmvQ@^)1D)#5l%=)vU_iv+r z*w?lMJ;aCFzTAe@*}{gfm=z~UwuM8+nV}y`(G>%$2#PSBcmsZ{<^_tx5TM+d_yRa; z%&CG0f{KWaof-&$h2VSqgdU^t$DK%Zsy4+wm^`cxO+ff7xTGu11lWkMVcg!@vO~=1 zt{N3t6bH#8QwD`6ga3|81Q*SswY9PcVIaWHGhde89UKHS%HyNPhnqLP>W^B?6h<*5Nf`gTYke4Hz_mdAL*x^ zBcCba(byUK)D6qI^~CKCz&#C2G%#(({O0ijdqT~|T=qM|-YX3?oU1eYHO?;;q^-S% z@oFr40orfwhG9)#7YOnN{&vEjSi_Z$tna8D>h}hC^-&{BINk39RN4E}->LcUzmZHp=qD zn^2#RBHtp-eUA7@8}H|#0rVJcmqe4F&b5$7Fr#A^X_D{=bXryFA>ag2V2d0WsTXo8 zI0IrFH?Z^sHincMjshMCEHIINRe-PHP+JI5UKWWv+!mG12&64|1_f0?@DAcv3i>PY zlDtgI5s4R9@Zx00!VAF(UFkX0>b?SB*?}0bvN_R*U2tVrd3k&s$>%O?ez}-Sy$SUe z>DsarmoM6=Lokd!4B_?8<=VX0e6u2FK9%4Hq&qu9)+2Y6^*%tl&PmUho>FY*=x5yV zZgb-ZlbXMc<=m$|z<8J?v9%S;d1%-@%Hns8NjIR|jw zHJ^SuI-nRwHe}tjzw=^AE%LrSCCw}^oJ0dDd{WCx>5PpjcE0s$i@iCGMbD6FOR`8b ziyaV}vS1Yq6Kf42wAr*hb1tXyZ0td)1K{0?ihSaEnFao(TnlP?0Spsvb+JTvJUB~Z~=ndJ*MdufIi&jv8R`WWWisdwmLHwRT$G~a?NldeaU)Z``L z0y-(TL0btJ47g*csT0s@r7N8Q;S*7)62~0{Q4hmG-~@%kfG+_u(9&XsuKwu$n0BwpraL8T9lSL0#-|`t%cYDl0lS2(uD>jJ{PzKcr+E~6sAOGCh~Zg z9_*^_o3T%x$vNhZuR*<ez;4xUPKt_qcTR|M(OXtG6NqHQ#P`C=yB6VP%JnbsW#E zU>*nu>d}pfcE9}H0{D5qK+|XW$Dw(ORx=hvR!b!ikqTpvuIiGl$LH=?*^k2NoW;h@ zVnh}2emo+@mIX%kj-^jnspIi>_8O1oozErBT*qw32FtW_dm!u|y0)x>IgTFpMqi*1 z3A_4iPQm0G7Jbe80=@{E5!l!)eWXBZV!|QQF_H9Qk=js4p|ex-2e6408gHOxyIaz; zL9Au>5<&Ek^^B#mZhc7)Qs&62LG-VQl>5)qkO3^1_}Vl)53eNG{hDGdoQmQ?`G+nb zEk$JBfzBdfc1xhi{RPZ1&L^8D9lHR`qZw+7SBu0^q-|Drt9+So;4j#$~{dtV5hBz@pn611^v z91vZrk$?|Hdjj5nOR=$wWZQ*s5IX<#k>3|tx6S-89xPtR3dFjG>*g?ChYEXAEfX(B z%4UX33k>KTp?&5-caVl;AcW>hxHIqXtogAn#P9nPC09!aAhz3SM1nwOsyXBZ;dbno z!)W8695yb+4JL=|D7JZB#xD4)FqWi6dJSx8Us_nRL<>ZmW%XhrZKXCN;>2Bf~IQknNJ7g%OEaL8%ZlLp~Nt7~$Yy$u3z? zC=HE``J{X>QtHhjsvEu51;rc3ma%aDfvV&8g%YJ}nU>9|(gzc^EK6x;^|&=M0KX

}{ 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

YB($2kj&q{7L`LifEgj9|+m_<3&WL#* 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ăYrgno)^MSf&BSgyf{Ap6`g=6MzdzJRK=CZwD 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

>f42GOy`aZLLO z-fNxX3|}glutw=!L$y`1O5z&nvpcg!jpem$R-EIhXOqbbX=FCkv4h8*H`CR$OYWaBfB+MOc6Hby zw6at%oiGzd%Fu0{sS(Rg!39g<Xe@)2avy5CfMT=4^TbdEU=Wf(46F&MM7jiV?>8>j zrCA*DEb=%ed-BW!&q%K~oHKn?p}B~HA8Y}jjbDJP{REARw^9q%K+y+UA#FG+ zJZbO(6F`y?-j|>ZWR5ZPE3ZiemQkdkJpbouz#ek19WaWRtsqE)B{&1b9qA!U$Bhfb zsGj^lSgW(~5MPWQORhS{lY2;;Ooz%3;^sAvYbN@fAYWNN8`(go%gx7w$HogKeX^3= zWhXOT-Pi7LRyOE3z2^&e11~j$G0R$3`^E#Kd*%PE!eOTGM8&_f2>PQIwSHQ~4hH8o zMP-qXhrL;9X{*m&*&J}qY}ndZjy!+3%%T(f%PWP_qk||E3vfke%!%weBSGjz0YxkW zc8s(`Kl=goz+`{I^S;^Gi0&L%BU)grZsJ{>O%FW^VQcion(sW1E=|KN-Gg*~Q=Np!2B^LB4ldJx%+>tHZfT{p zY8~fvwy+3B<_sg5<=e|hAKgD>)W*$~rhk}SWl$Z>WY9lw(-iFsWH1w@yH<-#0l4Tt zG|3yycoyV0&=xXaIh5&?y-7n-L5tfBK(TPApuqH`!#bFY5MIQl!nj36-#D~nvKW7i zsiC@${Ys}e{zvdyxUOPxSdl!0;wUaU;^s%(02b>bRLB~xxh{MI$oEpYgkpArlL@QTCPF0U_;<8$R3+DwAs``s{QrG2X~Re{eVK z<;6;FyUJa8+C}|G?Pp~nP2Ll+`p#&nSRB~K#v}&U{%o$JS>ChK0%HO)2z`60e@=U( z(s9n*n$NwA#rR{0%lO{lNG9jht#m)1ZU3lsp!RdwR7<_>LGa$fsrRX8qxSAxr(+s! za$aQ%t+_&Xl`*!=G*Gds>ZyJ>*?tSt_fsL>M=)ws#;5JguoIE5Z2QjetGBD-mr6o-XfKD&+9DEpmzJ`bwM}%c0{>xV04kwY z;)d;~)W#B1dMFE-Iyz}r>ZjeQA$Qew+ef2fbrczKzrEMlagUV^-1)Dx*ItC&@!*YX z%PMu8Mw`D~Xnw%GUbTp%3K6Lp_+&7GUF|=-Az)sx6 z;AQaXV%n0j4b3H(5y@Py&Q>b7dUHSSi}HFQZQDlq_1b24<;t6`8&|6s0R4(p-D+pH zex%ws5x4UchQcxsZ999RmWE4P|J&a_jcKk%Uq67eZPu!=6J45acFPG=us-RB&##pV z4F2BJqbgQCRVO&^SPv7`+wxWKw94GJva;25Gmj`WG@CA;tl4ve&4+Pbc64h1!3=2` zy1X!ZtcbnfE%YW?EpayS$R;E29uEs{9NBiLQ+0;Dq|>ndn(u3qvT=Fmkh{Eh_lQfQ zct?OIwBKpJf_&TqGxR4m`C{|&`VH)pK9|oOvzya}Ret>Y5sbxyxw*_(Vz%hra5$58 z2F$@}zl+)~jO|+Ve^c}L_Tz15vg>5e8XoJ-is`pl*<6XgRlAAX*G%NW0|HL9!uT`E zH%GjZOP~GCzzgZJsNJ{kKExOSlbgba{VILoRNQkIWAElcvVybd(mc@jXy5z#eoS5< zUJ-5Cvn;CNUVC{@1X4>)epd`S(+2I3A24=XUz#qT=N zOk$=~_@QMxmyUkri*=RymDD%*+}Idg`jPtPGGvsk|3XHZ-@5F!+FH7t_UvfuzpZ#I z(8bfJA9}*TH>VZbl+==2|GK)1k785#r)Um+SJzP?M@jPMnA#3Q0(i zt9w}MIlD{ccI+59PddrKGlAc~l7fK63gXE)< z$(K`klc`V*Jz%S}`sjc`hLDBVyS@JMX!0a`?fouGRSHH6*%Z}q!v1eKX|^v3+lH}I zx4B$pI%7O_6L%W(wm-w%y!g%>Xy!0`6YuW(M=Hb~Kdfn@4I)h|nxjw!B%X^VgY^ET zErw$g0WAI%+JQJKXkIZ<6*q=kj9|LMR{^j8k zhGU8%HYnzTF$$!F$CclccnX-r7qD#O5KrdHbtM7qtxv=T<>e%1OZq9q?A}*!agfL) zG2o#R(Bmd>le>&N+G3>db&?5$LYik4vY?E()7!H5vM1#W8~qEaSXoVLcRRx~!>V@W z=iL>RJQ5X)hWAjd;;>n~QlkUk`1<@{?gg6_oNeSB)iU(PP|s@rI7KmWN8XRTyS=E@ zI8ghRI=rWp`aEFbYwq+T^i1t%PPuM#|JT&|Ywu7OFR1HEYkJyQdZ&F$et~td6~B4( z={AeSYo$4c0_v=7vGb{8lNicGUPuVq4=$^xkrsS`5pgB~%@{-=2l}g3G zXrEZ}hZsSyo4;8Po~@7P{j;t4>d;uYiJtgtrxRulp>fyS!t#{*N~3Yf$E6aZ*jMIF zQ*pWJ)RLPl*clL7gujMa{Gu*y%P=|fi<`GV!!%gK4GnEO!u1s-TCs4(3WdLgayrjd zsX@N9n-|j)&70>dOLtX0vdlK~eBTD2xQiM0xAlEfs}Zf`vlYK5pS!$1HWxGV;i5M9 zMyN7;+|*A}-Qb2KYDQ#)c&~Ui1gfwp-LtQkZFe03=d{_1eLl2=0L0#aq<@$682~ zQuAZ5oX-?<{qK{CLUeD++4*WSVcJ7l#a?wgg+VjD`rDTA;-noN$(G7y#u-hkY-SO4 z(o)7Ql@CPwGSs_QekT6BAlY=CMSFS8W<*dSed}6jCKHy5jcPMxL_Q*c)o%YO1OH`;rlRXX`f)dG=5;bx@@W9n#|e z>prDE_|6(~4aFJX9^CGwu0K4Wn(U`z>)Bu?dtvjalQ2|uBWS)9F5Id8uk=E(g7a?J z->e38Cp;6_*}3BTYK8QAaj~1rnjea%wNTm<3Bwy^t?WS887qySw(mTY9_XjJY(k=U zoz1EjdZ+44KXR!f^(Z`dYOfzY&Hzjcf}EN>&a~I;GR&gznY`zb16bJix8c^vUaEEa zM6c?*lezl8k&LuKHAT>G#GC3m5_Kim#ugjLajaD=O@cK48l#PTQ(PK9#4_jP2w>)d zAdn6an0xV944MR&pf8Ch#woZvQJj~;-HC&yP>M}YjE{UbAD@-pGy+$S%lB@lD~At1 zFK!u^9-l9<(H?h)*N)FzL%eV7)qCVeQ_f^;`b@}jsn}K#FH+f$!sEN`+sZlM{yoHc z$NMLZWiiHFx@8yom+VWm;?_FN`D%5hQyOY7`b*UzlR*Gq%b>pSsi0;Q)P*bm)uuLY zon>5#^`hUljlwy+>XP&MV@89TpZuzrpWZCprBX>DdIssnrP?3jrSdI3#btsFw zs$MZXjrP`b()QS6+Fc6l&bvD``tuK?LdcTa7IKE&)ev-Qr&@q)bHv`LIjQvE?R=hS zJzpT>sP5m!Ry1Z-x-OGt=z3t>?(PDl)!^mY-A>6vuDr&iRBPROsHuK>pfP<5AeVP{ z-MK)rYgbRWAGECeUmc zhpTqcf3CW>w$y>{>UWVOu2^;J|78aXl0l5o#CF#B@fYa#%(1WGUQ`c%K>YrZRuW%I z?_9cj;I_cGz^b#-0qM?qF@g?EU6#=6L;ce zx*6sH8i`zK6hACJiUx_SDOv(373`Lksqv$)y#^6HarM=JmIT1X?M7(lQxA>KLcc|M z>00kd`jf&gn2qxX>HUiW;?*Z{cUP55{&kmAI-*d;1m*4a0VB(cal5@%lpw+X{Qlu> z6?f#x%Swo~`-9_IkGZz{ojux-v7_fvW3Y?a8xl)J z83Zjolwil1pNyj4&Zdhf5RRl~HEN>j*71GXVcV!FEmQEFS7{4LuViLNbNxxzaOYnL z;Vvc!q5ThNvn=LHIJzHgCp&p;*Ub5y_FTdlitf+SBFtSxLe4Js>)S?$eQ$C;@o^^q zi{41tu+Six9|c-6^o;`p{R7T;($s9juP3%HsxWug!X9BE9`E9NL%0~|sbaNa9?8`d^b>~YF6fPvI9SM*pFG;arMr_n)npr2gae}Gr< zP6yQsZwW3WF-sQ_NVf zfJZdmKf+Ao6d3FX8W&5bnZ38~yBgXmBQwwj&3rfNWdcO#urVmjvQa8~ldK|6ebFSN zh>~^y`6)()ok><8z=EI%@+c|bV(ZOIQYuxQB*zI-Wl1hjBA5wCpygM+5{`U9^6Hs* zga)ikG7wOv-YBnR3A_pqC0Vve%K-vN5~N1sYf)Pf+)EM%`CS(bL8c(c5aJ|PfC5aC zlOYuzDew^81gf_t$4eg9%PSZ{;QPo}1DBz3@NJg)H?OEP%?jxBds8{u<(8 z>hb$+b>){p_SIm4q(LTf)E8+Dq_*w-Vyn~oO{F6c5Zh3G<+tKojQP^?Nz3NYLVQ_s&>Wy zI^bU4rWZDh_Z?&+cMjXVzV94!5tsVDBxo$Iv(OW|h>LIxSn8=5KJ6i}Vu`&U%2%-w z#q&18I>!L8bhAl5EM0+k{H1vn6O(5_^^)NOAcdOD^oZ_DN0L}-JJyC{p1>1ez~Z^Z z?752c&3gNJ<7Px$*J0aud3x6**`ssh&;r3>t=u{Heq`6+rS^vhE4847UQex2eY(ERYMNV;|Gsjp@i@4l0ZDu|EtG4|-q>`7Kr^fWkb6e>`|1JhB z&C;ibK-*4-P2F=>-F^Ef?bT+BkzjB2bmPpQT1i*kInDK&XPjoleYz_S=mZEXyl}_A zC1<4mzp-CZ*LCh1ZWt0H_8eu%R)FvettyKAizz#j+VTcD}p!_->g6Lsa z;#Ye(ejL^1s$FrD7*DT`>rlmEkHc|`0Bno2fixEL5;qvzsyvvKAqM~DU_=6n#nww! z9(UJdG%QZg`7K9@PJu*sR+0kta<~|s;9EwE5Xm5ApR+~<4FS&+AB-4R4-lgvM-PJJ z*w)O;DI>r21eW!(FN+91$%?befp!GvU%#7-uDMKX*H4To^{s)|-C!)f>IC4T_enNS zTgkL}+hBsQyY&^7yVWYQhdw)Qmr^(0V!v;3L(QD=t7y(HTgl+-*4!?wWVgPlW}J)B z1wLB#`J(Ok>P*e}pz7|N%M9idzxZzx$*&H0zUMVk`&Nx&YF{Pv|9I=YO)uzZwBmyH z;2wX>tY1)%uI+ufxG}7jr=m?Z)vninZ~EZU^0Ez?z^SU!4LtCYNCKDZzfC>A-KK^* z##;kvZR%m`9BvqSjJ02h@U|_*0&V+}owTMH&ryj5-Hx2i6?$vir|%h4bfQ$l((M&6 zjmOi6P(UhxcTF(Dh;UY^NZz1VA&&M&Bv2xo2M;AKRmKAZ9f&`EK~M!75V&7bMhxjH;1vs42sp;i2(G<4e~vVOXZQAp z#x7dmwg&cv>2tw2&k!h=BY^gbsOrTLT1Iv-SiT|Az3L|gi-?g3(hSKBI5c=*F4FVX zm}Jiw#i5I1Hc31LzKEa0eUjYvzLR$okM&@>q!Uao%W%5%Eim`FLPrc(Ua9BtIeLjG zpPMiq4f2T39)J9k^(Mvr!9%PtGv0FL9u{S*B}-CU>A|YOS6h1toAj6LfSJHf(H!i$ zR!~$$2qLDh>Y42D8}|ld`kq_ucEC6ra1`oMLt}+Rqg>j%Z&&jf#8vLr?NZ@gY!maQ zW(cCSQ^yO*HGPbc25x2PYoHe=)LGDjeEH4mi7OXH~_1r*CMpti*jc8;Zz1X(Q~> zgS(d0KO9(VBY-ge;DYee-e>zwKe~0{j2)d^bDb$1@!{>`Vb_k{TjaRr82$xpAP2DD za|cQ`YqmE8OI#)TnVM`jo6$@3NZ4`;5N5r)3IEPY*B9B#%69ll`_*M;^|N=-HVi9` z=qwWDp*@oH?dRurkP08IT6wi{lUsK>N7*pXv_<{~FFegEk;>8<_H6a7(A|9Q66^502Zhdd`7}N>WOJ};Lz|c1uW10H1$wmpS71N z(7>$do zG5YBQ;PcVMJ=SmzQ1DdI%^2%#Gr4VlAvxb!pLH*UL6scyty`FgA0&eh4rF$==!|r?zuI*(sg~pz$-coKKcjz9 zZ^nG<>%bYl0F<$+t!eAm_|_l+;AlNT-k3=bE=SS^XV<<1XuS5V z74)tH$29(#JT0Grtj)<3!n7>>yO#Q)P)?oDv#DR%5fHBXmYfd6ln%J%RL3-qdRJOp9k}vzEv{8cgSM+qFlw<86 zvpwUvAo8{HZS0&t1`)L0+4^9WUCbHQUKTx*OARyzmt*0B&i4o63FSu0oC-BYK$ge)+*nHp#dVZ8>i&VHoL3 z@vXWE;~K^z6vH5wbn4^2QyPH5oU*n1wM@z_Xom6_&RX3SM;x_U(w^OAs>z^ z&5YZALw~k1d14}CX>Ry}jMy+5luLiLR(Jm8vJtIG71jb%Hv z?J!&B4w#pHL=D0F{txt@Bz}n2$t-N=V|^!>?YyV&b6SzyDE8@b76eA!D=8&`5rD0i zN8q2urK3g0SG{zpmwb>RQ)^1MhGapA#G6Tux#e1;f?-S0r?jUdi>Z zRsym(gJ)B;z;5E|5|-i+Dc?w^y%W!A5Pq)yg1n26GNXOfruh&M%o}g74Wq~9&zqlE z3TVb;cX{u#UqctMbi~>EGk?tS)llglU^xS9e3LSOO^xwvv2CAhsK>F5gI8X(Ryx7c zEQ(^TvU2Y-(hbM+IiQ)rV*n6p-UFQ;bHYOU*LH>#7-L;`GLv(M2Uh4@CP)XKsl8Ol z|By%i)Ix(EQSq0e=PUkSt4D54mA7ie+FEgwb>AB`v>(IO(%gk?rbOIe|H)JK%b7g) zbL#xsN>u(m_e7+YmVriIWnVCJ!J~ni-p3I;kBG?Y2QvBm+sfLR`ao@+w-OC)vydcJ zr{O?(^J$lP;-hUBp;;9?IjDa@GoGoc@XP`APpwt9eA;#Wy0;9aorEbv`dufT9jOjJ z(%D#V6N=JL6rgvcG9`M5ppQuG+9$#Wo4>OIOK6}L^+F18lyZ7zGw)f<&01`>wYu#4 zuCyITXSP|Iiu9JM*4HULDoz@>Q1%|MYQdYuvSn}D&(kNj!MDqOLvV?9u!%#q@jcb| z!5GU162m=<5)Wq7og>Sab_nVA5U7O!E3+fQ5hA@Nn*u@MhoVy^LKO0F&~1VDXt!IE zjq@C=3;UE-pBNmKE+#{f8pVd!?7`S_RA%Kjl7u5FNzEF6NevL2So5lt_Sj95m0z{n zm?YG@~WuT~oec4zE~s@+|h>(}1V?Iz2oja$p5 zmF5rYwac{fa;f*JsT)$I%PM2OeJWS_PU=_P`+jOj3w>vsHFon*bO8YI{I@_Om$f}# z^xF}Vr>Q^1^b}$@?PYs)4LW|x--I!-g)8%(IecB=+Fy|ZHL_CK&zCp-+4dLZ z%GbE4zfVTdVs89z&;!qsB?a+DqSJ9N=ApiK z_5Hp!hjZwOH!hyv}DXpbH7VcyU4Qx}mr{@nyL9*n`ve z5T}l*g56-n2Xh^SnE~dwC#gq|N!vS+GUYMVX zFP$T?=LWe5yx_UGvm(A5tL3&tQlcqJfm8~-kp`(gMjW4>9uOEibxt6KxYXHFqRV{_ z2@OTMOTBk2cuf2b$i3piCGm#{!~6SEaRKY4C@-ktjoN@Ha5 zPe9JKLGs!w-(PuErsBAP9!BE#pw)+NZpw{p)@mzz|7WYwb2zK6({`hvzg#LKUl!2(d{I>wUX2(G=gsdg+x}fvJ#e9q+IGjQKZEM}yO)r@qzryE z>rLg{%wty39j5uxYPEnOFDoWSGlRfWlo*srr+ki{lm1-33LVO^>sImv?s|BlkWalT zO?cbB+g-_JEn|s5q>!(6z35(+ON2LV3!T3m6-j(>$QC*)f{PEJu`=7YGvvzS7amz4mdh zlxjJ;)!BE%LSFFK5zGn<6}+O(LnXd#9Km0dh}0`5?RlC=O?m-I%b3<~ITKaR1wk`? zn^i0$->NE3aOqd9vGf$+S&5HJ8XA%W_TWk)({;^YlrQSiL8*5{EogOo)r;X{ccoOe_ERc zxoMCs&%s%Woi(&kX|r6*ZDVXaw&qwKC`Pr#<;I{&oS?*k{Tj>?V}xQ9lGK2;Q{Rel zje-cp%=%HlddVvIExj5sFGzn(N0G$DK_t$UC0Xg1uz<7(wjJP0mt;K>OBMetQkjDJ zM=^dWH!_F^=I*6i!~^7o5xSHl96x{6>f_W!q_w0b<{ZTb1(WAnj*b{T1`jh(Jh_l$ zxa(cyF&sL$0fa2+v+_1#|2{YWr!9MbTDyhB+HKbS^{uUjOav+IdMs;ASy%qE-6KWK{jIB@IL=!Ec^4y!I|68Fqv4{@g=2x z`(yUhXY_8g|ARGy&GOEc>~;EPt%`0>Wqr;L2I?c?J(%X@Fl&b>3UJF`2pJG1XAt)!K-lGfJRT1jh5wq(n; zY|FNsD7KS0wi7$C6FYI2?UZr0tuKjKmq~6TM4DKKxm&tpEs@@jWxcILjXm*4ffeivm@-!ES6nZZEzitjg6 zbm}Cl6uIgej6GsX#Igw;)Y%&QPDkLfXB$GjZfC9VaQ5#Y@s`XMMa)`2!obkMIW}*Q$ zfxszmDd!}O1u0?0WAMzCciJgU=c>gO+_YFuhIv4y$>$$7r&=;~ilhkvr{wYz6{kEW zc0SfMPw)L_u=9V~^*_6QrgdqK=4-v$h}MLU#R4JB#xPH|u&lEDI$a);VzN{vh!K>r z8U8In1K6WvpHdrVm9tSKZ0BSA$N~zrS^+KPki6ch^k9YM1QTy!kVHb0oJS~svUes3 zHbR~Y(L8k{A(P0W7ua9$P|hL_K#)v<*jdeDpm8;mvR8ziO~B5pyhy4+N+63rg(?jB_Jcv;H|X`71xj!D z`#D@~o~VJNmQNiU4|)b+VQpQQsOdD;X5g1R^kbzSquyn`lNK6E{PzXWP^?c|>gM(r zp`57PujlpFk<}LOKOq$cy$#~(2m0#2jujIX3M)dm|C5R>s^!_0yQcHqjV!}CO zhBDMy;t1xuU>X#Cz1hu#Fx7QEx%8?^K(L;SAy@;JKHGO0XU$$dSJ|rGJ*t?ufi?x* z0pON6rc93=8xjF!ha?x3@2YvhlAc*Rn8ESjR@0IFi8x<{Z5M#5uD+sbWv_8onn>;HjzrNh;RR;4FP(bp%1^{icvS6m%D3uMqbvYVm(2Hdan_iAR zVTAfyGPlL0Lw1Pyd^c$mvKvAqUn=@j5?Bw4lBO&L!BeCsMRLli)?h%RU>F(`=U~Yt zSdbjC8JJ;&-HAISUO8uLCH16Vo3%=IN@a>A%{uLLp$Ha)TO!E7G&2jDvq^X_u?~h_ z(p|CSZ{YVB=&&?Jgw7#ko5-$}eIn~0tIkz~g)Vc5jUlP59DqF>?lS@%DFtx&aJ_R? z;&sA;f%u7>Y=U#HP4vTSQs|HmOtxgV5tB{Q{D>`JZ{mwUfH=-j#ATc#fD5^w)jmV; z)2f~1r3P`3rG0;HI$k&wdHv&fjaj6&Kplh0LSK^k@DQIJ@;VAQXIsrDfgtp5r~wqP z#$$`vUhUr$LY>aZ`XeY^!6GsHz0{Vqg;b9OU8#O&gu#Fa%x_Keg5J@%rqj>$NB-9A3|=kbX{l~WWD z^06U0Znzs~rfN{BpbL6>%dws+MnUww-FkAc^0eUmS`z<;fsku1K@t`q!I z*x~)`E6Jb3AY`f<+G$#K!d)h9jVI>%S}1j4=7%V_ zNq%ComCiw*VJolrEE&5NqLrMHS+xxbmZkQ@INya8A;Z$P9#cFXdH(+a*OqX-kzW6njCfbRK*Z+|x%`lAG zer2rEb;E#Su0@7=p`DM%)IUJa9vxOI{z*^CoGg?W)at8w%4a-l^7a3^WU17a9<-Mt zH%RYR&hdjvwLt&H=lxtSWdXV;R5KDAaIs*lkr`UYp)NkajLa_`YQ%~!dan#6+K=2a zmCU?v)SNAoKGs$dVeCuz_F&x9_=u^cwP8fk2he^gcsgO2tiXzDgeIWW8?H6nOvZK& z=IHKxtBx|m-3e!4yPLYS7@TxUx0inBRu~NGBwGjlJ!L)2etl*!SlRCed2_3yPU?5C zMyn|Kn|iIVD;_&dS95Y;uGCPCHTP9=*5g2`wwKztxj%4^)mx%(N`CAwak&8l1)yO- zBfEG-8S8kq7^C-$8XSwf5=+8+EAo|vTd8WM9t}t9S*0NOtCEdlu!xmDqCY{bWYT+9 z!81r+ydao;2-T2t5=vkL!_1Hr7P}Z)dzr%!5W~x`Hmsto5Y~mTY+HOrwKC!I;&sUZ zIuQ<^WKpvz-Z0~IFJuV$x#+MdLgJJ#lMoLRE<|w?oxE5eADE&cTM=G@z>r~rE&e7< zy(Ke2GlAfS4A~&Vz{&6v5mIv;-4phrYzO`$o=A`=qPoyKNxa3Prp7J554$ngYJ6X4 zL7Y&&!MVLYB}f;c-7A}us_|sg+{?fhW${ea*4t6Ac=GaZsT=-^OE6`Flp=x}T!L08SZ;rIWLDO>@ zdi%9HG(z)6b*Pvipdys+>rYHNl}kQ~lpI!Zx1FH;`naky<IBw z_Pr9}h?iOEZhFO`VUc7&R?rml)k`Y&ZL`&4p3=$05Dv>J&onj+F~5rt-@j z)}h<9`lHF{((KL=8m3Af{5tqS%F8E=JBM@vp!aU5!6`~@^h|8J<2i7AN+pwX$hFco zNZezyzCWRVL+=Is$uO;MMb}kb&vkvi>jx-u8QLzePfS^8g8L`?gj*J3hyqXI|0RJG zs~i=01-mYtLXyo&=+jL=Bp+m*gjQWv7bcOV#xfU{1Ta4TW_9sGqO#jl1A1;k&H_V z9h`hSaR-Q%gdmp?ECj%s(xPrqgA-wC7X^skQgE5PhWXiS3YBZa6yAq!cdfj)Vw-)J zE3+Ok8m)vIAGW@DB42W+Yd-Jtyj1_IYx^O}J zGv>?c8`=Q|g4=7)^Io*nSK@tyT)${}R{N(eki(566a}l9w@a%ZEII6|&}ysd&M$Jwb+vU{lDQd5f8BzM0E!1^J%~dN11?;Cq-( zz+I@a+O2)9*};u^t_3qKUfPr|`F5hImdmtbJNVnUHj)}u!F2k(XtjG?Qz6_{_nj8< z)?V37Zrgf(F>c3tEaxZ@(dwwTKVPZLlxa(a}xM>(H_51p|kZ)m=RrqM_{7f5w1h7dvuH6OeqJ`>E%Z*5fQLARw;1T3l>g@Ya z?uu@rMuaT`&4M+>l7|B~X)FDqab8L&2-x}{6Tso!L(o?Ny*fYhC&?DSHc+pb&Cw|&#zpbbXz zUW`zsh)x|Gh;Q{m#y~qg9!;;GLF0~Yz_@2?5gMXESsLq81l{!=F}sKUtiVd@mrp5e z>sFdO;;5&xX!lNTcOs7KIu|!}XcW=JKwmGjmmzxEtCr@ODX(bxuugp}uz7b+-2BOV z5%c#9$Ft(?7ZO$va*imjMwv5UyyZ8e2`hpQEJFWZpM6!M*WO<_$>^1`)4tg)BxzuB zd&~&JtXWNeSaVd;Eae|JH?E<&5?`^}O@BXTw1zWYygVLHZ8OOCx3ABQ>5)j>GviLN zl60K0^&=aLJvBe`ncNu3QIzS=tL%6=svU*d(f+Ny#vW7cjUI^Zi7tIrz5E~)KV)5N z4DpKFh4RI@ay;Utzo@4mF-4()8U-i$x!%Cmqn>(KPZq(hWbkY>lR)6^3#Aw;ti~tM zr0p@Bq8?T7B_KH(5KWMyjX7Qnl8H*t``|=9y&ktU+ieKd9{j3u`1oo+W;tSYXdwpUBWcykm1I&By#jb(3aAucpgF1EHNVR7^x zpjaM9fiG!q@pv`}@x@-han+<_^u#BI`5qb580^nx<2^$r$XhKIpI*bosluj|lV3fU zwVeby{waU$c+7ThDlGIP;)=TMcCIIeYYlO>kLVu{V@Hokopt(TX1jKF?a_t>ffip{ zn`B0{(EiA5Brqdh9Ga|0LV+s@3IzqLF~ddzIw`gAibRr>&{_4;wIctLO1XGMg#(kc z9}6L=m1H^M{3H(v0W27Ot#RpL!D$Hxk>ouGHV}q>COcI8wuF`9MkSZW_0j<;d6L9^ z=l(DJ3?aUfUoi-0eK@8amno;^Qz0kZ#McUp(D}cZ1xbg*9d`JKd?yYU#rbp9wMl|p zsqa)Jm*Y;b(ZqYJAbK&boxnwcB%C4IgEOZ_R}n@&r9XH2+qkYf zhM*&~Us3r77&@q&^tRd|tNWsMd-n8^mxEk7esMH)ks0siKL%bE-Bj2Sc%W}s zy8cLcR9PprbZ`n+ScWX#I>;V%4%hdWqVu=?0bRLvL!EvRxU2R&K5c*C516fG&A8B& z!Fe}$j*hN4q(iGjLbXT>7S*}Q7~!h-qxS2)j6QhelloIktBJZdc>m=VCvu$|Joj6y$R0^gj+JkmiRNqiMI&IDvBddrBrC}E-Itt_YLxJ72Dt;zX z4?J@P*qt#V!L@tFmKZbVm4aj@*MtTmze-SHjc4|ys8c%5c+GF>fNFpTOXDSH)3j@u zD`*Bro59&+$#_rJehuA(Z^C(!ntFFQvthRDimnf92S5y1K_O>WWL^YKV}(gX$%c@c z961j(Y1XKe2E*VG(5cnI79?^EF}4fMq_ohu>oa`-NDD$M-}9nCk$ zv6S=VHI{R2l1)QeWTMPPOh@$A5i~&TQ4%c@FQG?81a4^22|VtTzp=*Y4rT|6C>_@e z{k#)?h6th5Q)C+pTGzRNRtpqEb+% zsB#+YGn-Ua-=$0_8YW;gA)UL%va3q#fsjKyoNXPg}{&^(6kGkZu1nMIgH|*sq-UNHod& zYNZuh7-UbOF*;r~7kxJcuqLA_sQB-O0OBa>uiRXTI>$oh#55q0HF5A9$6n>=+* z`s$4ClJ(-Bt|q-?{;L0!&sVGFdY5wqt!O;tkW<_7K;kiNvvPaOmGkapFA9JCGy2ES z&xwIy2s3MK*9MJNxEAgtq-23+_lHTI(L!cbyiK@0*%qx3)n#2uR)l>QKQkpX6<(P% ze3By3cp}x}2d0!#fRsJ;2Bskunsc*!SidB9k`-d!#Zv%Kuva)87)GmO8|;+t;rDh3ApBDWMQf$;0C9Zoh1OB?4K}EW7C{-SdydhFUqqaY|qie z*_?Ak90oHW24R3E5uke7PPhoZfNV}ggYZXU8>FzrE*0!s!tC!bwP6m5i9MgtYhT2B z;FHiiGH<36$DkC`9AIZHti@{6_} zHwFSsJr`-p6YR$aTT83o+92Y*lffkYZ>>2G@as&{i^OM|fO5uqb}rxnQ86VBF{ z0!j+Fj#X5GT9^TC8!}bQBUooSxe9zny}Zt_bax^-NZzNbXhp<|FGk~vAu`Ef2UUK^ z(NS|)c{!^-h`RHLs-aa9lO7k#%XZT}*J4PbTiY~>5^Bzc_fhqx0@Ir@&EfQB&)1qI zk7k&<8<0G!c&efN*@M`E9Al7TLxgH^E)%QcI<-cn?pWs)BvBlIfTLFVR%;hgM}xNW_{z=I<8Ol5<5bi6AnZs1@T*P7_3No`=p;4 z1_5B3Y!#{UNC&-iJaa{CExdq~XAD{QHSsVt*=D_;t#Aj)we_2l*+ri3j+>jT z3#Tt-ZPW>enlQgN{Mty;@|nelv8)iUs$pZC%(^k<@$G=gn-3hE=J@+xQ@~~O} zg76oC3@N@lf)>^r`z$S0rQC#~F#^C=cRHHYGpE1+bxZ2@@r;b@swEFtTQb%$)V+HO zD~eWQa-DN!#5{jhFtN*O4RD^t#u?MRd;O6yI-k@$TMm?J{NG}4&BjY6dJnB3Uv9aXV$W3|gXNb5VjpeAc{j)rYORmq*T zD4F4`YQ6lOzTn1l_0iwQha|i5_>h%dw`h|@aw%D%VF=_C#0|1qa#O5RQIH}c60zP6 za>nukM3YS*OHme~7|I&w5@fM?Cc){%Maf<>L%2|kKR=Ux$TyJ<-a!oC z^i~3;&y~26P86K5t5xE)?unSILTFpc?%AVuf+-13sav%hmo7G&crD2Qfnw8 zz!+t@j0`W9z+N9aV@jNDRU)^mtwlP5n9%jnIjTO5O2ea-dWOecM?ED;bx7IAPSu=g z>X2hm)5*Jv(MIJ)j^i12BtwhTYmq06#65SDA=8d5mF@Mcxsn<--B;%;$17`7R$&8h-dwFQUUqeeSZW@K z+t4=F`ubHdlPeV?mN#ZuGgd!pg#D1jqJ~#3(W9Gyo2h-h-5!{arfbfO25-Gq?zxfl zqxXz@ve)TL_Z?6_*t(1E=m(>@sqwiO;=i!%t26hFknNi(qc1y6&DN?{%D2T}L3k~Z zDlRd6RKXMRZl*xtR2(q zMq1wiN(XwkQ^2PheE_&oil{`r@S3U#eV$38YAFd#X+i3xamsCV&`SRA4C9P^r`Wei zUz6MC>_0`c!JU{tWz7ebgVb#|Tc5<2#JhGdcjc^JLEH5^UEk|^wd=L6AHuG=M0;3! z6n`RnLY7#MItS!2v*Ar}E-SWosu@bTvvLIcV_RU~vH#K=B>-R8CLurlCj*fpKwWZ% z@C5oBP~m@ih7Oh(;&$gwAnjKIA$Cq6KZR%DdIXp2;E5p;I2gi}G8L+$W#qReWKfK> zgW(M2?ec_M8_PfrHdp+pB(|6>iP+Bj{l(i*Vwz59b^8k4U;~L6^19291x%-%v3Sy(SWK$(6g} zDi$s0FQYrl;=Puiv~Qo)&K}VAylua6!T;X7``(@A!d;qnz)LF6Or;J*bic^t#us!m zn6R{!l}xsjeFRuqk3D1LduJ1jwqXv&dSJ#_>d>wm`xpsDzf-0d`*iD}QT03k>>@ z^+G%f0V9YP3(WmaI=Ts^Dh@0fE16Ej8Yl;`J}NMA`)?7sOh@DXN^`IzIOW<|Q}b{$ zG{@!nu5mpa6c8!;5x38-D#10SfGLs61Be2$Fv6ZXCcm`l3CKSPWU*G7T_ct9>AV4b94?1qrG%BO^iL~tmi zReUE#if~U3z(_!mvJpYPPHxFC+9qK8BN}}@@Lv%KO{PVg+?Khtt7yv1# zHSy9}21N8MBRbhan&6b6k~xna zix%Rppgl688>~r{YSr@q(p0Y3gM#!WGge03f?vTg9xfAcE5w0Im4jAwyaa!pIkc%w zK#fQyqS4_GMf4o0VRVs}eS28B18$<4s|`5S6N7ON6=XB^w3~9A$Ek#yj_H+9x`5+C zIOM6=YUNB(kq;Pb7-y#TN6@&QK!suf?EPZYt*^&3|Als&_BP^ImZ8L$A#61h{Mw?8#aVXSjy#X%K)qK!yPB z5<~Jq!3HzqGILwbE%}wCcVQ+dKp)X7%UNU*}IB-!3Z}}*H$+_jnDJUZXTy{2(pqrtR02?GkJ>)BP0Lc*TkWIxk z2@)!GbDTDKBg#R1OT4^XJXV8{7bKGnHqPA=vjNeB`w(8}qql-`X13Rc)lyy>{fXV9T(ZP}hF1Cz>(+^|9CR8gJiO%cgv*_ruGXr&vYv z*6V-TtTX3c=B}!|_Jy1}ol5R21*u3WsK(54Ppk4nIvNJo&UlrEUt!Kbk+S(H=l~7( zR?v$PrjC%eCU&{$h_$Z~Gg*^Jsvvxo%qCN_o~pNh0kl)?eF+|SEtpE8Wh5vg#JMdpN)=c+^T?hAG~ zdB%-28ohS>az98M)Xl1c2Ww@FWdstVYC2{jtM53yJM;jAKQ@X$Dc9XdW{kvuWWLry zvs9Se^RWJ~$JZEj+FU_V&yagx1(*ITOOi-V0%W2d0CzhDkrx~fl z%9Uo)cKe3hFY8`nn9zqoGx3eaiOHF(XUq+6v2z9VDQWt2o9W^Jv1wfIE6-N`mz8u} z!)p@%{ILDYLZTc*{#PNPWi2EXuDA$TA+~C!F*-Q3y11by%?z>}vjT#2aaDD4)UbQK z+{DIe{D1#4Q%qZpvB=K)aN0A>*I&?%=(_98|?8@*s1 zC1s&)JJ#(DC+5Zh=-V%Px|!OuE8ndU1Twp-=mC8b4rvm_wSMrmX(F5Vi}h{d{V^^W z5u)`Dw-awaNV$@G+e-DsKQ0hbqB(y0m3F=>OIAL!Jv; zqNByZ!j&pASrlrzx*_>fCp|lts^SE~E%nBbUmzRa_~yHE*nR>)-h`{b->~P#XCsW} zl(Y=#Uj7ZAosX&e;AJ#j98@bvP1A=){GAd2Zi?<6m+_`H&@*^USGTgrB4#~;F zU1!=b%h{uTiw(<0)~QxS`<6z%ctR%G&d04t#Qryvt~moq4b(3ZG~=i}cB(tB`D>@g z(sr$9C^)#qbj?&emNJOr2{H5|v3wATM6E>T#t(+d6d zSR4x+dzp#9vHY4^rdWo6qH7=P73n3~wzR>eSr}e2VNp~zrk0>rxN$RXv>(@w>FpDa z_BOqJNlbe|weR8bi&;@Y$|!f`kywu=R+1GO~Z^g$sdi zLK=xtNEw=fV@n7Y7Dxs8CJYkAB4Hm%u27q-$$N!YH^I?DoE^Y1j8KR}q{WB$AMB4( zDOf@n@DN84abRo+wL;!*2uETH#8OO6`eD38t*42N;RWgJ4hWvY{DZv!(OupdB8o&n zQ?1D3ROc6hzO~-0dm@`EUAnefnm_+7L>3!9q~7`%aweaj*R<{GqHgtY?e@m$M9=c# z>E`?i2O+KY4ombY5&rB>s$6~s+;zFBawW(NOp zc*eS6jltk8>!kJD*Pc2{`|=LUPxM{6S5F+fLHE?}eP*tHknZGHR~#t)yfhQ}zl*6W z`j`v>)-bx}$Lrz}xTM!K9_`ymWPj=E$;+%wJyioM!GJ!r;8Z`p!>mKk23&_4t(W|;|e*L2;6-to7>SX}P47-}iyNo0!nCCLxrpvVKol@srF;&K*B=t$0monan3h(i#r0SFaQZ!RUVD<+;NFMcJ=EWFzU{ zab(S9`Ng4xGiX2!;ZHr=*I#ggt>>4~VRv8yr0&@|!`%0ThoHJo4(}z}S5Q0TtwIWA z2AyE-&cny> z4`=J`zuurvB{%z97YTyf?Y<4Mo*(vY)AaW9Q1!P?ANz6j8hS*ScvCy|ow>!*xK+7A ztLrlmK4ZpBJ@XBJ(SmP%U=Ihc9G4T=PnDE?;UCQqb+Zhpy9K}esQ%ydX^@U4Y9Z^v zIo{LtMAtXqMvClH$Bu^vov2MGLckY9!wH`qN6=BA;`NCf@t8R!Vc8TmpBhgaZiARzvH)US75{#16+!A?f`}eOOWaLCVo5u;jA#=xQ zU07w*-n@L`?R-j|GWu*4*GJY|b!VdZq0d<#{!p-{d*m+X=%BimQL9z$Bk>Y?IsQmG7P#$i z<{Tqe^z)_>`6=RSZpOZ~huPC!JlcN6Oz39+Wz~Flc{g7Fq|+7Q9YkPek!=GkZ_3L4bvI8WNvg|^$KUl z3!`y{TSsfA1J<61?q1k48jVJpiTE8+?d5!H`B&iKIzTkMCdo7Z%ksa4}>6uiCb*{(mtesqEm9;1L_VTi$E`Biu?LssQ! zW5*>{0Kk*Lqq71;I;`_D;ftxuIthO<*$HPl@dmLv0_6aB1 z+=ueF#eDCVB~t0JSps@29k;G>(y&x{_zCNEe{3mXxNEN(rT}h~bGpAG7fE?lhWSQL zjYH4Ltr9GeNVXCm(lhK|8FfNrD zSgc1943qY9wB<3jcvak`nc45|N9>I8KYah*J9OY_+e~PFB#?;!#+8}8-A>=QqDhe1 zr_EV+oH@EM=+9kz?cJ+$1F;mf*L1r5suM%wtp89|Y)`by3)fJ(>yoZ}yB_O$q3aKL zb!uWLG&`&To?^I6g!>%Mppf|xb380UQwfJzB9b;DRu@vKCtLCugB{Oekz$srms1W&cx!y%m=kC)HO!pjN$eQ>Nny^Oj*RGY0y~E zZgqpSXJvf*j5gL=S4n&13wCXkCa;t?xpo36l2Rep+e4R1KFcF|V%@d7EHl0Cyb(HH zGTN8WP?PnEkc)LyF6}wpnQ4~T#zWqPE2HDZK3RjA94pZ0Dp-G!6PM+w@&}m%y(~_$ zAhPNQq7j4P3~@d7_wL<(k8Zee`)9rxPnzWk-ReyQ+50o4nCsXtxc_Xr1!@Fu*Eg-A z^=J7v6C=YVBNq6ZFIe6Zn07K_M~5dGc6$qbla$z#B`5Noi?1Cw;_11ocO<+NfWmQE zq%5h370bFY{q^g6^ZCm5lrdsyzpqDRA+o??UM|HV6kk;i*Gm>=e?O{LhfxcEy6fLG zMpzGqwJ(f<*aDwW;Q!w<6H770#QSp=DYRjs5lICY3UPAhJmO$zn`GIsT8Mq67})Vc zZze+L+*7!Wa9Q$BE-%*#!47G`LAOO#uY|Ny3?J)27^0vkUm|QB!1K*YMUU7R$100V z-YiBzU7YygTp40yy3u2qM@jqPar1 zO3YTtq2ByFZFk)`la(&6$$?ayic~Ch zVMHs%UyJsoM-02(e<+h4Yb3oZlCgB;vj1$Y%%lv8-2bn&^7>D#l-Hx}+{hpnf7h?A zPjw^5;yZV?r-Mi{ef~w~jp8BG|Fj~-gcN41(nw&eM}`I2hzpEI+&q|ylX$@Hg(IprUv15@^;dcU{hc7{< z=PGI#U)Z|5uWE2DvOz$#;gW=lo^Tx`IwWqiLcE$Q4YL64?b*cR&L!EGto}UVsGzKL=AzeMDHhC!#xwRiwJsQPYH(YLA&U!D zXW8E}T=I%v1-?AVyssPSj9p9l#@c)EQX=pD&oh@dI(7Z3obm^z(JdT}sd>GM`r0~! z#woSmS&_A>+c&7222#@tnzb;J*39nYVMjIS_MVUFi@7F}5l3C~NoaO=c&E^{P1*L< zruMsI$?J-RW2asotCZ>n)etq2h*aNka=h~u& z4}zZ1{k%8tDl=n55C`<5?Z0>?*gFL~s8{;}bXAvfr|KDY0@zk4F6JFFQpsX2D_P^w zw6p68-&@Q`uxH? zKFq^dD6$a24OmICCdP$N2ltL+N}d;*F&1l%0&0qtEQJi#t@KU^w_27W=mG3IK26D0 zQUX5B?>H!I*Kl=9Sy2`j^i`>2f+bR&k<^87PZ}d7Qx!;|M%x8Nlyrt#DKKVuL4tk2 z-jfwd(#zk}HHGjl8xE^XfkZBZ-w4_P51x%7#Z37wQ?hEqbsnx>u8gCGdp7cbojOai z+`NhwQ7iSAExOK^DEWBs$Z&or{0iJ(8ADJ*0ngq zJmA8HrZRx6*zeQfL<9$VeTDRy76BX)f2T=J2dS--m=ntr`sn?7ROTH=P0|T z+CQIWEJWNm)PxFTFMkoT=B7uz=Y9?o+%PI+lmjPYM*jfCOsYR)!CbeIEG&c1^5-6- zd$gAfRuN;;iVotJ>+2LV5J;zI_1JYz{3sJ~K-(h0!1}#Paj1My$&eN$My} zH`8Fp+mvXXN<2l&u0!U+psbb^D^iN0fJzx9n06ZDn`lA#2}JsQqgpSY-SJ*^iYYzM zewOA)m_@Zr7J(E9UYm_9#C9w13^GZDF)5%4v9kwby}KBVl?> zo*_+lyeQ(8V_K;5EGow#nY(i)-mzYb%6b1I?mKv{Z%~epP9cW^=MCd!(yGk^%S+7D z%wP9RV<_>el{C_@Vxp;(-StZ?0huD7v$Y?^eG(We?tU%3kifB)q7WbHytfzG(FP(teJ@B zqDdzj(Ft;S{@M{3A#S9%qRrcZO{ZDB!1=WA217k*(?OnNu*Xkmg`#HY-bHx|G&~{4 zz8a((5#J1CG%a1q=Id#@GTuQk&J9*}*H}JoML6m=R15&}pZ%jJ2E+&ob~LC9VHYD?Tt832ElAAIs2`V5xkSRxL64_fe?_*`;UyN4_~MT z@6Hr!C2db_;KE7p%dB=hx?>YsUp?GQJd^?DJ zFo+fRCnMR|&g68ye8d{?=Vq(pGp(|A({t7fpJ0Yc!#a!9R=m66{#N^~=*;duS~A`f z#0+4e(t8uET=)ROe`l$3OG4a?*P*aC;Va=9g_9unFJuzn&;&&nwqbQV2yW2xlaPe*Qd;>m-W+7W-7l^7+XlUAFV ztv8?6qqom4ILWNpT2uiYNs4v5^qTrgGdJ>gl^fLHPNVxwI-~U1DJBD-v5g*Y=3N)4 zk>u%WJaskWRFWBb2YOYA-KVFaGJH zma(~w>KrjmF0ZTZ59KQ-%b24A^)=bkpQrv&W+lpGKVh550+d3d+$T0On_7b4aC=Ht zMFbP(SLt)Ljurt1@prZ9gW0$Ru~CHRvQ&3V*Guxe*C3sZDgCJVs;MVS|-EKQ2)@77+`3dfCf_1r>I{yh`W}CH1u(A5PE`nkz;Ky-;PdR_%5XU{?ND@_&}- zj;~!~wHDM~w7AP^&4UtK(N-tc;n7Tl);krO%`U%A5|w9inbW-Nov1) zZH{U0TgUYK7*q9Wm0v%ijBZB2xSPDBv0pRy*}<#$v&*Nya>cNve((;<$s!lEJXs); z@>??ume1eESXxAQeAWI13PYvms57}AxyBjG<+Ks@Vf`EZv9fi>qH#jmTMrDTvf32` zOwMLc1o*k%L}zUhb?^=^McLG8BReo= z(~q~NmEH`|a1fBPzLpXwRxt z`gdFEva21Gg}XoD-F7Vj@efBQ)SipfnP6_pX5O=}MRrxvpHcY>3XxdWF?JfOBa9H( zSM%F{(-W<08}3fINB$@IrIq8Ud#`j~5igu(egf#l%A1HoGu_;D7s8O_ z9tC}D5^<0cD;Tn2xLqJZvN0syzzjFp0oCF~uJ73tQ?)wJq%FmGoeB)?$JiYda$Mku%={APX z|D{uEypPO;{E~je&#d?2j-2;Jyoy z68{i(#Q2o*Ou0L7w4r~7#e9N1XuHqoKVWaSIoy3-T$7W=ZrA}85 zlRWF+J&ey>t7N`zu3AOTU-cEFeH58rOM6da+seN4@;tVhe{5ur^-{`x6kSf^sN?P< z!LE2la`#iU>Es|Dsv0yVGeav*KWsjF;}on1D-V4t_~I4A!3_*CN_RiGFEhDfeLmh} zBZsd-(x+~(ELW;lGrq2MQ<}B*Pxn^l##K`rYJa0|V-UF*Cf1!-5gVJRzE?MV zsxQvdRm22eMz638o;eTS#Db@8UHee2kgm;@YBS4c)M0AOG~aU#-r2$H-Qba&T|QMg?xlxg{eNUf z`;m5W`ro?!t-h1JsPX|E|G7dMswr|rHs{ntO=oXOl_JNJ8(g&^IUhM{+0w z_3I;5h_GDE@Op%!yOF3F+K#MAq>EoBs*y-aOQIgs)XtCQx(%%-mwh2-CHl2!r1Wm< z-LE`le$a}3&>S}&GH>{Td=~?qUVrhKZv~NTkvghQVHu&IggUU~SYRs}?MEp3qGmX%l^a`YhI1lMO{?S@dCCyYzV3L`$_6FIl}Fnz<&$>AYJY@z zjfq%g;-?Zrm*{<#S>z*BLXUvJKEU$<6q0}-j0Zp-LYlci=wZed<`d+$VbFwAk$Mqb z(jvANdK4^1znDqhEhL!<%$@9ma9i;J%*-I_oU)_Dz7jA1X+|^jqlY1$SY9FFV>%8UIsYh{3@A-Jp7#fki2YmZgsa-Q{0TrfYZ9!dGMbr*6s%>(irNibJ+SJX zx7{*66JM>pAJoTL-TtO3tkxzWC_9d-=G#AcM@6+}RQu<*qmo})PHOL842~b%(b{}| zGJ0B7A4(=m^;~Xv`o5KS&djR(*D5M_wBjKw2{W@%N^Ugsi;sP|)O>W^U5{Ar`E7DU z+KG=__xS4e#T=tRM{cd7j?(_o@0JsH{mZ@k7OUlbCwwLqU16geiH#Dh=QHHf2~>dw zyWT4{DkP}^=ZdgS3*1@ceaMOVm*fqh!3vYPjAV=bp(hYzLoATc@WYT6Fh_3UddyZ$ zB;yI6Lr*4%kD%c46y%jAhmagdGItJ6{4F3jS(4Y$)(airkoGSySRp;CiR)B$2S!01 zZ&$pXgSpZXOZ&LmWY4c^FgS4yeWlM-nnxb@8uz@-T=lh;QS10ahbR2X;RIxM0;IPO zTF)GQuIB%wdW^wHRwg}1|GnR=m>bN*6_Cihh<)qYoO%E{rDa_^IiRb2^5ZJ{-&)g( z$ujv;lb(}Q`X?N!R*Wj=g)M$hV&YGpxjC?`rAI~Zw7I|iT*ip^-9CK;{hI_@>z{pr zl%<=7mHb&`oQ#PB6v~aSngFAcTJ9yjbN9gR0WJAQFNGV@?%I)48w)c(a3I)_K41- z5EL#WZefz=5T-HUwi4hGJ#<36bH0c3Nshygn27xS`0=YYFJ$MxZasI@(~oW0b3Q7R z`SqnJvB20{s@{{a4^76?Uscupk=`dxhoP;1!{O>6JBR$YZ)IXad+juu$!F7EFZ6F5h&U>utWu*HdNvBNr_< zX6);(8d?3QI(7I?dS?Oq{dys3GjRfy2jaZPPL=J$`RuTBFo~2`>iSfsF-rZ9!)A<2h%$~j(y#J@<#uzCnR_SM$YgiGyPc1%+* zSl7_C2h~__OB<;-gC1+*uKbHuckvgSW@n2ZXazCzcYsC>IzX&uYGgMGcd5UM zTo}$*JV98@l3Q~wMaRa3yF)m%ghl6oD~ez2Az@CksCgYUgfvDt{vB@isyK|cwQ>bwA?u!1C--l9xEK7S zx_D)=n!YloJu-etUY(%gll$on)*sFHYb&mCKvz~W+OvKXc~m&~&hzm0^_k`EZ&OQ8 zsuk&MsRXu$tC3x?Z18lxs_IYpE4$P2ed_NQW-7JdhILn|$DLCT zpATl6Cii}2(`+Jnx>AZ=rb=$zwVZ+XGWTj5^w|jW=<=uzkJ}sj7^E?|y53)?IxAz& zXu>=;{bJ?hJ8v1X%vGQHg4LY4$1=%*67Kq8OCF9wul4i#3p^WxS>YpHpWx~6U;v_o z0!=nS#utW~lGzJvg|8r2l)D$EA~JtRj1|5dYX!|Vv@KOCknETs3Ktt+PQnwuN%$Ti z&`1*50V@AJ5O8jTP$@M@c?RW)NVT(L+`=4^+B^T^)M2JW;aw{%lm-rIYxE1YNPpwz zFR1kDj}8|zXfB#j^RttsvJKI6u3+ET9beI}Za{WTn_~K0Udzn2k60Je5jL{N##UYL zUi)}Z`o#NBS>q}CGOrkMcHL>BTX5jrmNMS;!9@16jSbp_ev#%+zuHhcZ`IZhbem2x z#T;1n#VOy4^t&nZT6?xMW39Skj;ecg`IsB3+h!Cb8GosrmGxyL(~CgnmKx8YoJbhl}73Q9pRji~$4Dk(qDZ z3X0GRn{4;Eb8Nu-Xg+Ejt;`3tdOCKna>HVCF&!x+$1<-!oZRy>$wO0sINsCyy~^!g z?!qCJjvXl#hpmMhdyq+EM!NODz6;lvtp5Dm88!S~?bq_wpO+d-^~QJ1uWs}{_%>#6 z1GJj2hJ@w_y{2EK&nW>C{j5AmHc$9)2<9evEX2nW5)1Q~eMYlpm_@$PRe27;6yqpF z0^!$4^vB(<@=&Rac(Ro`2-+JFb(Q!*fadVCtQMFO61L0UmVB4in+~r)Qe13zh`}|% zkFv71c(CH?^qu3mKetr(Wr^RLVdl))Xm+&MT5))UYUrN9!T5{piSJgL$J%p^-$reb zKbC(2$&!-Ah?2mhs;--7S|7dfeLWS1bCwRjuW~q0{&X3TXJdEernuamYxVlRDZO$G z^i(_e`e){*D~`24YDRvL?e<0c{!Xjn&0D$m4>Fj9xVhP=IiAn8lb^fDg6YGDY|2D(c&~juitW*%)JD1-%uUHmHAidY zRDuD%u)=qH5(%0Bp2;k?tL#O;*2Is0Is7WNVm z;&rO#VOGheBqK$D?;MRTG@*k&)Xe|*O6yHs+wT9!Lyqc$QW2tjPGUO93V{R)3|b2_ zR!MM$lZ>GzZCEjz&aSzW=VK&1TXDgEG8Cto>1H@`>6+H<62__w_~SLE5_}(c?JT4THrEBkk|@ zEX>$?a{VTt!tf3BqxR1gG>2_upU z9JDc_$wYNw{V=+&MpSyLqI!1J9T~RbP7n|38Faf24y~yemeagsE{)2dC4Jgl+5XTBr2VjmNHo8P+?0V7k z>XfSZSujH%*_$Et=S;#;+FJdu+dnEM=&%C4(x-$x+o?Gx0PS`Q+F%8^ceiIH^JV70*r zF6M0A%Qjk+4_mdm*Q|L@qYCLhK@+lnlrA-A(V7Yv+PJU%XN$?yM=xyD9#B7gkRB&E zcaNU8x#H}b4O)#Sa@Nh4SF~%)San}-mS%_Q=_|4b4Mfw8Cx5%Je|-C}sy|9*Uwgc2 zm-QQZwR=tUQ2)U#g9i#9KxQv%beulf||u1~{zU)yStM zKgec?lmcBQDnM84*k19;;#*re$@jn)DPlkirlP@T8z{_3aO7204HuIF7}PL^u7Lh?q)o*W=cgq887UuJuvf}sx2-gjl^gqe#_JMdxen? zd~&LH{idNuEVbcNSw8!L)kx^nC!+`bD10>M!5USJjaSFs{)yF|ZC-cOdMa^HTU~WO z_jgz3{O`@U>cba6^LQ$qv9{5ZGxxo6&w#gAy+bpX;YsWhaW4EON#5bF#0#bARryh( zu|(};f5uH+)ygG}diOQuF=O2hbDg(pZ*Wy%#&Yj3TY3(jgf*OhSs`A7nbjfv+JmzY0lS7cIlRC;y;+@z<{!j2i^vW0&!Xnu)a^JC}@}g8N zc^c_3Zx>6-1{FY&h%4x$f-f&+$#%#f!wt-DWr%5*!w6E)G1UY{Q+g_$x*zmV^>m+n z;+DJg0ZU!`z_K!u!BTEt;WzS?jX&}#$L+o??*Ad}-Q(jZ&wFvr&bvGB?#%AY?#%A& zRkPY%?MPZnD{CdKE|z4=wq(n;jAbL+vMp>G+t^@;Fa}I8V8Fo!8w}Xt<`8I1$}Its zkdgo;3FOj4QktY`(xiv9Ng7Ve?TbD!LR+D^T+8q`e3}?otyT3-sk## zzK=1`QLvS?bQe#?N5T7KvDzDJWI0)L!^H@In>w)@x~0z9s}8~_c>d|lPVA{QY+ zqO1(fw;b0DdgSbI9z9LreEs}AgX1D4d&enhsoWy^0AXlDzy`+ z1^e?kUHftrej5w5LdLWblqzV@WP`>%m<6N;j2$cy!8isHt9%K5QUZwGX^|zyYJTWP z_M(;rZAWPtk!7LfrFt3^92S>W6{?W-5bYRzw*r_;!6K9>Kw!mPN(?T^zA&g==V-eo;*$X4p{sQ8a08 zR1`A`_!+x?5qo%(ff;?lq@?&`i~YuY%GfV`-O1~B-CFX;jMjfvc6@AtO(y*Z{Pv5> zhJSo<1)?M0ke5P^7R1o>N*T!ijHRMVQ%(<bMLm7Jug8$Ed>`*`Xz4h3W)-#VDx>cC#^-Zxmp%E=8RMKErE_{O&bkrA3 zY*UQ5rtMcZmDp^QDS$$?XvOf&3y%X6vp@|)3oD{d+I#|MKsuVPNxK(>rizKBq+}4L zd?EXf%?<1ftsb2~aZeS%j}I$oFp!9I>ai=5cPI(fUqdfYsEXKz_Bv_)oa^~zQ>qeaa-F$&|Sl1plmSqXX*84U148e*gCiTT#9lsDe_B zXOO^2rK7REP-z1IHT5G$rvZG#uv`|m0L1Wbfaq{e$)*EcH};{M);V1EwcfQfRT}0- zOU`Cl7p$~aF$)m9ISHm1>DBV9SkPoM<~1_S|4bs_&VU#?bkFl!K_}N*?)hS#7c*V^ zKsQmcKX;L@NsFtSx9)DolVGab5uWed8n>m?y7XEN~p>C-c=eISff?1(rnR$aHEHoBmsQ~rX>4;F zJAm|W$;>{G{)8Pj+J4Pmt1|0n@M74(iMLCc8|^^* zUdN5BVw+`q*5iBWysjC4v{Sa&m2K?h!^l-46Nx;D^-LgWoHug`RNxD79Ry)4xSC(B z`-{e8I1t1rbtRv@w520o`yqG7d6_*GI%>3S^ZUM{xrUt@)cn*zZVW()L(8kiJiu8; z!r1dJcvzl7ejr;_x7MK0_*6|w@&+%mS3#j zLIhLa^W7!XQR``^?)rv>QYWY}(zJwIqw>J%N}AT^(3e zx(-qCQ`+O8`k@fq_2kEq-cB1Qk zQ$*(T+&;^|`ypd?XkhK=x7)+LQX72ZPRIh^BmJgQn=_@G;~0FxgfpBOoTZ+cYbVaXys}f1<9e9wRP(J?@egn!9>(4sLL;6^W z@$MmH#7sAGXPgB*n|hopyuV`FC^D05Zr2gZzE-!t2nIE@b^!)y#yi)swCcTGg)Udb zo_zv&M4I8X4jgB@F>B~`+PP$SDTJ(7q7VtvOUmtvSX88n=oJ}C=w0kcQuo+q_3CbY zHOZ?*%}~(j(C7i|Nr4u}xU+K z(9+~@1MLzG3ye+OpCd~T{XwVD;$B)uAS!FUdGuGo-{7>P8!2p#u`c=SU4xUV?M)&|*F?Jk`y3(PR8zYee9iX(+ z!{71UxH&4bKg%EJ3WY7RWu;+4c&1r4KXT)evFY@xrYDKfHqXy$zdQo-9BpLBk7>@p zdc`|jj8>ra4^Fv@wkE@UTVn-x59$XB_|yO={X+ymxh5-86t`gpu!5k?11f>-x~ zb~JAG#*rpu?4kr)qbfRj((vP5J=Api;Dau+?^x9$AJj~vFNSp981P+JL@%gmJ;l#v z0>LZ3sGvucbTM3vwzH+~AE(n`0#v1eOs_PBaF7G1RUP`}BhtL#%RA}s`|P$S_dO^q#H z?8N#3QS5)^Bx0Xfq!039UH4+s{meaOJjg?fZo-V!j}B`?-+phtbS%k9ixk&&RICtn>7bX}v9PA20EKUzd*f^i2R zTL%C++`S$v)iLQJ5)-JXy5T1Jto=};KOL<73P~9FOGwvN1{WbEV83M4-V4W$!harD z)%4h6PdvL0A!C>fsKWr8NOD#Mniys_MH^AUIegqH6iz6p zz2JFxKZ07xpHDoZrbm#I)~3PT2cN*ZjIC<=%5eUZ>*ua&G$!UpF7{&zE=Er#KvmD0 zF~Fpt_V%$zxVvN4(k7O!lKItp23bLlS0;7-oQ0vGF5@SUpvCQ|9%vqQ9cu%Rkgx5) z?tUA4avAz3PJ{XEak9HuAOgQ487y`KX~PAVh_o>gl+-agn?c*a!|`4MmBo=x%@!+HSPXy0g@gLU6Ne1 zfw`~2^I-$BdPIr8@hBl#H^$lB%-}2%S^&fY`6Gni=j{-qwS*f)k zMq7zd0jqk-pH5DeX0V6ZNuwjY9Z<%VodE3kurH}cm3VnA;MT8bxR!QiGljc^O+)1B ziWdk%pkd*-QjAI}lZvs}wDd2ssR(=9sRp!=ErcfZVbjspBUGt}cF@8;6%I6(4yTK1 zTRjINEGI1$06U0)aW8mkw3t!qz=1L#dk@7<6()KBQ8+A;zYMtnG{}P3oiuYveCX4}66s0vF#cIQOgejE7Vqrb9crdQaqPFT|NkdkT zmD#D~b_?sg2CXkSXsypj57K&>ts01^~-)Pg! z4bd>>7lN__7Sn^ppyCbI`9F(aB$fq?IU{-o+PDAi9=F6pU zB;=Qgcl&q6eP#G@nw(OnBh;3mgaVfa%@8z6+eQ9>mFr8648SAzN{Rmz-ZE`YJX1uA zC#q1MQl3Qssr23Zi|K$b5E{B-v>5ZFEj4FunDid#1u4V-0+@Mgwh#`vMOnJXN^l=H z{pd~BAn*(M?pW;K$+E=T*c9UraxEviA0m-KwsiKBqBaiwS^o9-~W_yl&0>}#G zvEfN!&qD!FCmFN{$`siCbxDH99fk)+m_R5JQV3Lmrg#iGM<@>*S?^^8ya4I~egobX zg$+_PRGcOaLpQ4T&4=M%U_c>+m86Aq6{;b)1L)FVW}xc`7mq#Q$&({O)haw0-Bwzg zM|(h6n^fSZO;qB67~dfb2Xa=hn5pZU*!_pLZbXxil9K*86M4wvv&Uo6(_W>9CXGf* z_BDeL7mkDp*6KiL(OhFxOQjCLih$!0TAZIvBYZ3P5-T50@sD@t;2)p_s2*&KDy_9o zvCBhBJbVTBtPW;;zvl@>{QZ3_6R_8C*D=gpL0`nTCf4#PZ~!~fS`M=Ar@jn}g@oES z-S{LjuO&6O!yjVlUPk>)oK2!S1z-q9tzd|F`6qD`sn%MJrzh1xCx2jcVBIf{z5wTa z?}@@zKe(w=(rubGv9Uu72Tn7L6i2*PuSrq7W?~}bmEb*lkbDAI6nwKN|5tr7RJuTp zH&fD!zDT&&mbboe;$Ncb0GAxjLQiiG>cwTa6xiz&Xd|OS>x^U@{_hf~jSiyJxNryr z#Wa)&Y!zkuKpjL)54Z_jK3vONw?l}5u>9ikRBR-tq}Ky{`J0x(erxexo*Dh`@!;s_*gUP2t$QEIo6JKXzl#ca}LNM;Yz(OO1O2 zfz$|R!@3HVZ*32JxKMBcz@YXmzE)&*AQoWe(sZfuRATXz^DMZL8^=SQROBL_$|N;? zKd-$M4J0BDt(!X~zjOhM3U*Xx9stIe>Lgm09Yo)(ZOrF4@0vV}0rgt2d8CcomxfKO zALUM8kRJzTdOP^)9%=Yo!ziR1?+lxSDzSiiz+k{pr%i=YAYe80 z3HAowBQOEB7=b?Ojybssw180?sZdhYMY8!~#}hV-VC;2godWDUSPik~L<(J0K4`P! z4W!kY0fQ9Vxem65)Znq8*0IK6kzkL9^8p zOF(K+P;?qM#s=WLl?UROw}&z>Q%68in+zTXkv4NG2*{SV#?sL(a0{}iFY=GJ^1H4( z(NYXr?s{pENj(ND)*e9)RJz>Nbh~!sf_fNn)_}tk+at)@#oc+fXakxYb)$C#jdx&r zA4S)*ye%1p_imflx?E*OWxhGWOa5FdKm@1yklj6qF%TAU3~I-VSE9sheF{~W87+?i zoW(THM^}|=f0}jKi+cu6$dA7UmxHP6bF1PG5&_HOn0jRF&1vCe?e^!`f0v3)v9sC? zTp@4Lx+aVD<|4oHb*y&)a~U>b)vo|T_zxPsf>p=4-d$Ku7vr`EEgG4L#C1i#=dI1C%3K?6T zTIS#_VoxJUhTBn7H3aH|WktM=Fdui7aR{a1#1r8FJQ1zE2MKx-LsHG2_Zd6NY6sXP zQdamuGp}c!sqP?BAGzLCXbaxs81qPf9VyQmr}iudgih|14$a}MXK1?wR|1)jxTp3+ zu5dg=p%T-I@%_q{2)}ZwHOls2!%Kd)yyIG`M|aN8B2!c8N^i9=E7Yo47Z(7|3`xI0 z)82&78`Tf;le7oRJT(ZYff{%^MTITh%D3=HpQ?msK>qEndnnG+9i>oAcC8DiLYG^A z2GKN#m&WAoWS zpM~M(so#J9Gwwh|E5D9J4}bo+&Tf^eQox7F|c5aUX{YBvfrd0LvcM(4ae>kC(mB_{1SsA{z(QcvT(X;XN=lKPqwVz$K zWci|HnZPk-9Y4ygi(6RjTgl*@Hf1>ZG~z6K4eX`j2lebNE&T2)wydC3yLROwX9fe` z_o>plWqyO5Kg!o^B}SnZ`KPIBUkI2>weMDE|c zrIk0+rV=(WhLLTJS!}gOnK=W;-_VcCNA-EQR{mn4XPXWl1xwa25A>*FEN}F6U&>$7y z7^n|7(NGi|6>DJs&o}&j!;1}n2oC_O9;;`*DL+GtMSd2Hcb%1@o}aCX?TclDNV#Nc zs0K<0ESLonH3E}hrKw{O%ZsJPzp&USmQ%fkGP)2$aR>|x{5HZNkr}}UgmDr2L};a) z;AE&EmZ0#GrV!E*utDOIc!>I3Hz1S*=Ao2mUD=7L0W4KXEgzdNI1Pruv z+v*ir9^c@NJU*A=vp>}AfilJrZEP{6P_O?scIQf7HmYsNz+!MooZohyz}?$n%lHJ8aVunZ|wxe zO0;F!G44!nZc|GB_AjvbmAllYL$Ll=*^$1Bknr2jz26H(4S+SGni8DRO36PBH#I!~ zL5eL-%jR;-Ob6EA9B|B7njN2ZfUY0h?+JnGy-@pkDQkCF>8CdVRm0S4Z{C8<5qD(c z$+&%Cn!Q;``9f2VJD6_id}~!J>hO7XoTIG)Ifc}}Jt}g23|WQ64TGq`eh%wTy1zME zAoK#6aD2`6Eec&N^f>ViU?rjYDOpZldX&;a#D9fgZUM{$aUqirX;|3(UM%7Viok|M zkXr{CAVH{C-GmarOIQq%fuJg$P&0Yb2Ba+NHG_Kom$VNZN21_*>>4oJ;Idi!_kq^; zFnJOD3p?KzfPp@M>75&9(V;ps+`&s<1awXc9;Y369s zI>Y`jTL}J3>71^arE3Ay11Sc3N?P@`YM|kb4taddwg0E&c0{KDVY-+9?ju?V zgTB-0Q=xFmy4apMqNtTXusva!qKD#DV7r429a!6M!A^+XDYE9!$MtL-Hjl{G)xYxv z=Nw-+3Y+-CX_>M0u~8R-Ht<@|Qqbu*7PZ$feuO}o^c)r@>T z60D3cyJs=SwDqP4=QnjdxBfBYZl{4qi-iN5Ow}`+irLOhucV9c3E;s7nh7IQa%$(~qcVa#4bu-XeO(1xz?*qu@qz?%<~ZfD zgwarZ40wWkc@B07Q-_YS_ZF{Gz|VkZi(-U4j#aEl00c0lWij?S`Ho+L&)@}a#zOyV zD|-C*Va@`kEWka-mrpxg#zrTTjcB?`%YbeUr2eW{9T7L=q3-dz?iY~e#fjv<)t!F| z?ZTo_P;6LS5bD2*)Br_}5Q#jgZR8-8s4(~xj(CL^ssO?!^gy&O2@p0?3L@iA4gk3U zV*A_Fo&%*110h)U$Wl-%K(&H9ki)wma|H_dNoHXPc2usOWci=+K{$JkwHG6X3ENb? zlBvsizUDgGURCXGoi);7G<@HVswYNKR=Ni5Ik_Q4jTi6a&KCr$i(8 zo6ab9(&>qR>9Ll)#Zu$;1pIL)Q2EZl>5>2M(`;0u6;a9Hpu zVC>|2;1+mLN?eNcqZeL7{oX`(3r<>S6#7N*Tnn#b_?4vA&Hz)LYC!Jg(Xi3n${LH{ zm0OyQgdg5;cQBvuBi9BNarT<*i-M)xDLYm&9&r1Wq~+vJMO5pIU(fhEH-JI->5KvX z)7Z5h-z|uh<3{|em76w%F~-B&-@0cgJ+lG>?xUaRQQ0kRh6JdEa>`dyylSf&ZY){K z>aMmf4I#zmq`oWS#JF@}V1LL-1#X+lHYP1C)bE%5ike;*Zwjmk$bn$SyGU9;k_#$k zD7aFSOS+Z~w=~tx*%(ilYL=YM6@ay(uRaMx8K@McsAPv!d)exkw9*Ns!z+3fHJ)k# z`Fb!MOr1Lf3VFqf%G{$YZ$oC*j4{7Q*?4V7z$*nZ&H3r84zBJ;Pf;K^b2B^qY=vOB(=K2e}`u58Mt*P~AuC%)cV>O`O8O1K~B)19FiH;d+o| z?nTYXhBS(I(Xs%7b0Hf-czU{h|3UmrNb&!k{6aIvD3wh%3$g=6Ru!Cscs4xY|0J8p z6m)Mc(TLwn#E8v8jGHfFdwSVMq`iT~fVf-X4y)hjwH4@*i8S|lEKkTt1 zOqyhg0%l1D?eZmw!v=uzo{vRS(Ln2A`dpA7n}n0KIb**bU2(xvf8V(H=hM`E$C7QoD{ zbAE3$*sQ*+dVZ;3imyK$_hQo$z0I_)!UZ_wkvXI$a_blO=pKexPMabLhF9^8n&iVUc@ z3yopfFLT@9+wxjOr2D=F1LAb<#L_pwOj%NBx#^>$t(lDH4SzzA1F+g-KVlWsDegitv0}lYrKu& zmEKrD8qfx4_99wZRLovrY*7)XDcYc3V(fV3)qXa#8GIl+cgO7+Lm6L^v$^N*y|E*} z?50&Jh{^?!jA{V>g*~p-DHGQaq+rhoI7~8MexZx`HciYj)`cqr#4?AjApTGe<#BHZ zQ@2Jr05qw{ZXkW7OVc_S)03|kZu~+#7f0c~l>h#LDbpQ`h6dG0NZD9>r3~f@U!v!{ z@|U;3-GKv)9G!c2()0zUUh3b5r5CZyi>Pu8p|U5QFu4Ra7kfC7_NsvmGmq)rZ|iiPjs}x8Nyl-g%qtQ9G#f(k%fm% zo(en~{D)dC946|GfdC+Crr5edtr&zrhOFQm5mBK>fKnm^eCQ2RPz}1J9?y+;WO(y! z>WqdVzb@;OxA5gCEIfzxv( zT2lrtdf~4=(`CtjAg!FfXqmP#30vVJkMU{bMEe^K0Nwv1ig1Xf7`qn$rgfdXtJubzY_{J&0gzuKVb>M=q;FO!F5Dnzdx5*hUmtCK|oY7@KYkqD+tnE^>}( zi$P*!AF%Yo(J@=rLBYJG2~4glLQEU05Z5>HVZI8SVz?pG(9*E9VGR&!kD-VCi_i}E zwkgKMUW8Hr^A+M3Qqou%gt)FKkWdst&^5$E$ajjJN*wSMs1yaMoanEziH`$Y7{Y** z6(ufk86Ox?NjZqj7#13LLNH6lgd$zIp142F7bMAoya>@D6e|gDy?d4fO^72}@5scE z|BNP)%X|H?&+PolP{RGI>)7_%Un3+foR{UdkUC($WUm|vwMx;PU$wxhetu3zYlkZB ztJaR6>X`X$nBa+PJ^#09dkV=^ z_Ue>;c8Vk0IAZ zY!VY-8M+FY^E2Rq`Y#Jv5vT)!W+0tG;Y)o6h6oubeyCts5WM_*=a2k2e53!tjfCXj zXz~XrRKfZ;qmLHSLvEiavC$+JGe$8|wVRL-kRuunMUf010KrN##MEHy}UIkH+ z@M3#$IC-$(yvD;oPGElMMEudIlb1YE#4O7;qTQdD`dJ7g7=6-;?P25Wj{8E`v>snp z_4?kBW!2vp=x&cyLu$IG)k-CKOP9~G8uOkTeZKW~1l4e0>*Y3}Nbmn;*!o<%sYd!I z`_?v%oK~A0fFJTjsR&X*Dd<5BJL3)3{&AbM`;l4w@_zt0GZ+fKE^U^IF-bF0Emwpv zuiSGig{kV6Wj-vuX`}Hj{&!OUkS+%@J?*vo<49HetTxG=T8r60kG~S}7#SI}uPrGW z4qUo4W~{_sk4KhC+p@uwzPi_^Mw2c7`Xk@a80W=B%fcM)rkqp;uIZkM_=5SzE*i|B zb^oF7b;N_^$a8>Pn_(c=eV-kQB&_(C|A}q%P53tX{xxAEs`QUC#4>SbHXR5nZaR*M zFR4L4Tva}4V|??dD*GBWE|c(357~?ImpuO>_(Ff#@GBuR*xNBwT4{GvS=1tRPPH|f z@P=#%kO+cKDpJ?zR12}aWSyWU@;>r0DC{Ix)Cb)_d{#x5=>5ncfY*tu65v5}_Tgi3 z?IPvR7Pv9U#L=k`H&`Y`RP>Nzt52CDBO{ax-5U)N6{KnFNIa586bPxW5O>Bi*IRc* z`wHC{?-O7azW~X)UBFXN*ORoUyhHM7?M-M`8Q>Gam~sgO3C%;tQ>^+2EDx3n1ye|u zLq}AfMWHU3EC$tdWbse{9V_e7^T=hPhu%;ty6*39?*}AuL)K)3P}gi7$qZH~9w?Oo zcK{m?l*Pz{$sjvfE@^xmR)}d;dyk`0GpE+YdxN+b3p8h%$L6eO(70Pg1`Z>|{wB9G z9NX_13uVrw3|?H33JrPu<~WbfvQ+JjjEuJbrm}e$G!9bGFhG5VtCTCwsCxIZp|{I> z%}983Fj{mgys61)ZPkvI2-;qh6EkcA3NPHt14}f$0^ATkQD9@iNR!VpQ!Ftwc>%u4 z_V%B|ydzABta<{F(C^-5@(12PO9bPqnsVMe^RHZlparfti?#S4!24Et*wPHEi+OWH ze^D_+3dT8Tqz;RoMXK>V4PBMROu9bcH76VXxGr%J6f!4}A|xu{9s*T?pcU%|*Mzt> zB0x+FKN8l9B#Z=-Ab=1DLdQc;Md<~H2xJU`63XI1ieTvg0E6dSrmJva-OuEK>9pF!k>fRisv?hy`v?L>Ny?u1NU(~4NS z*b4Y7HKnOH$Oss*Cf9bb>dWTINE0ZqAL>1s9aW;qHT>mhd%g|iahD}_!k8Ol-k9P! z&BBpVX+B?mo_)pgMDjBX!`7XfQo5zzw6pr#kKSfPny7vsa2O*5)8p(k=AN)~ue03Wc}C6j@?C~KACB>B;O@OU43Pt=SDs{7wf7Sc*&6ZFtQWYI6n@XH^;La{65_MKzvkp->| zyE7VgbR^i@6qNHnLnoDHU`YA(iOTsb@9kf~%+*0eE&q^;cc6m@f`XhV(YJFj##CQp zH^j=CGkiFozBuKHAI5+>ts3J!m%|-b6cq%2oE;ctb{bwa5zK0ex2#O3 zWjXdR=&Q4j$>xRJQr$vxwkAbOiUR6bOy2}s0H2l=R5Dy(NbziHivwf(82DpGF?J!m znzy3x>&JvtWf4PQk3ypdqs$CRd2=|tOiz{icB-s*jRL0^+}`|?>*MIMaIMMy>yC%I ze^mj_FWmg*wwx08U&HNEq0Q$X*38Lh2oZwjM0^e=abrM;+5b_Unm@r*#PfMBCpwf+ zG#>ByRzgLFoOdaq0;%(!ZjwQ&A%0wb5jCbc@SHvZO!6O!N)zlCN_jPaa(jDcBp)SgnPGd+np;5@mBc_VvMymz;vu=|(7jXzo_@!3o$nV8zQ_;uCN?*&c! z-aPmZLQCVm=&ssR89CU9Ov7Vmz><%Bg5n4)vZfCzqlOU*PiE1vKw}XrM~+zzAa@mv zATXf=sbWBK@hWsvi{K@>2H3(+i3}VHKv)`|OP%zzMo?2E7>L(J+=J$LT1G4;_VdEB z3hxJg4poK(!v&U&zfGEA|8q2??U^lAv`cURs z`?-nWjY?GZUw<}}i>JCiu_CF1pq=?|+_=?3IJRPlZNCHYHR_Fa+~;4uoEx1(7bB?h z1ek@*emog&UZx?OkE2oK(iLp~#(0rk-B@-Xg4i8RO*^5@NO4Jz9M$gWi)Cu>zB6Eg zxvY{mRq4)QUGbN+1A)c<0P2c^fq)^IV+OWr-X3&N3}W`i!^|yg;1I#QmI~ZAt49;Z zjVcGt((NyW*hXxa&>IBcb+cVW;4ia8q%bkHCQ&epSOjn86p(Ab zq+A?K2u+-Vn@fvd=_!LDS-c_*;R8Z|f-V6=a|ugO5Nm?=5n8GOEKXe=AWo+LFdJ7G zA6&F|0b>M9Bzh6hSyneswAmqb9{>w_dr0kwoGoOKbdW1%EF-+Cm$^xB$l}21i3)l^ za)atx2?B%&3_g;Lg5*N810Ghy^0+?aj1o%_P0uruvLAc$(redbosusoAC-KCQN=II z7kvzMwn=Pbj68w6gAu)tT&%?vG9jbvKjf3GrK92E0 z%w0>D4E-esObsBTb;=LY_-I_P3i_k4bsm0hX`qCW@?fp!dN;ZWtsT~DjGyaBEo=I* z%g*rKf7R$0!@S-q>|l=fcBM3dN(IEP16{S{qF>FoNpOHV?_v=4EGZ^#xh{l{Q~zJ_Vu6x&1plbqBo#zMr6G9yLJR8PWO zRS%X?Wa}u3rk9wQt1MIvyBa<9vJlb%bg~$F1(}2$5pvr=gJy7mANj=qXOC>HtDJYE;j{de=fO9b=` zQVzlV-_0RN|1{^@c48;rbb61J4|SHu)}EE;*gC??_+ z_B@f~i-8X0+!T`$)gYuMx+A6b_sb`Nb(fhsfjFD9tir!viLAoz)^>qD3$2o>GOdl< zsoIE+p?dBF5c3nmwg!kL$fX%O)P8=J9ULq%G-th-$lckdqZK{=64X?O97;&-ar-B4 z;xJV2z7%bI{+h2N^ONF+7HyT5zkhz+B0Zki5AqA zx!p3oN>C}hX{U6Iq`3gjrxct0m;yKSjcKSO)iIujN?29Qs~g$Um^ZSA4c2}gSIzX` zFH>oD+L^d<<@c+$?47e@IsMy_l%AG4(`UT=!t+!;dMzKg z#27PdrF=!VBrPwa6SVCK7P`N$nI&TLyW6cJbCE#skxT;gj@~Qcu6!p4p@AQ@rO(O_ zBVVkE$hxax3+xKvW8t|-h+QaTCC|`TrTs~@VJHX?%YX28Gv39DU~R-)Q1V|Wt|b;W z%Lc6o)`WzIhN^%^L}&m+HrV%A$cWG**wpw31gPF=U4+0<@}&pjMd^Qvvng*U^aS}n z5So0QJNko+5DtZl1Jxk}h`c9?#JXvw99%1boP-#nsT$QV2IrSVh6d=6>MOckAT@>Y zSD3PS*`H(8dlKnVwDujumUw5qq!h5x4=^C*fZW$R4+T(8pcrVNs`sXG9^dN=u3RLvZfG%L{iWYWi zBfBwCLFzEFBzB2Eme?x&`4|B9kiiix;8%>=pUgxeXwPD^E|Sk&>SN||oH>K=6ug^k zdOuo97#m{-x-mUI-UC{d3j8Xxi3GM+dTT#n)&_>*_R8&w{4?#QFHb*)E&+3I?T!Sh z&92rY&jI&|pBq*dC8hDR=g-Xtl27Eg*Ed{B#a6HtA=TIt_FtYhdzC$_j5!~yq)n@-myv4U>wFe7{3U3LJhGb`#ygzk<4LACAU+`=m zFap4B`}1tdO2rR?yUt~EvNy0Khc4@30B-?-z>f(?1JHWS?BSh>yIkUkyi0EG=c>6} zk$CMjV|^pO=@e!!6fm^-`RZxMHab?UjmF4tI84rM{YPUL*Z$EHM)$qOx_yrN#mgpt5_DR*ag$4zJW^UonGgP8y*%IBxd$fHY!+ykjl^LmCL4a6J< zSOoGFMX{H**G`6$>E1;wPAJZpwKY^Jrb{tR}K3otL{jxVQ9shEbCy6()lE@ zH;@nf0%i))ENu&XcQniC=OPn`)xcNois&UzmC7Dc8xRF-dU6`-i2Gs>z`7zi2t&0X zHn24d6%As-qOh4F9Ox-jx*>;HQK3#qOnVRpQP}`OA}SUTH|Qq_0d4@nqW~YDQdXHx z!^K279S0z^7D+w|-eVO1QRWXCmR^$UgJ-N(s@)}A*}zFc(5O=svV^8V6jbs(o)eMg zP(Ur+TaK-jyVw*en5H#sg5v@+>E4aPO)&c+hBG$E=f@4b zV<`|%dpr%Ja(EQf!QD2x_a<4c2_rXLURI-xQ>lWgIjUD<@mRFDk(EaE;3B7##a0hq zYDhk{;3S%fDkP6FpckC9RZ&YF>~v5)5NDB|w4CDnOd$HUw4-B}=2)2Ev2z7GuSEln zvnqEYts2MRsGEOfVFIik zj%5`!kWrbN)dE3H2aJ>{-(Q=kz58wDuRpW@`i$Zgv8VMU>|mYO=TG1#k7H`vQSc{z z4h+RVrg{Vta-?BsamfUVSFmHSaPVBQ!q5w_To4y{9}9`vx}!^40}8F43q*Q_6bV@# zEVZzI7{`K>U?z#t38#>E+g;B?qk@6cK0s>-#e(x8QY0rNRnSxLz86T_78Fz$ni+5j zFv8hL^TBn49<#h~)nJH4HyUwFbqVMO30t@4xisv6G7z_H3|Dwysb!ui!vlgPB{_Zl6yip2poStNUUvi1ErUF!)u}zh|R`P15UQ6 z`c>#8>NCC^7Vbt7m(!}zDq~>gil0vHV*(kGCd?e&{3j-i3LJ%PBMd& zp$YKU0$7}~dN~nsL5NVPB25IR%TTsT;T+nrfJY%!MStkAaDu3yc*(K>d5+7&>PnjoE9XR;#?eTi`TX4yH;|mW?z@ft zhr;?mR??)d+Ar>A`M_1$B>QR)5-YTpRX1anFZ|Ug{0eF#gchN-L>gZdC@C@$HA7U0)tcUO~9g&k;CWky2-Y{EWy8mAmBEl8h|r|MXH~GQU%r<{H=5&c;CqW z)WrhAE8c{3Ac}`_@kQY$;UZpPUh8g-bVxO-+Yl;&Sd|PL zdXb*o%N}pDrB5frryGOHfp^&>QP2*IL!0xCaYys6h6mKRnZGhn%a+kp^r=N`MtZ^* zjV#fI<0v7G=XmRv<2-ecIettWJs89!6Q1n~`J-E+Dv$X+hnDuLrJa44JkK`kUR`17 zNH^4(SSY z7-2)>Z7d_a5Qsx1)3cMGhq6xcwMQ zgWDJ52ig)^(#+>vr4nSkrWI< z{x6TNER5*R?YgeUn#xrWj!kz(->U~_mV zOH-lH49p+l;anGY$}+i1Fk@A4{h|osVk%S_B8|VskA*VH`z*9R$=(fdqf#m7hqb2G zK4^if$Cb8%1xih8 z%FOO$((5~f4F%2ysc~SOceQY{b@4RQm*#G|3x@v-oh0)9(Mpvp7@QPJ&w2s1LREsB-uwDUM7JCyMh|(hW{l8(VciKM8z?u>X zRnujL9==VjRxJDy`1(zLWjN{aIoA-dkaxtp158;TRPz5b)5YE~YMC6^$U* zH}}0UQwoD*E3$TF4|lU;T1P<&@Lcft;YG=#V_JTHurU$XF~yr!Ze7k^Hy!4r;o5K1 zO=#&e(b#b#1#o+cr{6NiP@6Z;S!!iq6vU>PNNf=Ygc`9MfJ#hn%v|V!>l{Q*g8?iV z%V-H%4$Ba745;&mEKK$Z#?Z=@1=XuL>oy0#nvMD?KF03>3?!-_Wy-N?VBka(SC#o% z@#`s=K0^azX-3=&=;e$eGqnbO_?-f=`e%?$n!P8&Yv>koNJIk4j^>~cp;jmqBZn#GsY|d?OR@5(t1Esp_#&QF>OJ zdXx2qCKy}01?d#k%(J)J3Z5ui+RC)1RD z^(Wz6dxw@{Ow(z_uXL%~;Hz^(oj@edHuRa#T5lu51ER=|44FuKu{_vIp2)~qvxd_9 z8Re-HEnpXkw6Y1XQP}Y5kLr1T!!90FT`j1%yLjM`$nLu}*<|rI`h3yfT1z-i(a)j~aeM?J-z8=p+zv_8^%_5P2*S zWoAUWF$()c^|AVVXCVs|l_MJfGA5*^L{+QqB>^Tua|cLwkm;*;ynI+cMY;$oPiU5U z#b3xA#Dc(#1OXwx8n>;RSO^(3)CcXH5LtqEkjezINciM2vYAk7I2m{&d`QZU`YK3{ zDKj9WX*Gs88rMdUM`BLY14TCi8K>$9$twvV-u3JNVzH_b>}!q7`jMAmb`_J(<)JPNXa{B$a#H5~??tzxDLl0Ayylo@;ioCX%FhE64cSWdrqm~Fru*k)OI^VjpPnz&_adNAn=!q#cyhwVb95wJv@Ue|x52jRXWI|=^| zzPh*~fl74h6~v)=^14KB06A7lp(6#}LmH4wE#)X+rYQgbq6q&JmYdS9#F9gnQKd+B)A|l}I$T?+#RVO5JC z;P|6fuGlNn`%sAgN05O3h%OMV)X;I%PEhY~#b6RVfa2M+V}ENQeDz9#6a}+VcQjGC zI%Fnq8Vo&N`<+K>P-_3+k^D;ST}cXmX1}uQnR(Cd+IKyQ$4s=%hg3;#myRbfu)||U z!~O@QZzO_-nzUQF1k$UHFBneStSjI+8>pcQNa`82nD_*Q846GOBTWH?uAzqSs ztyX$*DE^Trd=+$E73bj%TnMF0aUTj57Th$_PogFyyhnO6m`bcxKFNFov(I`y$NqeKk4K8SDX?93zHFCd2QE-chtt^Yl@O|?N7dT-^pzS7 zhY$OzliWE>29ALo!HUUDxpItOwSD9>ip%Zqewv@5u<*{WGQMUQsMbyIzhrF4f~aH8 zkS^i9gDHJ4h`ln6{&mcqEu&NID-%aT9)z4>R|o1g(D~+y9n_On26UCpvtgchjq$K- zg+gCzY4jSPRI-xR)3-_f-12bfpplP+hX>{G{g(MiP}i;A!9WGN$AT0T|Hjnm7oT5cw*6MiGR_O29+` zf(wt3!Unv9h!lnoXW|)9{((QZAWh`!!7x`zDi9V^n<8%0Luq`71dQMUk{6E%Ye!}o zQsSs(EoI{OzAosbQLnS%AK3;tc2k3vYdLLU(Alsf6=hwkJuxEtyOWWiJ1HrhJLAv7 z{@^^>JXwAPI{)XH=yMzI!6em7??=oyd)F#+*qc<4bC(SO=(OYLzSzu)S7W&6qGw8b zR5l4-{>3Xs%bEp7F8E31jxe{R;QN0yAIj_xNl!VozYcxgUDipN0|aE4I!Rz(HY#^% z(IRC`xNQM8phOYs4Z<&T5@;Vv9aZA`x<`-gmsUW4MkA3h_{0ItUom8GOGzILK(<)^ zq@|CmQGKqYt?c|f2x6rs8?27cJ_hhM(R!P~%tIwUme$nZ7GnEEMAm{5HO#QHBkf`u zD6;0xzx|O+^%n+=e+ek-6TtXCE|~xR0oud{Oax8~dF~`7P$J|o)xphB1B5KVUzD## zk`tOmC<*eR8C96`S(ma72=*zaBe#aOHaR>Zp#iZ1QkxtoI#2w84B=5xd5u|Zek6OW-dNhLSi0&BbSF)!$-pDWzIwt?bZlBZ_q5c) ztghOxpUgyfH^`pY4j`St?GtLQdLla}H4G37#})$*P5MWL2Mgpu=hRV$J&4SPyH)zL zG)trGQ~NpJiVfbJsGxgHgU>z@#SZ4^hTZ=DJ1n=^m$}j>Jr?hXpN|CD@}O?AjzP1k zU;-rT{u-iuIM8uWK8MUj8nUOV^mp524V2%~2-9VITM-T!5@PP$P=Qx`A*Gyc`gpjD zWfEbPyKBrvA)i0dpEd98DCp&`{v;+$4|dEV6LLO3yEPVww%^&4hGVu!H-4FiJ2v}f zK28lPh)5Ib;`y?C<*qC%O)Uw;(FTZ|j2FXa5mtCI$D-}>@VvV}Zh&e=v8qe%P{Ora zQTK_nA5I%kyPr6g31nj}QV62YO5fx{9?!{tA=?cFcu|v>79FCemLr-Xcl8?Y|4C0s&A@sD zVmdFi$k`^JXT@M7q~9}nG-Nx$dPnH9paxMBy^E!|nTq=xhkVIMW6tx5Aeue=p>c!z z?a&V~_`)kaoer>It`L+XP4Z*XbzT;T@POfSRjCv+RW%cPsU=n&95O*d>a%Q=l~%8b z>zH$9Ry+I`Ql>9q22!y=s#Xm1ojq{+R7V=#e@BbrQ-eb%TJ@|J#1vGIY=?aw(A21! zpG8=c9`?)Oi6(#0_dkew8^E^8``+J2=aJ4Mog?Y!&5|wIvMt*uv0}@yY%8%7o5YEo zIKc@H7!rsffdm315FpS{!dnRi3N3JX`$7rYXL^&NP!J8p?YA%La~%e; zj1N^)$u2$K+x%F2ds3D9t&S_#C)1f1UrL4I-O^2(vD#xdE)=cpJ#MGJVQVCnDz2HZ zI-@#Tyd|9-o=7}i>`dnRx4Q3V0#xSkQa0D&&PA5QVlqfW~z$=3b z#QsZb*S{Bs0wP7Pay*aAn><*gVk|Om+`<_!f(>r3PEMX+m};=AR57>FqqNskli@!a zBiMta41(+|cR2!{oyAOLo@Vs&33Yf?eHQnj;O?6)zOj8rPbB_v`aiP^GU3 zRj2itbk{4c?u_gpmF)&+Y7nK~AwEkgzjXD84q2Z}Iqqt9XV}}5@s2R-xj1dd;Sh^; z3Wt{GspkpUD|~3YBl7px?=5$-U1#-7-#EDxZI69(q0h(pR}!iC!MvA_yM9&`x}&Sw z0ex2IsKJcZG9Tnq81@!MjToci1FXXS&MjzNFmb3+ml5Qs}8Dbg$M`)fjGZu`q%j7gJKCAg zGc)2c@plBucwj?hSt_0j!wL3qi1c5}IySTmW?EA`@YeO*q<^_ZIzB^0RW z<~w+^)#B+FNkX(c8}jb0&HIyuLk^X~8q>@lecRND`HfNae7XZ5zmoJ1cO;|Ng-)x= zIm>Zh_4Ds2b-q3B`TpuLc350qQE$l0e%29c%DSxR=5zAx15<39pF*@5<+m=wL>{hP z>$_8x&(n}@|LW)s+mhLTR)@~U;w$YP%re}X^xYfF!2_%fh;5nomS!RP?ov5xKASAp zXh8PEiAy7y=_%UI(ot)>(;rW$o)Jfn4(T;J^{H^Q(0dX0eo7taNq#JP(cq;&^2^t6 zZ}`PfZ^?f2uPUkdM&7|4ZsS8%CQ1_N*HdJjFZ?W-3B$t z!t!{j6SXBYa|OkalcbJ?oh2mds>}r`vlbnk;WRXAQK%uxre^4Ov6Eq@%hs69GH2#h z|3Y{@M1mR*Gy0DJ*T7O_OUlaL%btqa!)}^#+D63$rwm*nOqO~utZ9Qn4Z)(MIV4XZ zV|$Dw3XY@c5yqEMZuc`dZ8;ah(R>uz16;Rn2yIw68)F8uq`5##~TlGw(wwK)Q zwaPilfc+=fl%|J=?M%`38Wo+HW@fy0AI6NXjUJ-HIFgE9CiS;OYEIO3!c*!2*S<}W zg!br|Lb~}`GWdp>gDg9dzGVM7{(PBDgkNB*h2fsXg$qTD;S%{kQXDZ`A~rb3@jwD% zzThF;xd^YsL8gTNLKGz>Ntj#{OHuIJFM*dB?g?zHkv66 zN+C}mO=7ySlf19Ir-}V(n5K3!XPWutlJW&V`PdKZzlD^BhFQ6Ql1aJr1W!7y5|ol_mx1s!B)B# znBR3dUQk)LG&)n???rxo`LkUy3RPJeH`ILDKU$uRwMQ>S3g{NSX;?izI;L*#t^VOy z!47JU>+UNsVzYLtc{qQ#l)BCd^$%s;%$Ctu=PN|+xhse}IuVg0l(l8{bw^T^Zm-{= zap|ASq>2?UHF2!0D)V+qtIHFEIp$>_E~g@dV_#|C<eViK`~GN?p8fD0yR2$8YXo zdMgEXU4+(THsXh9*YwC*@ZB+;$?ooOHx#pbZ)nf0&1Dxpy9V$@8$cLB5n#E+7V5aLYn9hYlZr^YvEBiNF*%WCr~%BNTmuCX$&?ox zdrcZ-VzQU3)?4K}{N9qYmfuEh8Y9nd0*OFQh1++eroz;H@r#))y};1#>8fX~4LFN8 z_dlEUi@EC_FL=G7HTeTKh3;5TV^kwf`Pub5QnB--U)8|+vhJC(Q_11s@7UE3eQ%i} zxcfqpmE|H9CVz4^`a8?lr8;{fpBxQk-+y19^;%v1`Nf@M5zXK`$>=7Ta~OE;Xl8~& zIjm;cN-+^>bG&q8>X1usTXG-t5OuVid!2mUVO0}=r+T_|J(FRyhnnv0k9JmZvOo`~ zxog(0ccnM3BR{RiveX5vb6uOfHt%%2_Bhh2l5pzE`X+Z|Q4dWC#Y z!=#^!>fapJA%?;!yXaSA1K@=i*!K(6+@xU73t-;>W?>VI!uQ(Vu~>ONGIkI;S<~0vgHDb`3f~KF~1pqL7;?tZpBDa zvjuq|AQ-!AqO1!9l^_TiXh6VbSauP6)Jc?8;HfbBf$&#-0b9|zV5`lX=NJo2id%59 z{8(%@{skQrvKIA0In3Sit+)emE@(Zxo8S_()*uRAw%Nj_6P=0Tlcf(rZ#vs<$aLJ_ zZKdy`m4ac9ltg7~^S!^Z2-AJ4k?R#FPlj5NTt3=-AXz$~dp9%hcybC)-w2A8fISmW z(bbE{G6%RT`}PrCDTZTbm>cYPOX>LIRHK|tq<+3NTlhJJpP!{)uu7&UBboTVXZ1Ia%#lG$0KPgD{g zOUAm6=Otb4Ow)j*{UVL$43ldNUe1sn#xHC4mdzHElO3v$AZ`gPLAb>VhMFpA6~C&Z zTSW$3hCTH-qz!cQ)5WT_+xbvftH1mJn?u5$|MEemD&Cl6_#A3orGKcCvDu~psU3cFg-`Anm?c9UV!-}<_B>Y>AS z`L^y9^^VqFsz7qzqV?mr+S2?T-t|DiKWZi7*C8?QKCxl5E=EV{;Xk8hkN zp`1rGuH##&_O+KYk<&X{%O^Tis@-2Duj2q+r8UHK3j4kFP8;7vz6dK)xUS1GAg3R;LDhL^CI%s1`yA^lVIz*}7Wo zy+T!{rvVg ztVn}Jf~vEb_OEWwX0|i#$NDXOd8bmZ%3Ftyn82WN$IME zTN+Bm9w<+rpI^CV%k)h00QsatIlD)9Mg0AA_S7Qbs-2{eW7BS@RISsfR8gaux->%N zdMR13?ZiZ`T>{mKfneQb9xUGs}R$X`pkW-C*&ZGYo;shIRhBiG?2D>ZIuM2<`Y2o>{Vx5E8!i51??mm&NhiI&B1sCJzmcIR zfn#IP@k{uuWWo2kH*|(|B)+v#%(Jf*7S?s6^-NHyXO;=%uGmoXlZjki7kjt(fNQafwnd0<_ycLd8t1MUOSyngYHHG;91nLZ%{ z|4%I+>KF?@hkQ@4_7^p_NdJ19@`0A)w$MDB>7|1V*V1l*VZBLdF=Yywn_RH z{)l}FKaw2C01Ox@0i@h8z=BbP*Q$|5#pwgb?H1?-v!&exIMO)z8xMJS4A(;3S?jPkx+anp$wi-gInf{OxGcD(7Aqicq9ooJVky+ri06 z>GWiY+cQTWopf~3N~`v+Q$>m)O9#egvgusv3tSI$I(ox_vg?0*^+2`yPxoV$Da*w_ zdwYldqRu?ODPhl>8GJg3#r>Z*rpKVJ!tV1UmMZCHm7?NI?~YK_L0h(MPG(;tIsHVj zP&gK-XeV2nl%6V_)k{5r>|Z>>@Ld=&z^y*OI%lu$&{?j18tnO@<%ElwVt+gtJXz9* z8KQG>YN#h2+NE}lU3RG^WA4^*#hUsN~UNBy238BQ`Eifpth6nm~+%XAb~-b8vc6HT#i;-c;9 z+~IsVcTM|6@dT8!N1dPp+Rg@@Dbs-pzMCLwpedxT%Z9V7up>m~HRam+*e-Xp?R{+@ zur5NHv0LqRm-` z=5v@z2a$Jq3AcNpA_7qbnHz6}V4D$#Ef&(eW8eg0xcOC(;&Gx_!!wGrxsaYgA5j?j z^?3sCE>SeUtRqjPKNW`Da533BQ@imF(ija}7^3jR36WY}W!xpzF>2?q>Yx0C4JJ)K zdfe67>4CLBrlyZV%FIg9)-(HxtMj`4dVJznE_(nyxw}u*mdg23|Kol=iG>`@J;|dI zf`OB#nmE$= zK|i(e&cKQG##kKGkJ%j^>WXz=UjT~8=B!%duoEq-ISN0eN|54p>eSc|ldiZhA*?aRdbZgU{!e5jHTrtsO z`;$ersk^dEWeStlA-6kXyKccB6EabJ!Z)eU?`VtT6Z)BRbqzk@M{WOTZL%&Ep8(&2 zB*H_ZFkxe_VTAh{2y2&xND(lap=iP*3Z+N*i4y_Qh-O`YJR*-G*+iB|WeYY7zCi3I z*~%(cF}gU(>j5A<#Z+N)1wls=rXsX(*4%~w8{R@<1@4ENqJU5yGVlovGZz)lCG6egjQc`iQDsy`!cxQ>4U_~F&H>*cVTai@$dg+ukAWPl)lRDASo|ZB( z*+W^Y$Yk}}Sihc?>Wyr1daz&R-CQ-V$i-A6kt8*%M39lSLB&>EA~u00*59M-qefLK zv-o<52P%H^rQvPq)X`95h_S*?jBL=}WY3H`hEy?vCYQwNk&w*gIw?sLCQ*;98F>eq zi3Yzsk$dxBChJxjZR&_M-wt#x@zjCjYflg^2d3AYu{>_&DUWhlS`fJA`JRJz^t3fv z`BI;Dj`-!+-kE|IjvaibTn*w0Ym1X-G=_Tm%e*)~h+fZCxAc0w+qm`ntG-X4h%lMF)S=UD~^V9m5XzZ8ltp`50344 zD%uBgw4UgO)RCQh-MvwlPkZ4ZuXczeu?v5dY+pzZL#H-IXo!fTy2Wyh{Z}0rg)P0b zy7Flkm>FFtX0o?fjp3+O0gHLXX{ST^(RK!(wRbT8HwylQ8#3I{=j>mhFD8cP+F-U~ z9*~S_+jCac=$(nA5$=X@FchaXsK~wG1<(e8DnKKUyqN=tTL95#0@p7CrMeQV zR8#9?Zf|!on;K;9Q}b0BKdCeG$@WBYN+0JnGF!)~v-mJ^Jn1dA$2)r_RWWvj%8+4H z`tBk1*sbcEZlpRt{+CMq)_JAh{b1jfL3CqeZRo|JjNjh;??KOImijUnRUHi{W|lz( z3;QMfR9BC9)_4z@rRE#z`d^<^Dm|J#jldP@fc(R;@>ijZYC&F22UcEP(zBldXUd); z0d`anf6cw^xqvK(sHmotGpiO+_dG+$3M*huRhV;oC~O9v+WSMJ)>{3%tTo==Z~`vXVasn}0*wJW$g{U0hIWu+Eo zTs7YOZ!COJJFKaE{4K@QOp-^XtiI@DOW!IAe1R67VimO0F+{=X+@UTg?*CxjT~@ne z$!MPOV@dTwHecQHIcxCgk&Khrz3-6rr&TVJxb}rh*U~dTU-Q;XQpw>!4`JhUzQkEN zeqq_R@4w7h*_Fwa83n}grsJQYsz@d?_UyTIZ(rPI4)8X{TTS*zPCS6)mik_gbJN#dKW&f`E0!(lwn`G&TlfM1&ex^oQ+uXvg#bj~EReC1cojkmJ z|AGo7CP!{^qxoQr+pbPr_xK!m(oK#Y`*|t(>8r4HNJYVL3Nn|4|s{|>SZ?uOXSb&dA zla((_(2(sltUu4eO`si|pKL_rN#d>~GeB`JIwBE-P+1q+ip?2GT})3aDIg(^v~CIl zlv_Zbd3WnNG*!Rtt}&FFCvFZa`s)V2%FAwI~#cQ&{QeJPO zNXQRrlBb9yY}Jn?zUhhss*hs1RoP?|$a{i=X_z>TO)(_#Y_s zpGLh#j`O1bLcXmkU8)YF>hrhHb8UY0;8V5f%wJIU;Kvs7xoZa;nGa} z?4l;=t8HyX%w`7Pgky5(n}SMfWS9#?g_})5Z{D%nd6W(-*(gZKug&%qvxFruCcDN; z79ixmFQ^QGzgS!6$Ul^(_E8!v6CvBnA>3;>^}*C3o6=>PwctBvLZ;=6l`wrHAq)ra zk8S4*d6_9KW_uu_fE{T};s7S|?T?4ng?_s|Qtr3b4)*v_>;H4>A!7LtT0_>kFeH!L z{Kt5NOi9>J)km$(?2xRwRy31O{;Zoptf|axp(!t&wbt0B^G>q3vz+RWeo9CDN+}*% zuh)9%|f1T>86{7AVzt&(SdUcUahU=~k z{IuN{IlRMSGMwo-Jqv&PQTW>^F+&$UfoHM%=%#Up<5I`#0QjN$BLkozylNj_AwM7} zhstht8l&N;Cg);YMZKL@;!@E~6H7?Aj$dV(W1YYH!k7rkfMj$m)?B>9pnxJy7<#t_ zyzl@*4zOrtg2scr-PYctf33bVb0APb^PIA9pONMbG!1-CZE9|x53G-_OVWRyeMrTJ zUkWEDUv7})-L|7Y`MOtY+~YpV^rq%N(kaOpM(a;9^iv+zEb-hGoH`DVV}0tP^i0Z~ ztUfu_Q)vXAZT*HsZQrJmFCZx;zI>&;3k9OiXJL4%%!kzXnE34 z?^cOuExYkv*RGQQQ{IrD8oW5_WUd|o3p)$-iY`R^4uAUM3;Cfk`Ju0|Z(d?F$?o4q zCKO96ADN*TfG8INqDk?;WXDa^MkY|wM|=;G5fcn_5fX$C7?>1bXxr;Oe4ady=Sxmq zY;?9)n8p@bGW3K{+o4Lm0YiZ@aW#T5uEZfYK9L_B1y&ogI?DrwBQaha;_(gjd@>$7 zYi-=QrsQu8_Z6C$S4tx`+=aC~aZr6~+*h-QgE9*}AT*NkN$U$m1+ySQtZD`ivlcbu z^xSfdezC@`G+gBL#n$if`KsNCsbsd?IbKRGozP1Qwo=_+spx3;3rU&VcV)g3hXzr0^sP#%F~%trt8)zM zQIlu)A6E9gU+wW;IGJ^PdbU5wVby1s7%UKWiX~SyS8X<1(->2{i77xe8d_{wHvCKm zds(Mu>6@XyB8!~^9}P0Qiw$Ywx{!2iR38kG0hmbmfJt;r14SUOc(64WNj_;&3Dm@# zsV{?21H*#xsTI(%5;)>jU4D?1p{*9sQc-m_8efSi{-h90e|c;>j{Tr;dXIm zRcGNOlXp@>k9AWIwuixkEXCB;!wkz}i>hWe=^3UqYBx8sq-y8YH%h9u>@H{h4D~!8 z*yibEk8nuY#paJy^@+LpgUpgR_53k+^;7pU@_{$gnW2u-P_3SbQwOCzaLrQlEmFty zE3}j5(01;$dNf-aCGI%lsrdHSx+4efzufny&g{ZHq&8gh@|_*zu;hGy-~Ny6w2I*m zd+02?OLP*OCisP8sX*~hN=SjdgNWcWMHvSt1{|Y*uyGu+U|As_OvaN4Q=TQ&y?kYw z08QzaVJZ3q z)TjU6v774C2kqRu57cznu6dEL?OMMihf)1O^QGCFz91i!3TJBM*7;5j2CH$nDSIxuGhaI&jb&BUz{7rm3Hp^+zm|s zfBEduV^h2OIj>f&Y6+i}YAfQiZWgpE`Y5N>Hxb5wv8gsEo{9s+ccGs$6?-Vu|C0VT%4XiCAx1(-R0p)J_(x{yQik>jbzkQn=EMi0@Aav|$c;e_&B6>a0^ z=xzfLDk?9jN~3-fuD0S905zeEh!1fJfAdCz6&b&wUSJ^f2dOGJX#EPu^mVo2A^SDH z`23-*?Y~{MRdSg+UCLk z;Gvz5>SdSlE`hU{^_)`k8|`b@1Lf+kv*M0fi}TF(t)2sf){vy^*mQU3)ZH_ceEr_* zdgwoN_eG&Xr`4BI6eMNepS%=6w1U6BoUWt=kPzZc>F^bnQ1FOYWF{aQ3(hf1 zBr9(uiYbqYgBER-18==GZy87zs2sY84$eSA8q{0v>}921)9U4w$?k8~Js)V{Pf`;W zIy-XbDVaX5<6iO4nm@Jd>p7zZ$tQyZISLn>3@(J6c}Eh5l4f*8~x|}Fc>6DTiF(Lm_isy%ZQDZ zb0cJtniX90IP1OSN6h8{t??7QGMyLVw-FPe%_ptR1Y{jnHap(@%TVOSGb~rgSTJoVvlSD?Tv(4F)1eLu z%mnx9a)OeJI7nB)i&$xN$LOu$NjW32q5LDpTU?&R0xbtCh!@Qp;E*_81AUPJ5S8Q% zT0Da}8;;88F1m_<@LXJUYX_u(N|3ZIHRWuChAw95N>?$q8I)g-As_M~EgD z34w^~`H8FoQol~CbW=|`_Gk089|KW`zo6&NF{LdQGq?wLTYUZtyK5vQOZPmTYwzun?atOM-Q!P8^^(8Q+3pTBNU7BDwpfWH1mvj4FQr<(czl(_!u$6Y zn9K~L>+*x<+h;Xvrmcr-Y+jR6x7Aoz@@hApXEFFRbD>1-(}gfgBp6e1?QfRZa9jDn=4A1yqP2x4 zg4*ot@`h`i==JxkU_3@1t}{lNdG*0~pLMnGjBj5{k|A+D*`^3~`77{6VK{_196~QS zUfBzg<9I|>(a;I$Pm&C&A!7qnj;|zD)R-_x346^+8%QiCBudEn3xG97l%=69pDCrj zQmH1Uv&Qo=yV!A4ZcyPk;Tw$rAtuO>0ZGO_qpdBN6iDqni$#q7V%|;LJwLCPQS?2~&^>QD!qE@mW$cTumTm7>l6NQ%K#a? zOm?|Pf!VCE>pp?Bc}p;LbBv1Yso7`jIu!xSvrGb#(N{lNzjHXM#|Jys+PQ%N@3w7` z;&(IY(O0u0H{X3iy>f^^_+VvuwZ3t8C6&$wTd^i^#h#=3_PObfL{xVqZs=9{^j+R- zTjz34X>RC_ZNT~8p3o0}rf7yOgwwjagV&HLq7F%4x^`_y0$1INfzanzTRRwesBH#M z{A`;{-7_^>0)5aL2~SZ`0wIjD)EQFc((13}vz`n%E|gzpoQa7QJ&zcWCUF$c#e@gO zn<5ov+CjTfVKENoVSKlgv>0~6ICJMovELwN*GyM%{|DTS;y$v712J3Lr?!u#ox@M}TnetfAF^PR6*KJ=zk1svm zB&{p1r+%u2I#hirk?U%wbP1y}Nj3M?w64E;7Fziz%UOxEw*%i@XPsM`)YiI78mbWb zK-bLy6(zQp8#%q?u(_N__XtVuMDcLh-8y^~5pHlsDo#{*kG&>I%oVp)c5;?_kFwVn zJ$s(AKTqxZSk^mAcY@Nl4=g2XAGwr9G%`F#9^9T&`usV+n&_IhKGaylf<}7iO6us; z7(0er97R0G|I_4g|TytDh= z+cF&H`#)!FkRufq+MTY?SdXc@xPJXIHl`sDPH8GUn$d+Ivy#a5xX2jE-olSMf_;R0 z6s%62mAnsEn$7yrjMx0>N~$w6@|S>uo$EYu-ui{ik&CY+3{WRdUDB_TvkxqRoy$6J zeI!c|nY%${5*?TR=-WHX^cy)N-x)ga20b)yk*J6}Tl&ug0oxa?NZE?|bthA~qQhYL zFCz!Sd1czVVZBD%PPcs%)(a0nzbhZNIBpO!A3r%eG+%ICLxQ$;Y)Gr)6))1N=rI|8 z4hsn}Cok2{LWLL%XAWQNikzVs2jk$eN0U;g;6!plgM=#bc2X{Yw=w^4IiwDM!12%q zKiQpnbkF`=>>(W)IIX<-3`ZR37nAcd0h98A)dq=jY#~ zB?Jp_^&K~d?cCJ+o@E?yIGrNT@w?UXVH!%x`NP>*JHf<{_MVuZ8-ko086C06B}ba) zZ{!!;cl^zoO0L18AjlPgI#j+bl7ndF8oy?Ln|)F)HbFMf@1zg#+sK01;KD}?us1FN zhL{+hN%fMu<+w;Y!l}r)3yu)lq|s**ydofC6=YPViRHvqNJdG_hnNvorXnhm$ZXYH zwL&o?4#a$Nz!DNxgt6Id*$tGO=@P~U0WHKp1egLr8Db+^H{$e#0U7V1NK+u(nj?lN z|DlvlY+LK=35znMFF|~!-+h*k;E|qOn!(LqB^=uQo4D7v6ho~apHolG@I$4(c5zQ< zw)bO>^e$uv$2sdIU0}FPux`t3Zo$cB&y1B8j{@T9+oh;G+kAbFYN>m>EBV*JkPDZv z-JK8I&rCMo#Y_)`D5}&mz)4br)+W!LLne1o$x;h+azRaddaEvF4wsoF)zJx$>ZwC} zj&zRC@?+rRQ+kEqu}YSCp7JQ|kKDQEGw^DT-njdK*H{guU(Bq|-_dbm*O3bT|~(4Lo;RG@-!H3%Qq6v$&j!4?}YcMjLiql_Stw-T@S#PANUe5Awr36gLx zDwK3IqoEVZ@F?S z0F_bsi)Yk}k%)#^O}U}lF#?Klm>r$fwYQ7fO&x0VV~FLB-0Hx@>=skI3pYBkubDH3sMpy!6<+bPJpUhL+Ki@DrVvWt!^w3aD*!}p;^{UNgx92fJs5DTBf4) z*5V{Yz6fvR?gphfh7eN{*doL*7UaeSdGePC32%-2(qB@r=+z_Vgu(k9*zOl&yACc63B_bZ6ZSGJgIMHIOJ4 zhOe?+$9foYXj}SBL$JaMGYule_AR zJ0|W;_y>Zt}9c(y*AZ7aGA|Q4W$p$c#dR9P0RP# z#VvVF!5r5yo$K>5RLPi0c17T!coUQQ$(ul@WrJi%nAo2SWOKU+)q6O_U=*R5xPTCa0k$VV z2Bm@mcnVzQzCbe)uX2mXoDh=ceIyJ8;o6Oq+2@(25eccK!^!N)svfpH~0j z;asRZU9&#ih;=boq?BWWW4&y9l^(XR!fo;Es<92-$j_=)kK%t~$cxyNIG!j0GO3W6 zohGkv7nbF(V4EG>$ry&BJ&$TibrZ)9P-)SUSREot7!J;&<$jna3h8LP6^*N45W|Cj zVp^_h5h)3`f86ld`$g; zbtfJu^Pp1gpHuhICQ2*ZC*pO*3hry0|Kr5rNA;a$x@VOC_1O%oBT-!^JMY~xarMSp zvZPl#zN_<<;SVyq+q%}P)+P_jgouUj5V^(2`K97rxh zWFgGQbL@XWVEKPJ}M#Fl~;_^}ghr|?wYX!{AiN)8fPkwcTS!2uvR*ay*mdU()n zF#}|MIbBY4f|g@pPNWipv@(n+{YS!xk!I)>zt{3)Veup;Jgx(J4QfR+D8Lh0#35q# zXp9gFPCSo5SpK^9xEXhQ5AKFNS0!^=jGa?BiSs(r=M-U2&0kUYE(~ppmr6zx+CFa zwYA&rq2bO6ck6tnvm=>eiIX=t9!xvo%Aq=O6q^M+Z{C>&!WI|wnFh-WyyQS19K5IN4X<`mwnbfGVL9n8g=GX z{rS>~5AGl;TzuaxI!DcC{(t>aDG_HQVtYEYs`AW1&A{wd;@#xEXS@3;<()35F?C^v zPk_(+UFtZ6Z|P=U8dFg%u!szCGL%=#L?H>{<&YHIocOD;ylk+<8i-Q@eF#dG$XN`R zsk-BEB@u!wVL*)8st9#rLT9WF#t>mMCKC2UL;yM9=b&-1i6R*iJqeJ5@bDet z-+GZ;7k+S7*O}HPwAnYdOJ4WIJGw9M|V<9Ql?1*{H?ln>l^byY5Xd!SLU3WqoSGSooZ7g zI@Xyws@6DxUW$$}U6hzvMaKJ5t0e38g&8TYDRs#gmZq5A>pRYsm&UvQTFo=2_TtNR z{rO3)_TD^pFMI4VulhB;lA&A=%o7BTo$JYP$@QIGD$+Znt*5=2ETX#u5BmV+fi>;y z(P#B8@+{DFwdS*_J~wQaYR|_c6e%D ze#D-dGf%F!n^2xNz4rovmkGGh= zR#=aOni-N}>%>C_h`JFk?i_Mp;w3&q8-aqnNtPXul#(<~?eD*ey=s1@_a@Z7`C8Xn zf-%L2j-u~8^yxEJ+H$^n9eIGKM(lsf2CQujyUPwkZr3FlldI0$l(yr{B3pbwJx_(U z-^U1s<}(wUZCyMWwmiD453?F$Y$V_OX)01#-4r8T9+dl78THrpvi;-xHhVqgI(Je# zqt9oPrPXZhNa#ZcuuMzKr>Mv~*T|N3GTtr{%O;=iaibw8@AX~ZLV zy%_IqeruLR)#_`|zX#S0mc7ivk@>HW*>^wAfxrJj%_^4K^UUQgbYDf4;gdQ(SdTsI zM7>le*p2$QseKLoU8n+jccC#|?L6f~VRq{DUHnPtpTXW)Q%k(D?LgZR{NMYS)%>Sq zbL-+hRSd~G^xFQ@v{SlM3@&YZNN0vasc_S-guK0b#{|GZ6q1A;b3~DcE zQAZ+JCSl06cr7+aj2hceOm#osV%vlg<_kpU0(e4RBtKxt036@H5BL@$QSLQF@(H8J zKn7t7#0nzYg7>XU>3C7$_np=1A5{O#aDEv6Z?ctcJ15DXtqVh1Ki2FT{&uQ=YTv5fYpQ&5z-f9rk-A7jr?wZIA#x18Sc6Db_kEf{= zh%;h*$+w~hl1!q_S66F5S;wEU_UqX0e zp*0S$3>OohI&?LYiHb3_qG?94Iw7IVjD0? zfyhoy@N{O-wEez^n2VEeUiIth zOkO=+W&N$%S}(&&7pIR9L%o7vy*aZsxBc@4SV!;sUeb#vX?+SOl=a=Q2gzi7w6E~o z$9mQE4-KXYo35*C7Z5SrF`bACP(PDB=y~Dv%tCe&NOd^LsyWIB^wB{MgzLFgQVq?Y z4YLcbSW72}__mgl**ft2XgO$}hSJktlp~(#2vm_3Q`^_YJ7c+HSueCynHnYgT|LI+ zWC#mpr$|2=m_B4}*DK!2DOkz-SKzk;&D8Eluh;y& zUbZf3wYaj%3|v1nI_qtS96w*TZvI4#jeFF&W!=K2K}2fbhe}(a7gVaV`k-a?h4rLU zDwa73o%aLX8TTwFG^!jXe&&Uhq!6xRC&5e_;R7M^zgpm6)10<}UwqO20W&0QzINL# zwJd9=bqN-Y+33NA|D@{y#NCHEzysRAW?7&^z>kPT2>AIs8Omk5@m_R4%$79k(C*TT8$s0^@ zU-D%6kvKu_qnqjSJe|9eZ@aB7i}yn^Uhg^K#?=@164vTiQnWLFnEu zhF-9~Y=1TMo1x$N@8-6u-=5Sg6JuwBHR{|D8;M;N8;u?G-t8P+zH!NN(m79e z=2F(dT#@8+q7bu_s`*$K#GGaSaXgoaCO0f*bJL@n>Ya9^HthU)=!s9Rix3HA>=eqV zLQXsy|8^{GZ47M8R&wIzS!{NJtfPvihO_J#VkeqyS)mA@6BCk>eSX+VGq%f1l zkBl>lrDBl$DQ?)Z{7l5PT))qckWCNyjO`COnPFa|L&>bQqwqqf#-q@gv8!@)YHC~bg2jd6I&Mt0)<4UERsBIs#tuLmBGNJSc z9WOGAt|m35rZ%m>e3vtAA7qcnIX1cfP1`?N{o)lQkQBU`yO4{d<07Wnz*x8!tS}Mxg>0hGJ%;%s9s({%7KyM|YW-X2 zVKR(AxsYO#pr^&{1UM=#VLC&ITuefX7@qh??h2S9K0{`awk{`xk7T_uFtRp8euId> zC;0-EC3!J9!LmzUns+9teh?OsaO^_ZPoOT5H!n6}wj{p6;U+$Nl(PSJidmME*8d{^ zxDiLt+5Gi&juk0p3(Hm{Jy1SGXo9NrF`>Is%`(omAVtEu=F1opJj2i9?A6+s>#*ZT z4vgs!MM;KdViS6ON^M*J4`;~f$2KpsvtaKIoP%0-2Y?}_emMO_KiL6MoLp2Zqb_{V zYW_|V00;ND>H}$#o)r~OtLBkiWw-Ad%euVBxzS}@xTjv4qpdU6^%j@%DRrNpol&QX zZqT5qu4EUUA>@tyh3YJ2mT4V3CsDZCk@nma`OtX8i>6ON;3NyV*h^U#oX-p{=M1G% zOL|@k?qr-7dz-~^1Z!j3jq0mDd4Ne~W56T8^X^afpJWhOqP8~Ys};JiBYo^qI5VBs zl^Wy3@@I?w8-~mL;BFw&;aWb(EUl>a4F`s=`qh1Ped=T(h}*H;n!IbhUl%q_s{C>7 z_E20~ooDW2zawe>vFQRk>|xRKyXMomle<`(p<;Vw7b2{eJD1L7E)u^aJ9NInj=>Zg zbh==;XW3hFDct0-wl7;Qk}pt2kP0#+0u3x8Zw9PIn2Fd8d?6o!c<=)w_5c_7MM2L5 z;Pg<*JO$Gd&m{~X655g3&|FR=5r5axEYtuw7U>nS1>J+i%~jAG5kjs`)`MWIg_1-F zxe31EafDWc91>eFnE~CPA|#Uw@F-EGWKKj)3@Vp=E&2tujy?**Wz?7Z4vhL4=BOpv z+$8TH*=F46>r(v(@Z?{e|Bmf^>xW%2&+|{GpEi{`bsARyta-igcUnI~nFW7pCsq2R zSMF5J0T87$tIP~t_w|cvlm{es) z^UO$usZq^^PDbo3jFj9JT0gs?#g%V4ec7%3-BFLi!K$lawxh>(W%7PaR-%j-U-Qqt&HGe@ zuABvo1=--tiguO{e?LpG_=Htpk+swy`g&#OhPQH1`AY*IOwQj}TR7sMWG8Srd(VeQ zQd^GPmetwvy7xUdT}rQ`#w5o$120<4A9IuC>KY2iGL)!f-WiC2_1G7c*H>&%Y!OOa z(eE-KC46z|$hdlVze&xgV@po>R!A^x^)&eX+w|tR)DD%X2Op#Ia0gS$4`8!@C=!cp zC92}YNXtkLmT4c*L}Ii0Xa_(RksOY`fmt(*XT-EMPVhAXfoKKcoP!w^Ij$+bwUsvc!@EAaM)q&tb~gq-Pm`2 z#8q1*1HgWQPXYiTOeM@UgNt}R*o`reBIB!Y4%B!!2$)5{m^Lpw=%~}qI>JO)g9lUq*Gc0M|1mVk ziBeFw3IkE?m^d{PpTw+X&rzh{b~y=#$S4}IiTvECT^-q`qf=u&ekezM+s^~n*O^xX z&u3oFV)MByS{RQ_^bQYfi|Ip4UbQ&8zc4rQYX14pwg+3=Lr!TSc>YF~?nKJdZ)bAp z<@C6*gR|JdZ<~Cd%<1f7gU#`_yW1XU`$Nl?95FJ0L^X^kNLUESmf)^s!3YlDD^fCo zH^AA7xq%Z9>nIj;LW~T%$U&~8N5uSK@GiJf6RVklnnDC%#&Eqr4tdAG2(eVIyl25A z0c@BmxB|n?^FC6yf|W8|UYBVebY+7qplvqXa^l%dLx1bY=UgZ{08f;${ zN>r{UQSb%g9Mj9;rU(~*&K0v&IdBK&dNTd?fYFKUM0wbyOfq?;(G_+Ww#E+dEf9 zIQ=E|pO z+x2#iP zv}+^LSh~YbGIod}#&Sa_-gIozSR${MjipNO+oKmN#>X#c_o33HyFBYgz5D}*evO&? zLH4CElgAHD`!6NQ60Z4Td+hj$gCs}yI5%D4WqY#v)Pp}r>U!6()8W&+L#s_Rqf_gt z3yT*VGbZ23?xA11Sa#W8U4|?PyEM(Y`r@$Z8TJgHDm}w4{fS-JKFKBC)Aj*oraVK9 z6x&2H;*hV7+t@;DW3rUFB5ZgFTbOwNwWt-A` z%u{(t#7Z*VIA(bU-}Xu7NAN)dLJY>Fr7R>Fg<_)^0k%!-Fs9Csbh(hpBss?Mnk-Pm zB*Mk%iyxQjDyXken=|BCFKIm@i3 z?<@5KO0v%}L*u@8e>Unqdb~a5B{Pq!i>HI<-7K4{L1Z@$74-9F2sWD;%C7Z~N_~+R z-TUtU2AGOucI*Jv$)BFmW2J`of_g@LsC`9qH$81X(3A18eIszP{szjmGmW8WC260? zGw)uz%kST=-ZIp9c_p2@&EA$^sEezf{6W8B_6}m$+wL<9CR6T8SFB5|6e%CCkjN?T zz#1nO@6XfbZ2JSWFw@RS{F27RH4SE#^9d|g{iCE#2F zV6VJu^9O_T)Hh>*k-4Z@~>d{>`ZyU=&FrN-*+cRC9v0ez_}N+%|rkv$#kWue0Et<0NR zeWFCIto6Z7>E|mI#8J8Nw6t8!u23l9S69{Xo90wLWJfWON zM%6EcO3v#_-OwD7{KXsAC5*_-6BKR#G5t^7DjCZ*e&_kt+RAUTWAN?iEkYvk zN)7DE^Tg2$wI|t55je_$S~&&*n4r#x#8qM=F_rux$d-UZiLs=bLvR2I3-AYJg-ETG zw~EQ;K4duw)n~2lVhMh-!WZJOYZmV##@sZln)u9wL=z^8BJdIX%RL~)pi~kiElt8D zIDi&m^F*3O_vAb5v5?#(*0?4%UjV^)Yc>|YEuTxv3`1E9R}KvU*l`3cqe(4BLW0N( zDrr>R}NE9wLTJx z7S_h75{ku$qnEt5RIR8hL-!!m^BC^wXR8`^mCP5Tn}}|*Bg>Lh(--Z;1he&F&qRmT z*6{Opzx&RVW(OM2+geQaLpvz_Rxh?ZlF9^g#c2R4kEqx3GTB}GogJ$4F~(a;-!4~0 zd*0VPHyT-*F0&-DJ@p);x1=|V4DU}LaiX0&Ld>~$$&=VMzcG`FIsFMminQ6=dtMN; z@^`6OwUtf_F9>Zb93cIx{c?WC2=o0}B*Gh0pWWYlLG>_*^}%n@C`zg%zsD)+*pBSL zrOf)uR@O$+{p$Z`>V4qbs?U3W%jYYdBb_7ZNILpsNwy@*_Hm-bN-Wz7c48-XVkZtH z;DAX231CPdfj|SK5LjB$5=vUqLJMty(55Z4X<;R6X-ivL=+=$i7wfni_pW!B-(9!% zuD!Rrm+f}@v5niPzxVT1=ziU6!IGsT`Fx+hpXc*DPco6JPh}_F&I&Vu(vhhLz1k9^ z^h_xh8o<&;>eAefI)kU^@tkG-8k*hc>~!+!bjGmV=q5gmNz2C;mA&1X%nw$VuW; zF;yw2vt`s4KvP&<{j$MS~^vBwY6a5W(5R zYRH0NG>W%NB!)Jl^Fo7>UgNR$6QV*wRcJ>3!n5io3?m%VB&{~+ zCj&oNF07*^ObXD7xYGo>NVJ#HY3Bv!DB;q(A(!Z){!2!1d+44H?M8?mQU7;~O}s)l zgc2g6+xqKDh{ju`v0Zys)S2`M8`&5R{IgwtSwvhdPBUn7#+pE|YmWVIcMLb#K=vlT z$mT~n)2c}iN!6Oj&UUUvGy)%OJz(O?%4ghy*%#P(ZnX6$j?eJG)?-UOklUGZR!OpM zB^7wmUNs*#jBP+)%Kk#6XBq&nMr*pV-nx!lv6QvmFuFH6bHjmt6!+ZuyLb7yo}bur zxoPG$ky64>zWyWeM*7)m{E7fU)p(OoZED;>38iw{+t_P8?u1Q(M8i0ejL^e#-Szok zzRNY@R^gOYauNRG0dp{F+YN^Is}n|N@S=49CtCvDKBGE&ugP~Y>vfgs*4-^5YNnD7 z_AZ7(%$H3@kBTo>P|BR-2nF#}Y2IC`ra}q+l=z8lWEXyE5+d4? zoIj^*IH4$PHrj;f0IT_o3o~XtA!Q%WdXcd8^Z23Z$>t$z-l@*_O^2di?B7*(jy_tZ zYM)!1|KN>>(_2iEC2NQXHOs#6LDN#jSr2Hy@nC-6ARf=?M~ zlWtkQ!dv0_>#O+UOi?>gIPqa3wW3EzDDOv;WUU67LL2oKc%N%BP(Ui{zFHi|#m+_6z0L%bLxI}=XV{hb4nm$QMXAKC|WUoVk3AgITC;EIfyLW|=T#|x0)2TccEVZB>8C+Nwpr?zs<>^kusd)^p*XY8QiqSK-0qcEdByC(!lD^y zO_U3T$y_nBZK23uNJOSITZkG=DR2Kz=A};77MZnABG8}N)E!`|wh5w48csDA_feQq zg*i2w=urdvubft9++l>^@--}w=)PrB(aSECs|1uw?{r<_<87#$FPbH0t>7oFBbwjZ z{KreI?>|9*(5nZwM_6jNZ|m26e|xCc2(440Zk4>5Qy>2D;pU3_nDScOVtk7o2CN>l z!~;IEWX6Ag`h_$5*-f{8^y=GkI)4=|{vQ}$r3P;@uSHlMg5hnE?w)>5NKUOm1SI(+ z_Z&WujMQ$D`e9nQ|Cl@<1i5{>KI~OaNX`Z;$62Q-b&83Sqvl92tY_nZ$3RP@paHfl zjU8g(s{9tI^JSwNmJp!={tyx1CsJ35&9Gz=x3{A$%|m(@e-VX<vikEk56C`Qt1eqt)yCvPeNm9bo7Z-mU;5-_SE-cUh-6x9seJLk!aQO7-aUPX zjpgdLyNJ(i^2Tpmm=z&2KYVtGOv9PJvEz=8`=n1)qTmX*8O*AWFqNB($Ek2^B0Sm! zipX%nl5ol4;nfAdl~-spLohCL%(Q$7aDeYOm?eG|Dz!#8*K+6&>2yiC;7TOV#CcYKvYP(47ToD+VIgQ;go~Ze)6MJ!E-)X({ zR5BFfexLu1LA=&bBY6&iz>=ILtJ0gfgE8>ax2>Kp^VIskSee}L2Lpjn!VCmsEr?;X zi9^${&DKfK)aG7r-p=Q0@z!zvnqpw*`H!rQmWI4?E|XldDZ!3vaCi8gquu3HBs2m2 zP_PY}r1EnEyXk=NjBT?{ZCS>H->4#i>~Sl63u|q!bn5Qpt;Nt`Hb=V)M+DF4cRe1 z$u#qS7PL!a1|UrM0J0G&Sv5&g#87D8!o$&~2+C-C_YA78BO1A`RTYv2YvLX(g zqqYNB5PjmJ#}f8&#-iSuj3d#T_!NzYVFlWAxWodv|3pU{ zWFLW$@tD;$8ib>{4IF2@*jqSUQ@hbzrVrw*jM7Ej7ThvsYLCNl0A^r8UMM^>yT~P& z5MdrK1in2UBmugn&kQA^H}Dy!GnGdiwZ8zffcCHT2&gLhljS4kqcmfTnZfNh@QgXt zvlfmQeyY1WwFY)hV6D)-RMUro)_@0A7)q-Ljg*(>{kzm2vV<8^eQt>oP_`*i*3+~D zL7hp&+|X^Hu3de1kFT0NP#m|@SUu;ajq2gB**xnM`5WGO=H+ejt*27-vz!0@@^HDf zl^()4-Rlb6Zhz3O=h&mzddn$ay@Ft@CZmQ56H{GXcVB6-Sns;Wx9)Sk|Ja<0E;p>m zwX4JGvZj6e(FSa#KC@@DQeTX9z*w#S_N&InkvT_8+}e&!aGLjYe4*oSutQ>B+QrQa zoUlw9wipMbJ&2quQM8nZ#4sUKr2jOuV}cM%MBefX z?Z~KFiBxKtA!pEj3nZ7kUgr>EW_Zw!!&$N7Ls4yqC*mH-@x_6Wwuo>uS+{MOJ@nFI=@M<^EyKif7N~*yyLcS5pjY9QvGLgjaID z?1AvB{Ckv_FE3k>U`yq%X-?*7+OY@9Y*xz_GrQG6+Bh7ab}@nr!;B|}=?&BQ-27d*|esVZT-vM+z@LvpT8N8F5WdAKvrJ*4L6JOgde3zIt5h9M2>ZTSO_IQe%3Csvii9f?OVOsQz z9jZ;7Gwu)9B+PmMd2JLi9?hWkSH#9*9FZ&nZ!HsCt8KHmj)tj3tT1YzF}O~FRYgFW z!?Lw)q?(k#nJY=a+?Gk+E`=kl3?jD}M=_x~E|S2CuZ9%*IyK~3;&w4?FwR5=5IQlt z!_B}=WiztT{7cTcpr#)8)xvXCB4y5vfQSBL4l4P5mSIIUAw%QEa44Stb7MO_v9S-0 zS!$>jHO_xw#NJS0BZu0Nw{imoKi!*8%$Zjfn0&)EN_E;IW$K+(D=ph(JA-snSmhpd zI`HG3bgb)irpwB?FEhLfcxWtF>g@h3^>>;db1BQer;w_dcMUMj#GD!dWtd|-Clv%@ ztJzwS*-U3ULnqAQ*H4Ab!HmbY1!T2$@C0!=?L`brj)Ybb=)pns7#v+J4oUZf+I!U6 z#<I_kvhsr-Yh;oc1v4f+aEFhoxYe;R~)s>?^1;#8=tA`8Ootu|>#8cDOB@YBP zlSKrgGrnCMSj8^!yY6MLoX5yN3X=E-fG(ebGFB$|V)0v6U!3$>?qqhpVD(&YrdZOr zq4E%vZg*xHuUeY5UADQTK`RvlRR2&bS5;#;wC zVs5ZD;-uvFS^&lbH9?*PP(faCYJi+=N)$k-?vozXJFPHxL~jyA5hdxJ^Ws*Cl|)bl zbP?R1cgt`OZ6?KaBY7e_lC^0`mM9F_Cr=ZgRNFWFsl@4WG0EJ=tt)sj@D-K$Q z1)2k+Vj^Wdb6`}3*KzhuqA*&6RF>lk(2+F??eL4;xx!oI?is6#ojBAm8&)K}bG7RP zT1ZYlky1m~F?(@HZ3?{T4CD0<18N;KCv%yNu39zfEU^!!kPG%KA5{54KI8oV+3J*7 z-o7K&RVk?KTq;{xZI#`8DrcHzIXQ|yY2;NERxarI!b zyY<7xdT(&$$8J9h&0`U}7C~lOw9+XFkB7p~7KH zrhlokwdiWX{&})ZBe(n9JClpxit88MLWpD{uy4wL4=1(!cGYKCe=mKI|}3Ns87m%SU)(> zV(D}!E|yUP1XYRe#z5EG@**yk;}*Xszyi-^dyQTZYaO+d6Yv?$wnY;RjTj!ME$KdS z7|aYpJO+?6en^D84H{_Ro$teIlt zDUv$!qBN`*mR!Z4uI?u|6ArPVVjEN58%)y3P37Dn6}sGMR6ARLGN+1*V;9Ss>WPBY zl?iQ_DaUOZWedxTVmA!dlex9gXk}fB79ajsH><>G^zgMm7<3rqeOQ*m;w6YMS>AX9p*(ynBpkGHWUuj4(}2*exT7w?4UD@&La< zT03X$+MaWkbMO<@I1?EX}Ez{Ky$9_{ZD$9Z%@t{|g4 z$fWK2kgiWL+xOcY|5KtloyAe8z>r)x9qgGnvl09k^G3uxUBCnyQcS^;Na2JR;QfFn z#E4+H_(wxk8DWWP(fs_&;+Qq|C07lR#Y#YlpF}=_V&U-TX%oAf z28-acgjOQiiOy89uY3xLKriwr_7?xG&7}M*#?Qy>(W5E(LmaCqp(c)3F3QXeaSVC{ z*|-%5Zdjf2_9~{SBPFa2qRMe+ocFoF2eKh)~ijSG9HhjIiGtFIBpYM)!E|9l| z)SH!PEnu8~feMBK*e8RvjlmWyE^vmMo%5flT%Ic&Z3MokXedyvw}-ma?qjuFF#b-l z8^)#!yv6YSf^k)<&|g_hWEeha%@|g$gy4tY;#?k|esirCQ(u4Mb6AJk!uH$iQsmi6Q}UazU}pWMQJU*t{ZUH(ANIunVTr2cSM#|Ow) zpXm5}#|v8C#PCqAYY!_0lC zO3i4VjKRv!d1oLts9?l=L-prX?~F=E`Yy||T(Gn|ngPg!%pv4Rl2gX*fjVTZL*t6s zCYJTG5iNefQRhEpHA=wfWET7CfsYuaAF5w%-`@4#(;3sv{yH)0B%@yI|ElR~eLNd> zCw9akp_m3RW!X0X&xnU+O4gzg1gZP9^*o;PkYjlV;0&Fd%NpTDqPV@PVVCD^KWm+? z4PFrms{V+3QTo+Jq15OaJrK^s8})NmjlI@j8yXRl<&;>=8{lT>)Y9QTR! z$io(Pnbu0W3;1iUh^pMhAF(E!)Pz0jxy9S>^HPbB7g*^}R@h zD@fg+1_IbQObk{@M{&CCH4B?jJEW#=&qmDJMIqR`QsWh@f}p7>EJp3#cz`37I1LPy zP>!s&sVAHCQjFG2cgb#%U3?lJL?Mi7nS2B}36@U^$cUOAw9{-S(8Ch{X)moUs!pog zOd_VWrm>SqWWRQ>BJ!9zY#=#BC!9734atqW$$f>pAr?@7WBfl=EOFv(z*ghMx;<3n z(SSh!ULRl-wpqA!*i7YykE;)M35-%>S8a-#NMMr-glUNl@)zbI{$&22k+P>F)uSlMe&?l2P{TM-z4s4rG|&J0(plUrkn zto6Jd2JPV+vxPCcRqYR)*dv*{3Au!c&T!iOdi4mEEXeED7=sY}5*(ks>fnv6CyX1_ zVx1u``%T-T&a+R=Fp&qr{Y9BHh;k3R(;K&POY1d)O_=^8>qxk=x4wCf8AbEezU9Y^ zz@GPI)#rbF*0(0z?`@KNIQ%<1DLKb>y2Y39%y)PEnQV(D>eILg7LP+jOeHp#Y*!=^ zZY7c-5=mm9Lw*dOVE57ziY3yXjzC{~Ua@-iiUAUq$L2V2j@?Vk+Lzw0%yp}`0u@Jb_#Qi=p z^z75q1-JNhBsA3>F^qdR94ndi9BHF*)^0Tm85r! z1AkR#$O#*dSB4{92N2@)707zgja%W1l=>h21*i4Pnx6Z!u5$`0Bi1LCwwR4D7|W{o zoR>|EcW{>6{9Gp-C||!nbv;VYPqJG5h?^&|dg({$mJX&A*lJ z>Q);ncUd4jF>8%gk}M0Eg)9dPSW^);n{prQ?_GFRW%~J(%kLqYux_U+Gk(bU{3iG7 zpPXNL72Kj@4^Ozdz9qc3cV}HE>0I7RH^m(t?<14`j4;&U61Ras%IMnNPX*cm9HOQj zK9WIgJxhXtRTEnb5QA(H0SZ4xi+)B7IP!;B42(7@NVM&!*i=E1ky9eZDTuu7AeFI+ zNE_-h2Ej;^+(eXQ>kcwUN+eq3!M4#B6(QB}h8XyFV@k=Ti7kjr1Pl_7!1vPMKzx3? z7Kf{Mj*}-5Kj}HO1}Uqkd?g@T<{HJ?VJZ?6amLp>!;6Zu4~VJU_hM*{fp9%;4t&fsL-io~*r4 zG?!-W90YgYV{xskQp+y4gV-{oaU>r->IMg_2S@T?;E{gczrEn@I7+vJm8_8PO#3DY zwq#b!`c0X&`jVQ;Ro!~%1`?>)hrvm?<&e5 zH7J?Y7q|U-#d7$VxjW(3Jey7x<5Lq|T|XMLzWDu@fo9&?)|M;54yBGR_>9aj8)T1V zlY08a>`UC&@v}feFdFDz((0m(8+M9>1#8FbDU#a@d_>aUQQ$+25^j}+e!SQRg`s94 zhESuAh=oXs3$+seA?g)zK+K z`Xt%c#WqQHxQ?#RBaX?@VVS;n-NyFGu;TpG|0zFZw=TB#46HZ-C?NQEAj6!bZ zRc0*}?P&xh=wVoD|LB4|6l~^;aOu}C*@f%*;x?up&h*h&PC0+f-z+uwsFUZ22O_mC5%xh1zxJKVsBUPH2H#i`W!zoRB0l`>%yViTYP2Cc@zeALV*>XXx?`+<)#i_uZ1to_55(df<} zQs$zS@~j)XiWhI7Kpcwf_8P^=M$77FQUD_zykR>2H$bs+<9h>Z^XY8rVdqfT>`kbF zE;m0j8Q%{xJQCiu2;JPGn(V0Ljf?@T&N8S1UW2->(Y0w(=(@RhZ^AVvRU%2ixxUlh zUT^`H%n4;Znf?p2`-WsX5t=_(M}wBEg2PU7VxDUw<0?D4QF#mLxS5YGOiKC3x%s?;0e3^uMd}+s39ru%= zeTs;e5;vs13k*Ft1e`k5VBIt!22!M+-M5%O46PP+QXFB!Ahd$&NQV!AWm-%pP5kn9 z=^nxc>ph)J=xWEZWORzSEu#Smm)6Q*kly ze|VR~0s3p2@->*<&D$mXmY9IA0X8O9niNjn2<{-tfcXZTkWW(DZSeK=$MO~AZ$=G- zSqSDPR(u2%;t%f&tbxS~^kgxJ3OV2CH0&9o(SSKrAZ|<;4WGTpK#Z=LT(+)jmZo^s zsx!5A$QTBjvew+mg6=Q|>bLB?+)&A$bdNJs%{E-)WD^N3nx)y+(u`_6UIa{{cWued z^|J0{!q2Qp&E$+&IF=5NyYYfydIbtCYXD8jqU?_*Rp)j7!$sp7qZ+%5EO08820?Qf z(~eWvW~=e}TjtoajbCvc)O%CIAz*kmgAGeM>hqs#bah9zp>riu>`QD2! zs+!wrHiqX@Z{&ud%^WWh<_|;La|&$3jul)Qg;TkBDrXc+3^hU2qeA^j_U+*YZKEH= z>)Kcf{YlLkNT^u42@l7KxBOH#6gfsAOC@f)Vuf|`tr8jELRJ0M&B}{siirW{Lj~<< zk8ArT05N;XL2u)!t>eFZNU1M9^=5d0F{0TM==_InKL3(3fA!Q0*JTrNXPd@W1$&Sh zxu>@>O@P~9MILtteA^!d3UF-Oc+r3+Ng!Xie<@2!lW)87ji5`kt9hgdb5lSH5hh@Q zrhuYivqzdbgUVotWo?g!GX<8QB-%FWguiJ|Y`XXjFbWAO_(x(@vB1oc1N9;^6Q`o% zk#>C26fA)cpcx)lLLk9I;lR>kLO26W2oyt``BAJqa@-xzL9)=O=%HP=WYL=n3CW3p-zc>$R13bhz_pRz7^qcPw@A=JWqTNWuVHtX!U3i=eOe zi>JFHYW|9J3mA-)1QRaYzd)DO8`%4_RZIL~!TRh9wd1nB^Z&|dZ;I)q#<5PTT4H#2oKlo|*A3ThR?(j9PaYn$8mXyHx=9q1ec606dpK$Wk}*Ym+Y)k~ zhLv~-Hkr9<(``JxsGQhF>ieZ&uUc~v#jL?*^E!XXV*H$9^$eSW3@1P0GS$3!h2=TN zawnY0ZLq<)58k3(jUSjym}`>GL?m<~nWk7!U{TqU3MM0~Swyvi!F_WS6RcFwW}`2y zg$1j$@~>!lsce~Hz(;~!(*I+B9!^MKfTGhev`hnV( z>zlc4tl4nvRPScms|wC_yR*#TS6_MJiB03?vF93=YGW4P)BLplj+J0-ZAQp}j;{x< zz=F5)DRKTtlDKOccZpkd_@PBhBu&F=#03316BK=5K?0VTDmf;SI0gHiCUzHYqAt6p zkt8wa%*aEqM01G3B@o6Wi{7M(9D1oT>3~5OK9NQdJ|DP3suS8_AaxhgHEms`Bw+d! z^V=YwRz^IGl=eLhLGVr!>#KETfVJB253tN~U1?us*g2~TxnkSAW9LP8q&JlWMC?6E zNHh4F?2O9Z+T@!$aOO-E=5q$Ro$?3pScGsGIXjQVuvtjf;9n;J2#gdN-P zSasDFEN-L`3e8Z+OwF?imI=bshpOT6{NOm11U7K5^@*g_$jVxB>mqfM4bhyVR&C0L ztA?>P9uLxW&lIHj1vizXGRAJ_-u3BD!#fdYf0{`r|7_3*MY8WWUhD(Az3l2`OU|BR zyq-&qrvYP7m=d)wDuPVBww8=QVTMhZ%2~ou2xw85Fu=6OKo6Vv1{f}MgAJ_mYH#Kk znJf{Db5$?Y-4pwEhWuqK=VRR#i&ew3>6N_Kd~Z5bnC?sMd9Q6f@ZT(NJl5%@;#u}p zg||Aazv;TzN)H$}s&IZ_bD15x88(${tFys66%G$T15~H76NjMcTW<2>xsp1$QrJP^ z$PJ0U_zd>}l8p1~fj%o+I*JyitkUhN6j_Vw+#GYZ1eG9AP@Z=7O zw86K9n~)}g>ed|cOUZiovU&Bcj+X-vwZwfi(I4#gg>hTD3PMP)d*@|CSXMg(f$flK z2y#D;(sAY+gTC5KY znv>B?+DjPg z396XTyX>h#z3_{Y6ZeMtLeX#!<0huA;L_dq#kv%wYAe1QG%}nT?pjWn@%AlNtiO;g z;zF#R6-6tXi(JGkg|3SM;R|O@Z2xS+?Cmx~p}1o@)g#iEZrz{DURMQksIdd?%U{^C zqikm5E9|E>dCU^e>bhV8IZiOYVxZ%CEyE`|9swQvQpfZ3CbcB@MHFT9l`IXWwA`SI zf+Ptn0&GKQ)HbS!R!G8YpEMzWC zMv7h{)_T>Z`$o8}_F(HZRn1oCbFuE+(3;Q+o1n<4Vr*wsiS%T)_u@FKZWPnQ#}}=` z{@j%6-c)|Uq8($e_^3(x1E;Z>lu0Ja)2veBPunXpYxiDiM{X$D2M=eHH(C^Yi(6JO z6F+*(M&(TGRsNWn+-QyDZGZu&GiK1m&82QV2)B(o;w&ixW?VU)oyEy*5NsCqHA0D_ z6^7JgA%wYQQXbmqlRtvyPcVAR@Ogtne*&_d^`oTwe8iu zxY*rD$2Ks>Pl7qxV2-kXbC~(%8)4)Bb%45KKYNQM!9+-CC?!lsKtyD9@X<@;E?6XS zLx^r0L`g(OST!JxfH4povY?QmehPe8u_iKu<5}oVVrSui ziIAB{dRvOc%-Z+i zE!^-w3SIeMt97k^^Nc^@tOFHiKEKSmx3%*pj3`*7TU1#i>(UX{DNqoc+#dI;a+MeA zb(qec@z={b$L+gm>qJf*KYE`xx11fYD-f_gIt7m0y!s(}vG41$XO||kUdbM2T}$IS zw;3P2M13?g97{Jm`|;uIf?_^vb3SlsIUZtc)p9kxxaKC;^`EBR{b?5StmsjD+y^Zh z$5f4Zc~;3xz?z)JCNn(%5YTjJ>9HmlBwJO18x+G`BtEzH!T>X~;fR;Lj)FaK_VSGd z<^+M(&;kU^zQe}OLodRDcQbBa&#A`j0*wH5B$;UhBe7V5n&&aoeth3WpzHBmMg^X!OyFLhYEzw!rI5fOn-EXa5 z-cXZ#9Si{zyhX1RQr{_ZpEk9|8p@sZAK4SIv1@)~Veb-;=b21*h912ZPRR}xc^_OuKjAkwXiSB4-q!WFmx7F7g| zcWHP8+fIxxE*~uj$#sZ$N3}vVkT&iA+huO4NowoPR}qx_LZw?oQnJl-<**wj2Z(T$W+B(b{-&jX6__RQ$Ky zJ*MOKuZgz)*6OO1N{ymD>w7i@P0Em_*_p^xL}lvGC+V54I`!;Km0k&Jb@*7^Re>)O zCmUA;EkCo>R3qal_c3+-27vaii~2VRBd#lAIZNK88Q3`-g`!qtlb+g{mR?haGly$r zwSA3YRmbcuDIW(FE-?vwy)o^i>a1t1T2bQ|ZPM(yV8B~4^l<4=$tdki!^!RZ=+#?F z;rkwf0Rz?84TV4+)oI_&q>~A`Nu~O9B|)mZOASLlZ}{qry`+j)P<^<8;duR*nYm_d z99-V^nEQYVX6;XUso}oTu6tO4k;uTZmzhf6n#kOE#C~XNG-K~Qc;Htuk@OcM137!# zZDuEK*j#eQL8ZbO$2Dihm>Urc&1OcnZ;Edp&uncvq3u&p=eW2D(Y|mj+Ji5M>v_oV>X!@EOF9s=l!I#DOK45VOL$gj82?Ne`c$)Zltbb zwuTI!klD)bT0w+AsYcU^07p0;9A_XVK|fpFSX61$Y2j#{iY>c7lZaUEvWpnBm9&nG z$!7T@u2+QloZ21EJjfgNrmAHiRAs*6x6V$hP=-L6FnQ2eUXNt@U;?|=PcI6VG8J}E zKIfkQI~P!9(7d&i+JTq`2I*9@g{|2{Y~I{n^@15^(k!wIDib3wfhkg`^=wXbI=h9K z@_46FC|dh#Z>c#*eEH~5+Aq}?teiC!2;HO1*_uaplX5G`y@OKJQibe(b|3;`$?9Z( zLq&Rx_`=rGLpIs4RTwv*;tLyRMrFc9O3h#a&Q^CgI>b&)*jP+RpkAG%h0Za7cbu+F z5lv=_ST)B`8)gX0q$q>=1-9~R##dU&E|m=CQYHV`vYiTc!K$5*A>8aoGPW@F>Ar$e zkW#_WVgamG6>Bx46xq-z6}MCI^12YU7*(Yq#V*db4tkSx&d{9lU>`kONBdVN%SQ)u z4_p#APk;F@nU-J$67^;+vD^sLoDkyAV@!NiWpIW4YgBYK#bmo)r!TJZ+dniOXC9P+ zmm8xVd>#Es&r5Wco$4IUFf}-!{}D6ai}Z;MF#?z|10jrkou3eL2Jy6~>q`1853&d% zV}tQ!B(nfM7oIYM`YHXcm6Rj&mC^N#7Yp_d0hV$V5F7rFalBj}%Cy;uK%-)Kq7+Yu zddWfXh5VOaj}7B^yg=gv;X{RpQwjzWS=atdXVNQlMvQcBLItb9GAb0$Iqy$qV(FOo zuU^_d9xIx5xA!$O6pbdLR@c8}@{wpY)A}!AGn&o@gG{A88Ms8ny2HlSdm?vka-Tld|2$Evl1jL_>1U2CCx6@b&}2&gH1`5GUQ8>McjQ;mGeLf4oi4{#vf9|Ci7?bT_kWI+}t)O9YE?VL-Vg zTo_Gl;6{nL@#K=4VZP{~5RWfa7(j?IUmgSnjv&(E2S}*cRWXy;A+d`iDa@dT2zjBw z8+lS}EO1B?#t6uL35zO3WUls0#5wt8S6%(J_4W^!=ZtK%^=k_7Y!GFxU|?A;`>P36 zZT`X@47GkfTs`$i*+Pr{ghZ(I58Fub1*wj!=heJm)GP0a-^pIoVCMY)+CCjm7JGJ9 z(kc6NvQjHqxdYR#L!hv)nP)38dPX!_NW>3J71;LnD8q@ER-P@VKbUgkahko)HWEEt zkxG1e!_lP|vL)~OKy~VfJMJ@cuF-mBqUzmaghF){nCJ?8l9tG^Vtu?!SC8aCCVuY+ z#y2$f$mts@3ZTByw-2)s_@h4`mc6x>*-d5nZV$R`R(Qp zt?|MurIcxSnc7QP&xwW@kN$!(9})9pw0_CV9DjBuTYA7^Z5mwT;j-9*s60=Oz z!3-8i(Z95!!IOz6bMj7KAtTS24N#pk8!WherK~$~@VDD?7)xWkH&94Oh+S;(vX6#R@R?U@Kf7+M7W~J5D3z=e}_Risc#(o6? zPo4&C4Not|0&lYZg%EV&w6fpJxEseht7dTMrrC9qIS<-#Yk_c^kz8glTPS{i0p^vPj8l{pV%g_%mHQQ@8pmO$-ae%{@aym|bi#4zA#l z4sNN8AKTqJ;k&z5)B-!-x8=i^$eC1`nf(MczcAb1r3x~w`(h7v9BH44Xa(sEH%9bA z*AsN{K(dN~KFO=eyh&Op^=V53t3r&9m?9KHTPS&nz^w8T?zYg4Ik`U6MXVPFM=IW0 zhfI0ZcdhqkFX^v4dE-~-n6<(0X%BEtkjFSyBeangmWLYg zIaAdauDNAIS~Q~yf98{W&+ zBxt^XY~Zj3S02L9@96wm*#)d-+--jOE>UbcH3xIYA$O(+niQm0}kw*^pX|*gMD_~ z?e46fSv(%iGmHuqvFOKi5@w?JF=DuAl99OXnXy_ty?S-E8W~C6;kb}gqO4{!Du+)j zY{Q>8kDjrtL+fJ?u@U9CvwP7sGi5?pORcXADp`v83d3NfC;W@Zeh_cm-Lbr5v|~L} zrgk%5>cetAxBx^JAr{fs2o*;&x$8tyveCj!q8MZY3jvbas1k~b9`c?Mn_&@-=brko>R${_6teK-}b92 zPJkZh?2FI8&_^#}@WB8}WL_8Ad#l+KZM7;f-#W9u)_P;MKWPPSJIgONev@CmIBQ)A zp8(+XN-OeV%WA#7;?C543}kQp`6)YU$2Lzr?H1-t)89SkIpxz-e_aD=(F5M6lgWZK z@GdF)1Fm`X%3M5cCqqyn9$gk96NEvRNEWyG?9;NGeZHDA}$$5c{068v(&N!>T+OGag_Ec=Gc`s)shJn;u=;sir=;1fj~cgx9JL zo*%W2xdvI+OH+^ccyZgy#kl#5d9a*T^{p%vywdeIbmd#`l+1AU;LnDyvC8(?m43Fb zb+SJX$0KmjnPqnrRN{)2;nveu^)LhK_tvZ!1A$8UQ1>y;1x1;C$$bR<+LE$dg zF}aUo$+$~m{KZWWILWCo9}Ocmz>`2z?Rcq;`-;bwXEohk{DJ5WvIKC@MzjQ_ZQC$_ zTAmgVm;)AwSBG5$OA-cSyae&R@bCNjksm zO23|Ms_KtQ&Og)|FAe)%>!Un z2AEEfzkWhZog`I{)r#>z*ZFVm7!N1eGBfe~r6lj<@G}6wcJwqH8>k52q6O=3Kkt-C zB>{Sa4@UCnj8}?$B%RMh3|a4(T{6o|aXMLGICpQC?K=4nA6|4a!JfxHu~A*}*cs3I z*d0T+TX8VraAY>k;EPEmySkjQ5LXOdm==d3NT*I7xX10Oznb!*NeEEX z<;7?W?gN$lch|&&K7J>MN85wU7!X&)1Okc5IC36}*T*4!3sB)!$54B2Rq5r2Cc=5w zgJtfn2pO*ZX0+##$x5V>+|Ze;zc8)_0?W&@Tel7_@4t~|Lc`3?s6E4*d&12L>*zJD z1LgReHC4Fb-et+wx$Yjz4^+;nQYrAIO5~B&>FKSJ?pwOhtpC#}9NBMq*{LJD180># znDX2uI!xz1t8`B)o&zXPhf3bJM~~M4uhq#!PgXVwD|5wL+YYgEWca`GV1nT(W2k9S zd1=z2$APq6o5NJ$hw5t8U#9-o+KKd*xJTuP(RFI^%bRFh$fnNR$uw2YN_q-@A6pmE zv&q+zgIw9Muj4(x^EuikNgkrP7ibc-4a8sDFyLgdP*piJeNdfrgpM*Nq#ZGUe#j~{ z6vJR+p4dCB8W)0ziuOf10RI0)zoaJ^Hu)6?)DGr^H2z&y)m2m^eDI1)|G^E#Fv*8| z)VHYhsyD;=mn~Knj?D$aGx0w#)q^IBrY6}ai9b12f3wyby!QRf)N8#pOMhQHu|@}5GBxWk=7zPIEJxGrzfFB z8<*##QUgNh%*xtAaV_{6admBis1$=sk~=~J1)W4?bu#KF+WG%WrnPp|48LL3<9!e1 zY}-k7MF#FcqsAQf?SkjO&bx2h)02BAonc1$i{tf5>pKuLvVoyp(&whCY4`2v&Db1OopAv%JA#$x!9)wY=)~&`Mkyy>vTcq*eF&l5K};&Pk`Fk|6`*g=%!=RU{F`OvXOGU~W&|Lzn2ViW;fr zRnN}T@A<(@ZbN1I?!cZCsl*01 zobS4&YIBZ~(|?|v-qQJtAlaADeI^kclHJZG@k#H-1N7kPdNl!o_CC58!f)jPXCntn zP%h??5fx}*IwT3dq>T;PF{%%VU_f+>w`eDaW0GUSrtwO2m7{GJ>2>)9fQ3z?raF+b zoKIXl^@%|*<=EM=6DU%gQT3hwTF$<16#HzJhQ6$pb^fYstDn7Q99X^Y-pa#V~Z$&pwp99sXezRy;zTXQAbBf9p^ zMmOb*M9+>)E}hJz^B=dZWZCKb*y6nV$R;+ny`f$Ml1N^9$C~2F;l-4Se*ZR6DvMdI zPjD71(5VW!&P8a|E_e|i2f@VE;JinxsvnmlX-jXKctPAcQ5>$K9c{_v4xj#dQ4slg zw2}3*IyvlXL;nO7N^UbJ*4N|=b;#ECToN^)sc0G}i6M*QZU`6TLk2h%c@jS*$t+HZ z!)Gvv_0=EHhx~lt{%kfrOz*_OXQ3lAKR9q_DG_O%g9(!gd_NU$wVrD*pbI)pVE67o zq&$%Q(8Z0`AAGx1Rj@Bh9|~-FZUw^{k~5J%bz@yd@hcxM+8?-NAUx$21tQcKkQ^iK5cMv zv{!qZla_V$r8?FwvD^0>>|G)AXY$$vtj8T-FrVo7Y+GZ19VL*{hL77QuAzNS;#Ua0 z`wfhlB#zaT*e!njuJga3KK~vD;J^sWbQ_!aCGZ{HoJm2JBVyI7#?Y$hmxSx$_EHkt ztqTxIXuhKk)^+e8L`V{$bEd_ll1uMc8Ci8Vx59mzzZv7_Ie~vHsrQWV_r_aua07qN zmdTyh)_+pAorew+| zs8~$XGR(p+^{k}n0uvH8$0D5&(~Q;2m_k4do*hH7xnCE823;A} zpc$tFs zA(zrMYALvJax&IO6ihDBl$DhuI$B2$S$A{T>i(+L_~1%5yyOV-g7$Fnm;M`lY(2L= zT`ah-P{HwT`Dm*8)`9MxdSK-n>glRAwXV{+>1$o{A!#n~XWfBMUVHWq8V6U6k_E*Q8#g$`I66a29v-;uLnnID zjCKN8a0*2wLEK|CP&0PX6MUW-Ssl$bv5r+*SGzZzZaRhDXfS@iSMZ!Y3&BPb^sw(kXLZ% z?RbN8l*j>$XClDXX<|qDJUJW;Ax82Adpmn49-eAAp9*KXyu!=qhBcgsznya)>0jAH zDcjm}yGnUK2NEuPlGz2VUrtVSt`4mF3v-B4iz?PrDD#Hs=}TGj=Yq z$zJ-@8ui0{zc-N&7oAetWM``RQ6H|aVbx9LogQwe;-wKLRSgtV{dc>W?)!~F`&f?f zafUIrzg&Rt)~?6=So7Ay!HE#aM2@ii@WiIEX%5@oC zx!U-{XBae~3W3qU$C>@#dIR+cbT-nvnoE%p0Jgp_eQAaD!0>eeH@~XZS2B$6bXneE zGdSm{ft;H7?)k5&N3byVjtS>mYHN=Ws&`q_DfnHNuIr9b7P2DAx!+&TI36~|s1vYY zQ{kqvAIN$P@3#hUHTF|=RUAI5>|iMF-TN7}IN6uVhtGXZz3&q5)AeZP$mHrf5G|Gc ztFL)a1zvC%DC%reYx?_UQfyjo*9g9?v7e}p8Kl2+JF_#t#wHj!EAA~P#GQn0C*?CN z88yD1ks}dw?2IBDWMd>;5>E2VCqt?XViuPYjh~0m|wK%ZuD?R<%Dy zPfN;Z{q*Kxdcm$(?|z-WWnij#x+GXcRX$yEVOz0WmDSh(srQ&)21l|YSI@5Sozqs4 z`OTT2+e3W$VfqunU=1RSN#EP;ScQ28sBY6^J^V+kcsUYctVg^z*K}$*BW;vk9S5PMD`MU zaI9iVg{UKAQ?-_+MZv_5>U39os!`5bPVwFFL(Cy3Aqf_TghXhi6;qiIL&;^tTM5kP zbMDwmNu_jyxCU+sY{o{h!92@#kT?7s4+cihS;oZ~JA8bqZk>T!98JY4YT4x~POZ+3 z+ppE%V132&kT9Mv)|nxB^&Jh>-TH5pyjA`6qaVnJmTli)b&k(Q-_s1=S2LmvPdPDh zU4483|K?7sRBn%9?y`Yr~nurkn%yx6pdcEf?agLyZ~huHUF&eDA|gn@gYhh%)O1f zfb9;GLc<8}TUT)zbJNC@eoTFvE*Fg{%@F5Z-SHi?1SBU-7_2c^GzJ?f{)~89Qep6M zN~N5M+~sz4LtK=`CJ6n;z<@LtM9%EOsiAHDyZ4tH4V*6t6!;VBB2}rZ@Xt()rM8iL z0Q(}8t@7?_}vAsnXh(wxg{OJZZ`F%oS8BSF+QA+6A zHMXfda;phpBwW-eBEO<;+Z2n2hzNUGpC>a}+w^4~%Nc4J;bept)Yr_)s3Q(svz>tz zF@B6u(2#tuA@0#LI7H$kt^o!D6;VXZW!Y3aF7-#gYXV&>VF#B$L6R<0`vT?L@|R3=@tSig|yImY!j^nr6# z)-yVI`(!rD?w8h?TGm?D`nzPOadtfh!mmG;EhI&J);3FWNW2fCtGD zJUwk~j<{wCOZq-K`M=58p`sFMaC)rO(&2;tN&HqD8bN<0Swn%V{W>rq`axAC!r;U> zC!M*`1TEUx3~Hu{48Gv1s21lUxtrGXc5NR+t4qf6Hqr+{6(vy7L5oy6rLm0AM|2g@ z@hTExvEo1sF<($;-2U*lEOkSIOtL$2do*(KHQgDI$5+X`0eV zI=YW-5&726Wu1k-=XDlJzk(r>e>=xb9y-~bj6~z%V5apx<#9oJ`?mxsjHy(oP%hMc=Hz#k z`C=gGTA}WN+q+}H$EslDoR-S%X}apA<0Z!m8&YaMJak+=t1c=+50yshiELuCD#G+| z?PT90v2NB5t1*I=Gu^8x#jMAD*X(9Gf7}Pyer?5{mFe(Ln+RYh?e^r7*23{29m|%e zHU9v=86l4GbtZl-om-F5Pk@!iT1u&mb7>d-WtoK3ki}F}_UDkN2uEb#l0><3AiWq= zP%G@RSZDOBldbFWqefVe9UZs6o5FLQ3*ip$7cnAU#TIkyTwXWHOJ1nGg5J+AF%IUP z-|Di?8=(9whBViI$ZaG)n)1SSB$9lBo6w}A;yWy2vRbcA{qZPMH&a$Q9IDUtZ4laS zyese8CnMc&0-9BCyU6l?&K41`u;amc>s+yvto-JSku{k=aA0RT`zDUwF4{7?pw0k6 zHC(Ip;#vRhVBhm*eZulOZ&YB){$qY9$homHaK%5EuUgj7BQi9!{5Bo#K64HvM*$oS zTvn*{nZ+~_>WOU56)XQ(zANPjf8!LXV!B#8XO``>kzAAWkK^gh=2t$XBA4$zOM_Kv zrj+uw!QKycgs9a>pLUv9rx(AvmU$aba$0gM?Kp?~-cI~PD=(nR5_@V6hSowfPC`np zT%+WL7xWIzmF6JfZ?q#$ZZ((WaKKI^-EXTmJ)l@@z3+pW7|^-NKP7X~ZdNp#%M+Q> zH$rMJZ5l7r`@5%zhbq13)IYX@p`?Z6w;hUxZmaedTX>=ftB5~8rtQiRb+lUW?c5ox))zO5VgHDvwoEIha>M)LZJSfgmbE^ zCz8M~3Yn&NZM?X#$0~XC%*~c!FIId%T^dfqv8~u$SM0E=V<$Eb99(&GJw5dN?aC-) zpt*v@uDc+bx#h#Fu6C?DzQW2=Q02mJ|J?XN=l|iSvc@Pk!splnO8Hf7Puo)>s_iXo&U(Q8o`Tc*)`v7u6$hHISWA6kRj>} zf>4J;WaFE#GTXD8IlQVg0bqgobX~}noPlmzspZk^$eU3&Fq@I5T~=K{EMn%Z=RXn4 zhb<>}q_1GvwN&EoYd>fB)MqllVsC=SN77IHXQeJ#VKO+xd}LU)-WaOn119s8Mjuq$ zA5B3YFWNy|q%}Fk;HgKt?_?@RoUz}oS6i5$DD3Go?fVmI{(Z152x~kiwygEEI$OVC zrTMIJyE@8*Mmy2>9^W&HR{c*4$H5|%G!2G-FPw*@PINcW)>DipGC^UYVFJ-5LeaL>M6sj*BoPnPsWw=50Su7DpVQNNE?`iW zhyNd6Zv);|dFA_NORuzFX>Vz7Nn0PbWJ|VeZ`(?u#Imi#PVB@c-{3%ikOUGSKqwSa z0t85D3ZaCrl9seU!$(`_&^9g1Aq@1CPU%1gI&BZ-&{JlHGsB%`+RjrtWu|kcz0-51 zbEBT$+H&Z5?sK0@KX5ErQv9xWef-yd{TJ5`K#fD>wMGLGWeLf6&bM$W=8=S{)J^+z za%k1vH9J#&+a1@Tons$&(1mGksij)ami4blIJEl6?ri4+yPy{rzkY6gru9}=ciGr> zB=cR*)%X2r*je>I7b8xQFeaHLWcLx16w?M-DJTVAUHp-&S^ht@(xg>&1DuQLWeVmGe4DlQ|oy2W8jNAl|YP z=whEiF0&)Jb>gLh0$qN|L^DY^_>a^Y&;tKZpa(Z?{sspA(CmF5mN|Vd#9syxEvW23 z*CZKQ?Nyr2LH9+yTzbA+-Zc(Y)3PI6U_i1#MO<+5&j!B%x5w*hY3nBdF;2-)##82w%;xf` zzcoKw%GdYU#&^_Ot+rj_lqxT?qiCa^KCrgw4scP-LDyb&x%Eg@J+W8`N~yp?8m>*i zyq>l6iO|za&I(5kN|Q$yGk2k7ZK+x@8TzZCgRBe%gGIu~yrWRGVs2SQ?}jwOAUcd|={b+NH!6_iIrrpa^kf#Y{fl)e1oSM-rDnoFXus7Epnm|yKy zcl~Fu0}9sGzuyByfa?16_E{OhT=HeQRqeBddlhgMdf+LJ!&5wyZJ5GU0B;HEi_-}U zLOC3-+>G{}6ZgrT1GhvX5YL9JAVa(D{1o&}LRdQ`k(kI?1w1&pn^A$>1k2;LG4_$= z%3?l*IV=Zn%-3=YgBJ&r!wd+6|0l`4hfv6rI4>w6arAYzZA5kx<1e~7I+pyLmvbWV zVn!{xh#J~Gnd*GA@wSm==GiPv>DEh~9Y6E+n?5Dvi!7bV8(mL79_?|bX2ZsVm0okn z8)KnlDQD)+@On4J%+2w}->h&J`Ww!x*>)uNtgvxS~(YO^IP#n|D zJcQ1RGuf>w=->3M#Zmhm@j~lP^fojaQROFprJY#@oOprS9)3J}sACLd;b6xY+q9BB zGY5?Ox<~cnuld|p3t#$N_)FmmUrC%C54@>tIQ<~Z>z{G5akfS8 zA95S;fhPKpfkZRHdf*BpG6#M7h}+!@l2cxRPaQM2h>9rG?Zom-PwTJJ zJ?|kI6#l9)NwVq}!?gX@(!$?VZ@p7hy&bA*EPd;EyaySGsBz~UcXfG`|7&D$v>j)4 z(#i77)Ww~8_`;f%?Ork!iq$&1^g6<)LMl33FPQGR`|QT9b2^f28#2t)^3YN>O}|eK zB__4|w3*)>wr)azk&%himkVfm*JHiqm&*6z5@IZT1Eb2F3Gd5{G>4-4ignC><|7$0 zVtY@0TKkjcRpH2luS)4Iv~tH)=8+{sgNO)Fxm4z;TPBOu5;NF(2a2Qx9goH?inFLq zmZ&+|?whWvfP6jZMoee3e{9k%ZoVC*fgLXlXP4i8|RE>k5Pl^+KcGdT}zk z^nX3ebi?tV&86>ukNwa$eBn>L@&k~ht-|h|UhI-pJ$TxMddI$oJ+oUf+k=d~=%$;|Co`4zU}U`XJBC=*S!Y`Q!cQ{| zW;LEDBh-r*ZC$}H@OSw9TE=^!-dVZl!!vGopMqh#=omLs72iS>ul2(6Siga)yW>>V zPp9{VdioYCGoxC+d?Yn6zF8NZ4)< zbyDYHMxdT3f7D_ZNq^S$n-ljudRD_XH{(PyR>MTy0GCob7FkUN@^2F72D4Z3CthUoZs+>QCU;gkjfgqUU@!$ilZ4 z{K7~XS#Dh<&wm9dkO|B2u_=QQ&Vbv(;AOslgpk_}0f1DB-+0vQ@8wd=Kb-G2CXIf~ zVg4!q9}i9$|F1_E^Wp@C2}6pY{y#iy@^z!z;3avNY?88dpvrTZ=LGjX{c#$(CZDJM zQ{%zMVL+jm3%fYPYmT%(4# z{{?+B-wZw9Iuv#o>4sl2q%aC~D6|D-=;7)8(PLSSmLE*r~6ZNZ@)gBOEbQ$Vu6uAIH5|C}QW5 zxl?Z$ac9X@OKq54W;_%Pk6gAi5x>|fq)k4AVb}@Ecw^8?$@1lHOW)Zt%tRy=HxgFZ z2y6D1g?eL|OvE&LW7=%x$2#;g2v)QCh>Om}tsF2bFW!!ag%N=yHa~vvEG>4r>WH zs%Uh4oBm)zP*pIR3AAOlupRpG1z%xbh4iGT)r52*Jz_~HP`M*{6i@|W3hCwt8KuM> zB263fny(Xsid;g5z}D|K59c#if!z3kpzlPM>IGRSDJ~w4+Mnn1`eOrN{~?! zkoMF_yIg6Log@$iM2#S&6|s+TW99a2Jmy?aA^rMztg*jt#iB`l+2S%iig7EYPuAb2 zg;9Jdv|wMu^16mnt&dTw7Bc78-tuaydf!fFW`9^?tg>hOg?Mu87eGA3tqKnwMJ9_(16GrB<#5Mmf4(kx zZ{`uw33b+N1!ays`5U6@>5A<=FxI~>HsboEF+C5P)BpFkZ9Or#I-0Aq91Kl>SIn$) zXj54*0CT}r__q(!iHLLKp++pO&$^a zQ%2foUtklFEaxh?3poRxLH-|2MeKqMVvKJ}e3BMbb%6hICe$EAM1ju368VG;5Y!?; z#N!!2LT!DD05%G8V5hC}DfOz%Gw{=D-6 z`o^c+N^62n7X$oTXYrph?3r|YTCY4i&q)+h#A?DC#$PZYWREl$BoV10T=DPuGGLt4`Q=j&Uj zR^=b`VJ+$1G#rv!2vdoiQ3agcvY24%g`#Y^^sB!1_emeL8SAu@$t|4qfjMSI&GH^) zXVd2=bQA0Z+-C>cQI7_CAw6`sRxzi3J2SgqmL7>Pd$ajIe(?kJp@MQzu3bd>|CAfK zMnBLjM~E44umRE|cEGnV1PaO#9u`MH70YS!KdD|#ZY;b0_=*~}-SWU3#BIp^#*c7- z%?6@nWT`>-6FcB1F`{Hs$#gx75axWODZYh_ResjqxEw-qhp{di?Pn%9%uS7qchA+8S$N+(bg7dW~IzeR!-IyzOuL(`UBocXNn>|jqft*|9>s><0BPq^_ElXpL!D!>cF zz^e2&_{WMaeBTOGI5yK~?tRECJrw_Aly06*b-KH6kN3l4U2An@oyeRqPCsowv>%=5 zJp7bvm8w5Km%`Kpzx`VHi}XhXqL@SjeTw-j%{vD1KwLU_)sKa4OW}8cr6(Su%-p!c_ad1S zcGL0Rt^bS-V~3_`4xa3dxwl_0hy##(pK0`m7A%dtUYvezd@xJ_S602NqEXmAc4;CU zj;3j1%RgS-^n4Wcb8x*-J>;TJv9MaHfsb~ik>f2I_nbQOc&;n4VJrzWXheZ>g=yy$ z&RpXsLbRYo-A2#ZGaL5;NHKwS50Ef+ zQW##}H8@Q_wjA_7zZK{`$AT>sBLOq@oCSNRKt8hI+njk$?p(Whjfq$%onJ?+JpudB82z^gzXPmy6L_iD5Ge9#6H3%?K(c^?y`3>8y zKhu%Q=fBA?B;;Msez$DD?c4RMext&0z*vEJfFaq|4e(KXCO2ON92)KP)#_X2oF|KY zBEVeS)B0Y%GCle?NZtRMu3cJHtoVz$sTN!pL`6^q4m?<`>?%3 zv&L1m;v}l$>wq)ej9LqW+6wILzX@4B5 zUwDUFr_1b{N>IaC7pUT_@<29XB#bzDAEJZr`_Z0~C9I;)7uF1#qh-5X$yAU{R(W^d z(F~mTJDhU{Ly5e1kkM>IFtvoLe`T$C{Jm(0C)*$)`>9%*t7IXu^vJ}U`XN$a0wV{PzwvYv%%R5 z_#lpjcNUlm_tVE4NS`){DIt8-BuR<%Sfz~qx}FmW({DB`^U-A1vyTfseH_}YdOO{7 zv@~4LWSx6n3q=+JHlYo6rQwfPfDl$@?=y;?7}{u;$Q z7=rGf<{xQXFk$?e?9N+u3B1~{vvV^NzGGvt?!D!84h}?^q6Ia3t*K_7T$YSRjcm<3 zpYx@cWjkislLmSsX*{*P3-Yu~^*` z$rK*)do+HM=OJ1tlDxp*8*TE}u-D|L;5RuC+}vYheI3a&>DJ@834_Y9?_N(8jc|lz z53d_r-ze;~N0uE|^;gvq$6hi(z`3O7ia}s)Ytkr`#CWj9d$FJRg8kr{KLpp5{BS9# zrf}UaS#T{ovFQK?*#gMHY3?-v8BmcVJ#Zu(Mi9br@$?6z;TLeLv(cWwx-ceLX>gif zK^Wm&NhZWW1XIM~_p7)UNdTlDi;V{2VIBblgOCA~`TmEb7vj!v8)CmfDmWi-0+|=F z5{xIbU8x^r)J1P>i0nS*TW;aaO%4K6ECzUs@eZ`H?9dD;a_eV_&{~!d9y|4KtTVba zw6u`_>F)kypV4=2ys!1eT_XvvXmp?YoOqG~3FPpeY4hE5--C(}v9*vXSb|(_VX30o zwc4+4`+gUoB}&bI{}T99$( zjPHqcBW)K$k7DK?)7!rh`J8+>LY3e?SVJBTNk-H1kQwv!UEOMHDTzn`XjFH{E>o2b z7Nh-~c70bg1yeHTW$H7xetO^iwOqfNIcs5c5Kv6=e~o{TUWCls^#=Z74T$0=sPga( z?O-9~B)Oi!(elgXXeGppUv85j+Ef?0(Weop`Ko}c9xPW2&a2&q=UMIK5Vyji;p*9W zxKfzC9Daapiv%6_8k<62M27Sve(+v_1L53VjuNZNhz4T7AoWL%vvB-e`ZzszH{6vM zpblihX|q|TQwsf+s=ed;A6Zg+vzE->esOQA^ScVu~e10(+w8^@QV>bcJ@@SvhsLx-H9Za?Pr6)Yv%<)718Ddt8(9>1Pk9 z&WlL{=%07rAPwA7#AK7%^>^xnth}PaR*M?R%hYwJ)Pn=wAM{nka+hJWacP4m~yyci!MV(&xGz?Zmn0m0iL0pd4lsVnub4|g9i;dlClQD|b6VmDB~2ffF$sCRH$uRbL*xebwK169 zh(yw6qhIba2&Je%bGV!a7oH=Mi%xi#a8JH3&0*3!h>dceg{#y4Qd}~}+Z&V(e&Bi= zyZ~Yz0ZShMkt;2(UFtz}u;BCWtmR@`xptH+lyj$a{(2a?g6NGZtF9y`b_3#mV|(32W(6b%mF=+)cESN0`V| zEWWVyq%JN`CX(W0t9IFP4-ja#COTR20t@48I3%8BdailGXw~C(ffq1-F413PIh2nc1 zNnEINoHf`!lE&s*eo68a`|pb*PAQRVv6CcTFKGSCs|MrUE0^>pp4gKy7jI_Wpmw~% zK`=ZB0o5mG4l{!*<598meV;W`^{c7z1{;Ye+ufJu&GO6W5^<|*G4p?YES=@Jlts3c z%7=SniG#(kT(6{d?niMwf>G_B$@Dt4!1=Pz|LPpPfvbYzYc3#DssSz&suzx&cO@a_7Zmv-G^)rDj! zo#fPGjm4FfU8>rsDsMl&Azz(qHDt3cyuYq!ygzhp39W{i)hmMgDzl-_g-&r_yU?K6 zh6eBTC|p0V;5!R`$cf^(gYy(D6bDN##BV*o)_~JEgdpbftj(|EnIzQWsHG#wyFjuNo8mJCIkyA@ z9+(ED6OT`7Hop^w5Fg}b0=M$A^45HA;naQI-Q3NFlWS+5Okx1VVx4=VCeir_>5bt? z{6buvT4m~3sP%4Xrf9>eacZEmHJ08+6Rmgrqhnc;pW=DjiuUV}guVJI<9G>il6y_P zzV&BCul2i)bo6~g8&6NLyc9AVbG)4iAh5MA7u!R5y5yuU&#Sfgfcn;Wl*u?Ur{2g- zBXsb?*PMFd5f;vFn0`9^VHLYYZ!XyWLuLWD&d!#%N^6r{@4!F)Xoj@eb;dqkENjbq zYLaCEi!LH9)4!cAx(qq;i4WU(#7G3y@g)@WZBTnacS>O~5MqzD)z)jPh@xNl=gRK0 zq<9C(+Y7FY54)DYTT* z{e!%1On&QLd{)RSck4ap_~@uY^_C2iy-|wJcnG%FpK) zJsOQpX;&Me?!=Vym0lJhd^Qxy6rR%RYr~z9UB~I1kVx&TRC3h|H>lCvWX%Y*wzBY{ z)sa*88@&t-kqVIhou8VaHWXr}~=C*GoQSbJJny#5DWj z+3>|^`R5}w@`h1|i7>y=39UpJeECg_v+#V}a1%90$Gc-gXpt2g-g(=36;n6rKAPoa zZh4jZ={w}y@dd?sy|Z)YJ7oNB@r}J>K8Gmm^5DM8uP|`b+_3qZ%el#Q4{9tqLUFDT zYs6!dn6+`U(?iGds5DimyaH!T1}`s6?+vFe2>@?Ofab&l6h)3sqAxLA%!Z0Bh}tA( z10;mpamEse?3Cs~FgF%m)MM(sj(&CWoC^U?3NJ?EBd+~!XX<26oLS{_Zm7QPt6Tqv zz~#1`9$cS8Ax*blJJA^1W@e1dW~%GV;l7TC zH^uVSjUTMWI)^`eRtKx4cVQAur?d56E9IgxmUpIdUqO=gdDgr3>Ae=xb9U*8?wC`b z`iriERzU|kPNhsT=60h#53OTx3kqy@^Zb-P)^we6YjweOtwn;Z{gn)XswI3ohY8(j_T}V^iNP&Cx;ROW#Bp60h!;wbTULJ1;7GJ z5Vy@{Tw$b3ZX_@p-<9K+h``x$=L7G~9h1zA2P9~;#RxfB%!c!C|3r}ddp+%8g)yuo zxZ6Qcz)j0N$GGtHoO3|e<^7~9YAhS?95vM(!qxiSv!eyuo8a13cdFY~q*yiZYt(_gIc)nDBwtsnRPyIdoJ3`2>YW)J@H{sS6a<|R|e>%o4@Y>0$ zD%q|7iC>&_Gc&mG7pCpne}tk4sQ&t8A5`7=#Hou0FLjgIGuP7u%sSVFBlbpSP~%?(&R^tAyMO-$Fs>S# zRFk?Kal?>|-T#~U-obCS~)dC^$rYSdC7#X4)E^^;ea?Ah8 zOZfvC!h2KZLr?33J4SbioQ-4xj{t22$_5JJxDfdR^4}xPFfT_XDH$dI;T6rO!SbnS zd8#L4CH3$qyKl_o-$I?RDmKf*!l_geoyc+RzkpNQRW z{QcisfBt)@B{kE8;_*R*6PrG5tSQ&WTLv$9r=i5aVak{61^c6 zHNxqTX*jSqqQ+xJRF`8=PU4}6kz{MHX?zlj1z|<#8}LuUju(eT()x;n@QCXO6(nK8 zaJeIedU_1b#Yf&xFFv!KNk6_{rTv5l{lr!S1OfcE-5+4AC){<9s}VloR5FW zs&lNVr4vh|AuHy&$&fWNxA3yTzSu2=#L_)yvo@?4%}noy5(NzZ1+&cu@)Ah z5|?Vf|4zUaOrp(vF%{#11s_}RM!RAZ)LwEs5Oj%65+pEc+O(7j7(x){1vZr=RlJ$x zijxBA1h`IBVh)BWG2=8dG~liYQXuW$(|IUvYaSpBaKUhpyYNHem;?soVX{x0AwQ7E zR|Q^C>|gLeQdg2t-scb1r{KLZBFU$eR}tfU(PRdK){07`)%3CEd6(5JJAWsIgR;X7 z^vsNheeb|1!%@{4Bsd5F)@03$$sz@ywXND+`5}`jkBn)4GhccWZCZ2i88C>SffauG znxm1~duP0XX5;Bc;HjolhF(;&mfx%t z^gw-8|IMYRn;XJPeb`heucmUtH?XdccST%i>Wxl4r7g3%XJW0IYr1Z40jl~soRYJo z>d#K`#lgdX2@^Xecl70EYwZM7Ss(4pCQRY80bZ^eVup4n&<1F8Z%2JX$Kt~vD$&b^ zyR6Fb{ng#o64ll4OXxI};vp65E({lB${dZ!@1t9&!4=jJ$1Wz0Nu~0M z1&^?$;4iQgYI(*QgX|OkMQXv(O6?Fp3z!Gi0L@9l6DT0;QNDnU5gZymNluf<<4cTV zU0oYO5Mz^og#8iFC_@+%h%)g*e3QA?AY$?Dpo0wF(C!enx03J&>RHhck&;FdP_QY< zdc}zNX!15e(o0rDEyX*CB#u9hR=V0zu(o|@+`Z4~OnLF>i452-7dO15-a@N;;6yq( zzHaDN(v`l0-heuTp{;dkFXPTVURKrq8+3n~;gB^K)}bV4UL-7PwP*cD)s?2Qds?@q z2Hx7=)oqv^aiizgGPkNW)4AD*i0?sl-l_k7^{`i-T4&~sH>!oFGAxP6giF)EMJ&h3 zdto(wkXqttbKzMgePjmlsvqm9q$758uI{WPTFi9UvrC`{kmSz?Uq35J5o%Q`vs zXwJZO`V7J7Km`Edq!=+R*pXyYH zx|Y{w^SWFw?%~30Drs7otcLpMK3FQHaa3r?bu`?8H^SdzK1j4J7s6?m-r?uly#Xm? zaUdL3U?lxrjal6OOe11t7z$FYUwyW3 z`)vH#Ss-F+!Hp=x;&zgEv#Z$z?#xZTc@QB9s2_)O9qDLFuJ-+fPLKsQz(DsIGoOU7 zxOqF^HS54K&w&y<_v#W$$*_|;b&@*W7>J+T07iH$DAZPuP1#e4K=#qhBoUR;LV@%K_QBZZJlO@+b;gxqu%md_ArYLvBGJ0jCQ`$zb7J?L)_Z@G3 zo#yG)(b#t*;|F|}X?pHcfV3mpHGZ7vEUKV;-{>1FB4FHxR&#YlBR&;R22mLS|i8PWy+_5%u$> zvDR0nlS4-TYoASam=}jfz0jqRflJ3Wt4#Vsc5%x^%S}e16Cc-+?tzETO+P(S1DsS> zre~QOGD}C1&4r_=i=lq~-I_k&+psf(tA1~!7jaHXQ@z`tl)*BUi_YOO;^ihA4A25a z(|gU8W$Ih+1897jFYOq&yl-ElX7^{}os+50&@nc@yq<<|b!&~%35^--H1p)za^E_n z!0I(!-H0MM!{T#D-`A9-_iHyfYHMd|#p-tL^DX8D6VyHh`Vq_V2RE_drzU2Tv6o{47N; zPnNE7Fr7a>78V|W@Q237FpmJ5z$E4=NjM1c6l3E(LF~b(0=aL@nazUZuM!Vp(fzL4 z$#OvJ1y@~u>SqnRc6-q!b#T0g91Ry#oh;-}V)`%Y+PkO_f6wU2_>T3$igbPWdrJ4* zx-fI|&9!`H*W=1epbk?}?qa>`z`GPXFJ+1q!}MuY7}q^kVwN*;`=FUT^_$02x!BIS z@dZ0JTtC7`xc^y>bidwNfZMr~Wka)D5$U1CDRkX}gd}u=a_bN>;t7*K-IXb2VnP6A zDj`+x`Kn)W+{91R()$W_GIjfOg$f5kQ#ltmVmdyg<2O9BRog{&hcgYVy~jVbp(}oT zC~4W&332H7I#dAh$KSYhA=g5#2^ zLcWk$5~@_Lr`&N4F>w8W3vwQk0g59R;7QVl;C90)1+PLbpTNyC^6jR65BGO0B%T~- zOHNgg3w|sh_wt%J3Gzff4pBp1kx&pMBVr1YE(VDQVL`me$TLjbY_U83v*QeY8lFrt z7WPgsukHL<%2;RfSrszA#`-zbMY@L z{oeZkK%c&>)1|;g7o+#qA`*G<#B|2{>nA(A;&GNnK@q>F;n8-uw@6Egh;3I_T~vI? z%KHkW1`{|ULD#DRtM6=wsDU10?RK;!<~ zLKXq?5T(~hY9%gw9;%o>^wSYQVgRurh!28hNX93FLv)Dzz_GXQqU8K&B6Hpt3RhWf zG_Ngqh{XG#xZp+Fe+X#gLKPH?l0sQLNpQXrPe?oHE(YRK@@Z))lIWdCnCJC4H(zNj zjUGXzz;z4#cY8!`l}!A~KUwR$bFyH)>eZR-e)7kCQ>omR=-sF#)R8)Q`2jzpr{0 zS`z;1km|YoS<*O_xCTlfG|XJ}dU}WC;Ap{m`tQ2#?aro5!IJNVHd5nCBsIpeg9ZbxI5b9?t0_pNoYMfoZv%7`^>>x7%<1dS26`eI{r^g81HPdITY1R>!B z`4tzC`^z`vs&bT#`C|>*2S8EMJ&>a!-R1@cP#2Y>cyg9qL%tc4?u76{0(yvmS440U zGl}M$CXbNq2glzgp9)+}F^XHkQ}cpdq=URzdyrA;LPKc{2Fl)B)VkG=ukfmg)<1cEQPVt3 z7;?Gz#l@FTgkxl4+q@3cO%EYeuG~EgilhQB_z_e z!&H!LY_X=N9Q5g|?Dj3fMjY5QiEDvA??#f-IS~Af8M}z*5Z#r(477H-&|SF%eWdn| zg6{@VgM(rJE|je{%~Y&QoP!)z;K{Krx(}Q+J|H+LYzoVei_bw4GdO7(P!lSU5XT55 zn380U{G?2L5S-u7K7&^`u{4Pc68i(e+u*ApfKZoUS+XYXKsXY!e^lRJjNx_(}Lv!1!n_>U#wT*&*iyY+Z3TlE^@ zBer$DnW7b~3jdry^B%4~WG>bDt?~(-&HW$)HTfZ9S?&JR;u}9Va(yvk>X{=g_aVj) zF7A#cjT29}pIQz{ePct_T5qp~3Z&v47ye^cFPxv0(c!TrTIs&DQpXRfr+ryQP@O{& z9KPu-{f?K2qx@L>`lvow%BW;CHE?Y@fgplL877(-?yzgmuiHpQ*Y{aoBvUx#*qMA5 zu^u;*8nDZSa9YMo_NP;s&8euqd~|CqQ(M`Qd1_6ivMHX>#rmwHkao@J{9!w0HZz5( zyrX4Df_4(Bl$xk(!ljVUSslAg98MJb_dK~t00dE#!Cc8;5uU*k|Rn;B9$bV1nz=j6u&{!3uLP8pRyx}G*LAO z1||Llk0s9(htQ->6q;)zps$DtFhU}=qlA=s|0h2t_JFzIThK5FMfWh&bC5Cq+T0c& zYR3!Y0gF@4dpg#E*4v{4!?T%0x3t`WUHBbH)FOfB^KCO;6g6$QF$5sw{R%$8XfT^c$ z3jDRPc)Wubo;Gc#D~T7=HtHrZRsm88=*{`mrH#%rGg}evs#s_LLWU3w^XUufa2zwv zY-KUA5CH{OXf75%77raesZ58%7Oe40;r2df(GgimUu27PyXU(h;^IjmPq#QYNY0My z2V4k#76cx_Yub-XA!t(Uk$gyI!114Ek;}bq*9Z9~xuogJ6_y?}M?caOt?{-e2?|k; zIxk-Y!A6o7z8-)shzr0#s5>NY#g71k%OjHU1fGY_%6|pK3Lr?{k@QmbM@#$~Fyggb zT(9mSs{6;S75+h#!L=Qa-j_pIQoHY{`kOSi@#t4YZ*76)HQ-6%%Ht*5RE?Rl zFKHgGWRfg+Qb!V<9RQQbp(t?)MPkNZk1bqWMYXT&mvVc!!{t)zl3!GJICg{`g0C8n zs&HQzwNcCRI#Xf4X4gu-U!2@)nXzcsncb=y3$cV^PkeA>4z?=p(`>3wk;nt7kI+#OJ2jF%^Otb9T zZs_k^0NWF|=Ft@nD)kzB{-3F9X-*JsjQ5A>;ztnk6A zX+-AjIc-h4GjA*Pjg?lZbyBg#IH#IR*Vit*bGpoWLOODb&%M;q3pd!_l5ySo|Vb4eY9&PUP;Z4tqybyvMR3wBf!` zz*gzlzKne&qilHSTlg-2ne{ysAy%;G<7;3EHeI(76u`CwS{XMomGI~SatN5^5&%EB$wDmlM|3*!#r^Jm_-{zk|eO#M=Mbbhx;wx&EiOcUVFPpY*#34#*AEYF&>d5 zBHsmPma>NeV;)!*H-lpkUQD}qrCG~61~$bxx5pRcV~|*2jYN{1xH0iY)S`6VFqKhX z2+DpsNv8~^O=vfevlhaYHK6t~Z_u!nuk zO3e;yrdc)tM`e0%^*J{`Pk`BQAG?vXzxvEr4%x=y+NYNs>`qZkfm||z$U+$NK4Ta% zI|+g7tex1znz@A3uD3kOz;2T2J9YT_$q$pncO z2agd1=g*atbdx(SQKX$Bw39|LEjcm_V2lcAyg8pWAS@;$3Y=>jdyoQ1Mw{iS08{O0 z)Hbpy=O18FSQ8E>h&_T(kz!DhRs|9vkYK&hY6z$*z0z<)2Pqg{v_E?4-{gc|_T5Y8 zT)PmD)^@-!uE$gQt@@ioQJM_Q=)RVRVjkZ5kY3z+jNlIUo zDpl*`7R$)rcqtur3q0b#g})PMW>o1#or4d!4-V8*3r;L}b-{m?*wEGm1W=F=(LQ4C zn_TrEkH7;16OrVJv#(~QQ3>!g5@d{W72BOjj#XMqf>6n`Id!~=CrwWvCSI5V0_GHm zW6%zzTETcE}FDtJ^s50Sa=^a zo|-XD_M^q(N2vLdsViz={z`^k)ia!kvW?cOy|H%-2ngs~jf{G9IIq-QKPlU-w`+}} zkvq=F;x_;Vk=gnm>vldH!#(&(;VNWsr`0Q#hjSpnkX0(<1B&}5#6QN{g z;``nDK+GzxbNwP8zGCH;*lX8C)19f+k4}^rO+sT{5Ugsl zYJ8}3Dsk2ok2h#>??Hei6QkWSr-soTWWz9d0u0YoTqXP5jdiA=;_GQ)Wn3%qud%A8 zJZ-W?#Z;5MeVI|7RSbHU-)7EXkgsGv-ilxw#2&Um9E6AS@PZ##Adzd2(eOi94>AZ85;EJ= z;HJw~FO!HRL>(#`6AAnP8Kra)4}>owZ6$x;HRVUdLIOS?F@q1tb1=@)b}d5UBkwBs z2Z0Mf&L4lB=AT-)#-1}$lO01F&QOpQRc5kW-KD=cyf{2nROvp_b9!CImFu@xtj4bJ zsVgGmgC9W)+4f>j+vA0kiSBoL;`w`46x|n4JG4G%>^V1SB9qfY*HG=vsDrH^lyWa0 z87`=z_s@4eRE@7bE8-acwE|SdKTGy?+sNe&Jd)^MMVG4Q%(C`L8>w*$)oTm3e%6Sx zLbjwAZ&sC?mZhDr4ToxLE?e8W%sa) zl?C+Je_EW>nEg3<3Wy)LkrA+YDEr^$J!+$L|ak8qvi;R@jjGIc~E{xI-REn z>hU|U@Wcto1PxEXa`-m}Ab*fNwEcFN%UHXAB4w`Rz@~&U!WzO{;>X(dK_((*Ei*JG z;3`orR3)5Y^)IlZ8amxiO$N%&-CkgM#P|3WSpzBC`c)zSZpDiAKAG>#zRN|_ub(84 z^C)~4_4?bT{O>Ld4BGT>VYN4m-gZUuFe35N4t*n zS+X&vod1{NA<^r>g~$#cM2JPu&}hf7hji5aQvhgtTML_G#8g?Rrf))>da)?#c1?bYOCF0>hjPw9e!cA8jHlU&)p7Gf7v3thBoR~7ee5k z!17!ctFZF%dRA`|i)!%uWQIy~+=hdlOmlGjgvEVu!Mi~$$_uYLXno7wlq)T#8pv^z zaP;$4P$Qtt795;}4YJrEj8v&?gANIii)%eUM$6MUG|6PhNdO@Lz~&1QV00P0YUd;K zt1iT7FhBlOFQiT-5|vy@-g(E<)pPCRweNS*H%qXf@WM6Q*wM$R^)x zYVYXZ>az+ezCp~XKDEAex2k3TtmeL4bE~aaCh3ZGy!^OVo!l69jUSW?{_|z;{x#2~ zvqPVWZR=B4)x1?_uvu8C3kGT|UOL`*oB@5jUf~3A7ion9^lGKhf8IP$XFFwYWOdKD zI({DB$=%7!7K60qo4fM_8Kxay0TcIqwNZznUdE^Ic|EL$LRc0Sy0Q&K9)K|gHOO8Y z-d9A3#qMRO9tUJ42U+JZHmJ%gdQ^E34S@K0%qEc$*;9MW(t)}ALH4ls5eOEpj|g2X zz4cf@r6ap-@2N!G8`@T^p>&4GKq(!yY00UZ&kGCd-B#OCbZGb7WcNk|u`r=l$V?Cak`Mu0A!Tcj>c~(}qaTBlr=Tk@=E8_Tkaq?nJ5{l^e9(gl z&a>T76~Dn24%`^0Pv8pjWwIh^q6XD#U!%BYvKZVX5f?nSDtq-PAYxH?&fD1Y$OOB0U?Y=*?+{ml<_&_YIi6r z-HV8IDm=N*rkus;nB4kv->aS^XdVq+svF6k9UP=$KZ?2u>{F>%4^##iq&7aW>C}I# zIB!)P`>~5vY@l_bj6QSb-yeI*?7O;VyfMo)&y=2#j?NPq`|&H&9o=f#aR%{jjP%%M?>Konf#KBaQZhwXv+=s416*?Ut^lv`JKms&rpl-YTEr&Hi)LbqUe-NTxot9K$SR z(7?Yo*1dM0y@O4`(GKgTwF{|_+6;^BpHT}Umv4=`|EmH=l+OEnK>z*ptyhO3|_~iJ|5|xil zv&f{M>%G3JYrw-Q=o0mhU7@tKoDcZwA$DsCo?X&SRa~lXUJx zL9PBo^_|W47m8Iq_!k5gXcN&D*O$%hTaoMSb;G()KLpO-x3d?Sy7~dNW1OKP^RQF% z!uuD79pm`52^G3LPwwJob~W)F#2SV2v=KU#?G995!jagWiC)(PMLUXIXh&38zVoR? zi`>kLe(gB$0RL(Vi=)J&=KGmi9$_b>a`t;_y)LK^A1Ixw-Qat-Z7bLCb1PX zO^jGRQP8amc2>d_g8vyX07iZ*jTlgk#o3~jP#`W{Zx-@8Tsp4s5%fz>ygKfv9nlVc zS)=L9I#B9Nsk6`QjfRz94@I}^wKMMQ<6EHN?6_h1mse%N=jJDEw^*GkSk5tKKmc*; zW=hTOSX(h~Ot44c304ekUYRt$ZZ#NuQG>VV-Pwl87((m8)KG%f?9b;0W#kDCdw)Eu zZ3yu?v;G`=M+^sJL?=o}OdeFPvAv3cUlqMO*2@B`@MW=FeuXyD8oD2}p3=sryh-dr zoi{Y4T6^{MRgvzEjKyT8r#|w-U+-rg@xYJwdTf@OLPyv2ufA*`Z-eckiBtU*Z7N;( z?os#U$$`olIo-L0Ni|plnA^QttwrV|afP3^)6q}_i6b_rR!dh9E=7IN)cH-W_!`0B zL_elNy_A6|JjC?z2Ltv0fE^@BfaJm;r??8f?3EHX@Gu;1kVoKPD3nP@B`d{skhccG zN=je6h@>t-Z3Tua9T}WaK4|8*6OurE0IQo9fZ~t*m~?(5$Vj*mLYTCbgArKqV}NxH z*`OZK2IR;1fzxOn=~RI0vGM32S{)T*je15idNf7h;*337tG%RIjPhH*c&+i6$h#b^ zIj?6f_U(d~-@m!Yc#6zuXXvq`{))79jJ%XnF`UrSM#5~i9)hov4XI4+2~x&! zFqA0Gk!Jb9Od+1g#bygCw$fO0>gFRloxNbKYkb4Klal(zl6!pn!u+;0l;VpPhn}qLTKZ<82`uCWuZHvX-?p*X9({bZ}jGP=cL-A9Nn76Ofte1Aws-8o&-7K~R zBtbI7Te~O={fzZ-trs8gppt2H2a43zjXx@Zw3d$-ZZistUu0DG(uMGsozf=c$!cd6 zs-*ubmUc2sk2YmHFq2XW&b*j(MR7c`1%HI3_U#$|CVAGQljUNzf%GKC~SZf5y$U z2Uf-N;0W4mFoF(kUVt6(USglTDh4z1Ji~1rJ-Ln@sSo(}%ewY9gNL4Z@$HS)s{_{%BWx zLVf-6l-^p^FZ;lBxbg5EmF^g*`jK2$jC?`6)8jFn7%fRVE0fM^XZ*XnAdw&!snymGTofi5HM8eTmYi1! zs~eVC_f+61)Q~~nv^0fp8-|fg7%3sB5#VdzKy%>V;dz$ehb<$&I2%sj9yFtFr#B<` znw$lBhQRJ34i;SHU|>(V@g|5pxCbe5i8iBnBFb&C zO5%uw6Ooi52KAq2jQHa(E4`1IU$x%YSbbn~b?+{1KcR+Ea$2m7);)!M<=F;T|HBjA ziFe@s7PlK#7~G8W?zzp*O!%&vY(1GS-Sf3hK=a;+p}&MJoC7X$#%+ADEt?7VEa^%Y zo#(AB$KzRL-t?i|p3HuT;D}(md#HIQxSze*j|@f^-nsr<>Lu>ChwgdnCV!W9O40Qz zL+R($wk}jS`&a6#*}!$|YF}^s;Ef$ottY>DPBCRAhJQQ28lSDY;?SC1#FidbX{W-n z4tZ;x`m=~KmD-^r=R;mb5Sh-ey_HdH+C@ca*VPd}QEJP0*!^9>0&p3ji*#qPNudbE<2&AahRNbZ0Ek6cx=1@f{s74byOgM5%lu z6DF52g+!Jrnoq&vJTV)5H3s9i0awaeG6^Y4!BHCc`T$4?T0D}hnC~a95pf!)jYYDS zb^=R^@CV5C{j4JRQT$AMR7J800d>VO_P}<0hYs- zb86vnlnu#`vGTsqzHb7MeWA4oo;i%sSDTy8FS<;JJ;MA)0ufmiTEOW~9a4YM`cbZ@ zbk_B>S*uSWa8Y_%tG8S4T9tRJ%D(Xv!}_NO&?PE4|9a0OPQhNcHWm7`jto2w#QSh} z-3kw`yi2RDYvAXAo^QW?+-4OQlb1Bn>T0aBi12o?bSa8<$HN_qaA}j6rR`RY?gLvx zv02#TBP*6`GZxBT14iQCFyxdFBO5fQ{>u<|zNr^yP5X|zVojGbNH@U>BcRxf-Wcv9 zA34gXeIYEMtNr-Wg_TvEs@zx8Y5b27jyi&NDkG%1GmE_^{y)QkzdG0MPuU;>gxiVNU>oPXN| zNeRRsf+`4q(*|vM9`1o)B@?>sP$jMhr_xWk+%`ga9WhLCJorGY1cyX)lGo-Fw9D|o zjCtqieG@Vf4G$0Y~V@ek7PEysx$`Sz~U7iZQ| zh3HW;o7`cecgzBeI;FaX^HfWuqbtQ!M6yO5W_lnZ{>A(RKE+dE1$L%Wk0sA^%+zcu z4nXgfeS0w*cSyGx4))v(8~_yd8A<9)F!D;lA!kfX+qS%Sl87!3TqVN;hD`xaa1s(N~?W4bu`C~8#o=h-=^ z4yon@6xtej?KQ3XRNIT{nQS945%aEFspmhT{@nS*8j1kgM-U@b!0r<)!py( zuEz^;vKGpEKfSi7%tK78F*~PN2f}3AHE*aYTQPLGssHAV?t~Mb4SU0*yJMzb7+nd4 zMqT?de?6667!L0(#6y>~YmqO7KLPF`aixoSH0c%ZTyTX{xN?s8cAB<=nF5v5VhJEJ z!V3@?2EY@7`x$^#I0&g3BnWU|Bn?4+(hehS-_ZUVgONl8OoZ2_fhYB@L>oL!HQV+c zEGmZNDy}C=4kM=sXxOQb|#Xn9(G9`<-@$0 z#(a7{R#T-<^c1}}>B{fkQ!KXrI~5Cr()jUvoYLr-UB=^TMxAv=@tF&AovC9=y+fW0 zbl;=<#_dRF-}y(?Tgg;7vGg+K4)v(3kj3>kZddB=M6)?Jqi&1T7hb*tY00eh$>l0L zF~RPp^!j~$1fg8)$%DBo-QtZ)GFIiXUVTH-FNc?(p~L3&z^*{JH!mvKmi!G$UiJCH zVY~|Y2SVp0E;MrzaS=#80)~={)lD<`@MPIvw5&80>uuV3=h;v5LD9%*=Mr$Y?OD)^-8alJYRF$G;R=xs>3x zwMb%8oF34l;jaZdV7sVz?_TgAy7ym258%6P#ZNq`e|h?O*-`d%{Wx7zAW z-+iRJgI-ASn<$V$3xCglixi{yg=?JpUw@AU*!;d8j;ihp_-##-E|xmXX`SnLO80X8 zfkl05CZ!4+)liq|&gCNh5n|l z8tSVb#Mdn<=bwa$t$=hlB+FQQ#; zapKL2I@pu+WDpkdC$Fr>FIe$+sm>imUH!m{$9)V+bth71^PuJj;ulY*I#FMp#1ZMf zHH8(!sw1Q-S7q4p-FnIPePiUT$nY-51J(m=yPExacD`1rRa96U9Ey+79|mH{NX0H6 zkKQ&t(8nvF=vYZ7!jUw+rF4+flz;!<$$u00sq+@>f){@~YHdFV<}8DWXW=9SVtU+a zISoJCjy^&0Oi-EkP-Hk&JVX$?a9ue%=DBueXPpL0Xc2QFwr8fB zRk8jKG`8vHXe3vuJXE6fneOb{zW-FUu1eMfk{_z|Gu>N}LMm#U&AKkP1oyanS9fiW z(dva((e$4YgKZX@nMB>Sm#`fdpru&Yx@qyPLsfh7SRFl|KHAao+|{k;`?i52IB zmmvLtK-HUdWd3zm+ExcK5WDQ9(v4DBcZJOkWg;IF$zJ0TK z`{y9bGJ9NXonU%!K&ushUc2MYExO6xvwNPa@=N1BRrL;?cULbm8PyH);^*MLRmgt? zeQBV)dl(h5p9=!R2?jJHDVwmL?0lL-ZYt+O`XkprNJ-_)=RHwiT=)c@BIPl`N#Ynk z9pp)%v!c2jxCW`f1Yo1W8sod-un0;3G2}qPZ4;PI5Ir1I+mT>X0>p~13m9>HzOf(= z!i9-_OM))RSdc7ZqrB|=XKeT)5=HzF)%u;jzhq6#Y2!ESq5j{Ty?dM-XLaX2)%A2$byam& zbyxMhd%9&;84FurfejdJun8AK zoZtj47;M7LNu1YC3~>@j96}O@co(veKo$}wVK>Pp8#V9uR7*hi^LhVy1+sejQdK>5 zp7WgZJHPWg%9`+PbS2?IHqIrTLly4?-PmUnrRPr7(2Hq}Ly4@E#fDVh)?wWYy`#zL zzLINQYn{7%)ZDp>Y2s*!WZ$1*W}zEY{xUq%lnLe!rAt}ELekfGcgr%0Csw{?k*MEx;J~p_h>qb~C5pEJyp)dF)=>`|R9e}UIT#^{!9lDGN*-Z$4#6pBb z(De`iz*1k}Z5K-nk@?Z6eSsw-`;|C003S>f<3?e;EAk+jToj_4QHUj%K^`xL8hK3d zYO;^YTy0Eo2yYejf_CF4?5GW)T(U9Y;IT9aw`i6+!B|3d^8s_}?qMj}p_G>W35Q8S zYT~wZ(K%Piq`5EURF0_~fWXK>H{I`y&pn9(!`EEKvb_9~vMzt0{zuQ{im%=@&`X6- z8$D}&yABfY>CU11G7$N9x3zD${a|^&312P@zaWhJPpndnht<9#P$Ad9GZw#T$1T?B z)Fu$P$~z2`kw#5_yqdC7(`&Ptb5j#xVP_f;VLRE8YNqaUQT_I-F|+P}lahj0+3qIt zW?}oW#+^y-sxyEaQ^g`9O6}vBB3A>*A`M@1j85;Jt1VcBhWf2zv97E7>|S^0WU z@f=7E>ekjo9p&20noM>UQlV|-&!IDqRpk;^nX*9Dfs?mkm2`%^xsg9Oscs$utTgrf z65ZpPtp|%OiJ8XHoB0%Yad(#Gt?YVdU^JZT`MhQmor{OKm|?{+9Z{K+6s$kqC%cIh zlcXfX|Jk4BC}3ao3*0mbs|Z%|1UQlOJOENxMWEYRj;ra`5+uaJ>KdpYMhG(o)~!ZJ zQ6b(HP6XKL4k#c=6Zh9`9gh-3PSdqD**`CAm0442O#%=}?tq8mr>K-A@qYweM_v%C z#s5XI7Eu=KWJrpc?B8-a5F<$**-ZHc6K(BVY*y-b(}OWuf|NE_oilpPdpp0;`O5k% z6oON9QXx8+wOy~j$R;}ciQXb@pg)M?s=lYxd&s7$%lxf)TR}+_*pyQWtOwP z+l-g5G73&p9ZA|5B>xH^4PZ{L?`sCz1C$C{A*tg5V7R2L#QmXKwIa8=Xv`qU-*l|t z<_)Lugo0UqF~XsSz@TntV#YgQTl`+npY{B+rr{MEf+};1yFL+Tf*%rZgbhdIZMyNl zMAwq`5fh1XjBFm;=Urf#d`l8E>x3>EahKJymgn}%t$i=cepeZupCjv#} zYsfmFtOmr&=cCp)jJGZ0v%8)Z82`IuVFwe=*!!D>%hy=hjEOK`Fc;0OkiEuBYyVX9 z+g7aF8`t$MLJ+)yvc^lHSNucWVGmRvNYS)v4Wp%zCR9+tNtX$$L!t+;}%j;vC zEORt#q^_6%FbL>ZX&eD#&LrHuWjF4PFC5Oq)G;WquBCT=uT}}avvV*fYQb9KVOG&J z{#B=_?|06sIrj=~`#1=mdaXr7(uh; z5CA`Yc`{{{vqRQ+%ileqd+7x_WoNCut`8wGWbA0u+3yT6keGrX?izV=@U_6R+Vu70 z%WGqaoa=k4^Ml8)-cQ&w_@#TUD9pazo2B<>y4ak`Al(GIF8YI?gZm+ScaGYOOmC8I zyEibI@JE8JcZX?+QDKVgq+OmmMKZBiLBS)UqKM=m2#r5ue~d~lQ4EH8hy3ljNHIPD zFELsv>P7o=w2enRA|X`^F*V<&-XF05OdKVR;yev~V2U-9h8E0f$Q8nST#=O{xu@zn3{5wcC$phdN)tlQIvW zdTwERd&{h^{RLi1ouM#eHJIbj`Oz}kD$i3kDPPiCSJzinG4*9to@^v@j}fx>txl&B zAK(M(70Nzze#3Zn7$EKFYS|h-dSlq{n;Sr7yMIv>5t^zKE3_Q@%NzjEQ)R-Gs2c`MzRt z?x3~n(Gs%;L$pygZ-onPHFr%q!Y64lE9_VGDKe5OmUdOoGkC-vDtBa?(@pf}BLJ2N zPGq0NSxFQpelGHkV4CQFnp&VDv`&Nr}2v%GV*|&UQQ(PQ&B|#6X(ZqF00Ev$c9Oc*iA zKk9zkU5?t*d1Y*>sXg?;&e?tJzvdxtba4Bh50=#WRRgDY?o(cN*P?CH@`T%Q?CX^? zQWUHyPhn1Z+MU)(Mcs$2!btXAt|K`ouGUExpRNgIqC@(nD8qcbKml(BDsvS$WBt zE5nFU*4x-4&(@(LdZLBAz-x{1x#P9M$d2A?e5kAwg-Dm=rgmX0JfzaUAg$~irHOi6 z!-0BWT7||_)1x{%o9H}-Pj;U96CaJ{$=uC-=@mA+H2rSzMaLj}&Hvew_>HN^UYzb%JaAsoEz`M~a@kJdaiVZ2BsP6*bTcujd%VimK=T^|Ej#Awd3A z^z-LlCX$vDk1;y-!uZ$=Cu(z=3C=In2>xp}NrC`=+Oe1IY$3VtjmdP7GE1e>+Rp!q z8L`-|f}P&Ifz;*}P(stGCw@yyroENgC&M6>_X?foD&@g`-GANn=OGOnj4pt)Y?h5X#hSNGxX(fd%C# zmvODn#qGEaO9}sp-ezFC_a!n6dUdiX*ZS31VB|9z2?kCr;mtjw$MIPRg60tfy+3iY z_E&#l{9f#;gp*HRxdsk)IvxhO@qeuvwXJ2f7V^Z#y8Ck)RB6IJ>dY=T`EKp;}^UM?G%nPEEB&mmkX&Kc5Z@PVZ3D zA1pFM^#i%SNo7ypvm>TwwSAI#CruPk8FFkV?m2R-f{N~21TrL(9)Xq^{B9S!}Y!LQ4`zb|Xry1@*LqH9?3 z`-!o2?Td`1(w_FN!9(}J*O1xszpq#D(K#5Qt)A(qpY{Nik&`{|pu;Z0jBt|hCZd#` z%{1z_AH<83c}WSPP1_uqHziHMg-uDofQmsWSzJ=-lmb?iO5-ghtduZV!h&hx4n{E| z1sA|4arhiQqWI#>kGSsz@6E4enhTOSRjMDI#r?`m zjNyst>bb+A9yf1#F$_rf2n!}j;w@f>S|H7ud$T3iN%Y!DGtnYuFq5n%r>x{yBiNy43gNP49B8U~db2s5s}W-C99AYY)k`*~edZM=?N)V9 z9)jVV>mVTk{bLupG4uvqJ%0O%rgiRA+0&8Rr_vFAmy?k=rqV~}3u|xS%E-Z7Y6Szs z!7p!>xQMHnno+BEN-SFghx$yULzO{IdribJx{n^d5B2<5Vhl!#M0Z!PCgKC>Nn|9#e3!R(V^Wf?E>I~u09Hk24-G6YDzfkjB%mae!Q6S0f{7dx z1B$SyZtyA=E(m;+WAb6aqe!U0-!Fn09=f=E>cY#=&v~CTkmlv;#Ow1BSzqK*OraW``y&2e?Jo2On|N~$#hs! zmw2jWrei&G`VNmFpssb!E_&}6_Vl`Sf8u(6&Ey!J@6}$IwDfTE4cT6V+b#8l8PxV) zM6k9${@wRH7AWt^2}An>*sFK1tn06^Z(n%~ncB+UL)=8qzxM-K!*B-nBu0*_5A2R* z)UO~;cICbL{cV-GA;V~;gH7H|9DE=h_(Rw1YSL4~*ra(%Jej0t!ai)ADkVUs=t3`8 z%^&Ybj2&D?#7r$()}gc+F8j`hCj9j)rNY!49IUKgldsP=ur@|z>G}g!O@=M02OS1N zNcf^$bjew}b5599WB0W{9NenyCY+$E``OE(N*ea!0;s6klv+J?m?^MoV`I_^&Y(`i zM5iY<6es=0;qC4Nptm~gCreL2KQ@U>n&_c@oPLWmG423*XnW}RIsy;nzMcn}i~AAO z4L{FJR*iMY@pzH28gERJ6n!4cATZ&fL^)Va{nA|T4lkubz*{L zuGzmGlCHbZKee<}pim60?olh=|IKmri~p6_cCQ+}S8y85T$J0d#zQ-+p07i<{wIxzB=f~i=}^PZtI0hX?cVLwX>hh^ zs@~ZU77Orx(=HmRe}7yLnFs^pDLLYj8Byigwu*}M*z|^i+GR~7<*iEXSG$|KxoKMW znn5hS&_c*>=4gfG4HQ-0{#Fk@Eu6dOYp!AF17^#perxb#(_%u{D6kk3TQKvnwz7}G zG1RR)Vt7ekY%$Tb*BJm-X>0l-{0-P%F2fFSv{jqBHH%{H#4zze3j6*zGlquoxH;^* z)zj5@yszgWD%-z=p6?fX{u#^04smgkZZ~O1#gHP3QfLS~cqB(gdK9}k?!TOZME>Rc)KrB!O=6u2B z?sCiSH~kyeAr)wuKTzjC#o!6#(k%5_%-Hp;I)A{jS9y=8Dd73)J}SI(+S~Q`y5Cjm zbDh7k2g6^=OlGoAX@8doyYco1n{HIey!r(;-6v~>mV$jSWq+>oNZ$QsQ@!(!nM~(} zdU-srWgZlh$ZS<=$61g9b^hH}`*thQyWxPVeQTbcebkKt#uHKALLKHGE`MLEa*D1r zMm<)Rhjt8^xFSCem~ z8sYLokD(<2uHYlvf^biA2wG4*?sYm~S$ugQ>n@+T2o?8#NWeV~22*u!(v>L6p$M2t1ks z6p!bxME>7Vxiu<^U#mZPQh!eO@VkdVUcXF_+qZiD9!18K)~k(bmyjvqZKlZiLblPF zw&X{6Qhqd$TrrW=?kAI+PLtK)W~Uiq5V`ciRKYU8kC-N|{dx{#1hEp}mpvrgmvMzPg=E8U9;tFiCJSZX%sF z_P`@`dh_=NMU&1Ksy2Z+rm=JSa;?nn$7A6OC$X*qO zzT4MZHNAMDrdM?L{J*(@^X78Oc6&3Q^GmROVuy{G?WDr=$n~day(-6FWJk)58zm zgnGQQe~FR_I(K;`*RBm{C9kI0W1Cw4xc zQ5p?8nS!h5Qpvl^Wyd#5$U_^a^e>ny8BaG?J(6kPU)p&z^m2!HSXoQkNp2fA1D`xP zR;hTbH;KDHr~ft>vfGn~TRFpiu&Za8s>ku4G3oHpqOFEYmpZRRI?|6E#Wa%CMUK0P z2oJADzSL-5aQ>1SMjWUa=f9BPW1#b|Zg_~o^}eL`e5DzFA=USaTGuyGM5Al6dUrmo zUlGN??g>OqdM~%GqyDEBg8X%xV9?fhy!Z7m-`IQ9YM948EP1qwy4H-EYz!>oEq}>- zYIa{(+q^07#&h~)y{1k=*DaqZA=WAAZDV3-p+-2=|F3ZkZyZmLITC|#N)$qo@Y(WLajyGa!Hqw8e17?qVV=flejOu9Fh_}0w zu8|`>Z({PudwL#ca=@STybcz@_AJpP_p-|mkr+a#ZPdVo@I+;S9i51j>@Lcnx};g5 z#YI>hUQs?P;quHhW`!YLys=_pn4R3@Zk#Od70Q#ClB9A3BND;LU}20bD)LI%K~hR_ z(GpR>7AJ+{$?i2oPezv3y`qL>Z-PlkZMSI>ElCb3ACa#?&@PwVV7ey9g~Kd9=r&*`pc#2t7y%KPiY@Vp`0}o9E_VHE@(CWVy4aRgS>b$w``4fEa2&N zrpkAAyrC~b(8Pyr|6ac*!NrRl1HHOIWyTVJL6WRF<@y4LQjfg*P51l ztF<`}_^s=o*59m7Z6UxKu5jgbtD&V{gESKxzO6e9Gqk!VXyDY zXuCG5EsxljnWE!SMd#(q#Jnzjux+~NkI{YdQqMnWftJ@~`rgm)&2b#Y1xy(%SLzBw zQ(b2F(A4DY#T`{3=kCj0B$p6jVkX+!(-Hdo|Gj6oqa8VdtZ`BocV`oCC}6V;U1HNF zb0t1#rhwWALNM`*k%0sO(TWAVdjjSc)2J>k-OiGg7m2&;*&PvLJBhmhmhca zuQ8$JiBiU51R(ioP>b%ngYLT}LhR*+k$7O zJ!YB7QF4@+%7o^=LN3g@*>@yNw^$^M)WgbhI$J5{b1CNmtz3^KV_tdm9wSp=V?AzP zrG5Tn&U12ppzn$8WQ(@Th0gcfRL+e7wGxaZ4#!pmiG)snQ{0{EOIFiKhZ1^kQD^sW z)y@<>-znw3?Ply(s)_8dQyjW9mI}+J=7tkT61|tC;sZIS;ukj>miF~nwzoA>^{1L& zh$Yj#M%>Abe#y?I^_ge1XFsQXKK3rVQLz2|LdMxnV~mxC)}XuESS;~tS}SH-al~%n zDC=jnV$wCt7@`ggI{RKJ6Vu|9WDBBt>pobjJAqM3vR}VfkM*0nkqQh3MD0?#VPrff z=2_{uW5?sWVm7L|bLq^sUFk}~h;6Pp;M{J}+XbD&LgXu!Dpg~DpK&v}%f{T^vBUB6 ztz0=+y)rhsYHc!1_}Pa`NppBh--e0(VdwEix>_3OOZ6s?s}JT1Ub&%VYGpf_$fN3L ztusfBOMrZZLwN8f@7Ldt){%+M`vN_D`{@S%q852r)7L})IWiVn9m9SAz7n1bIy&g7NCy$ocMQX0VKN%`?xx; zk@px>nDOkw!5)fgg7{=G3(#QMSHwdLJjfSpz+U7TDM?;OJcXgti*YA#p-= z{y@8e;%ThBg=KeOk8_lnkigX=l?uQPkCAUAP%i_bVk?aKY6Z@j*W#IQ7CJo za`fkx`mS~SUDRJ8frf`&eF(O)yZm67(bDis{}dt3@tG4p>^$dBGOzUVKg$hkS2RN_ zma6OD=-jvnr+Q?-tCxc@O+&j+{Y$pg`BD1^z43=Q;bY0*N86E=jvE`V#ZfnoZ>_4S zwa)IMop1G5t#N=fY2r)RYWw)@;Zw8&4f87*cIUG>uK~Z!@%!v#Dbv^XC`r4)qgqh6 zA-a}tV31P&;*|fSqfl zQ0Yn%2t6nos7m?*+1sJySF#iQJ0&}2p%{!eEY#w~iMEwwcRN{mgl zgK=x!YE+^IA3Fc0b(`HrP*^vXeqs)Q#M+NuPjh)+rVqvjaZet_6MN zo^k?8s#3=X!pp=U;?bhJH1@ zW>nPVW@CVo?TDX~s1*!jQjD%k1nW9HZgeDJDYsmBg<@lZbP0rZ!DnJBGVqh11R4`V z!t;vX#L|a=-7rr~QmnPRG;o}R3UZhzbQKW~qFY%M;2VMf<3kP1f4V6r#emxc>YSIw zfSq+8bvFdLglOlGD~dQg*m+#My(~IN7jjZccds^0)wjV{lV=-^`Q6ku91nBT{b)9u zdadV~M@W|2{>qDX-qSn^x@6|JBz^$d}Mo077r-&mq(;o7bs6`QZQ zOQGtFs|T9vF3W0PbH~SjaTtPDl~m206n{9?`L7^Xs3X&nMvL##r;mjqr;JO=`)AV` zYz{4k@U{GzT(RKStcTEsp`r<*dumN7sEs3BWQEfM$QRBJ)3TY+Bdy%>#GDW9F3dWB z*{p!dyl-t^C^5a91wq9RKfP~5&8-^6&wd5g*zGmm(?A5TQY#!nNa`R0ZJCw6`q=5w z6}j}9L1tg)iuxq}+X`>pI@-7ATEj?Bhw0luK%0fT)|^?cX-!FM*0qY z@W`f#4|erjsUH9?Zlls_h~$gkH=uFaj-WYs@d71a1!e?d8?6JUVq+ygu_yHC{MX zE975wpSUFD3~c!Yh!IWmEeEh6$_6U+hT;0eLp(sn@lx@&W!kXINM%_JK`&0T_t&zc zV|nY^0odDXSBk9P-cW71-?Gk4Z_wi>W&Y6ecIHJuH?y;;*tfE$8?JAPjhg!!e%5Z# z-{n{aQiu;GdaDLFvGnrJuFTrURAJVtWmY*2C!~Ol@&wfrq`Q!3ZDP%+%*@jhVJ89I zVsv7$!^0u6Zm;GVmG}LLU;40hJA-KsZpWlEeSirls4x%Jb+$-*0%}K7L#jSZ9mv~a zm1%MA?NiB7#;Sy6&SfV1EO%zrj65?~~}UJO4q_`-{FeGWMt{9PQ!V zDlHbqv4|3)9T%v9CiG~Rmm}c}p(;f|W@J)Q_v=c_M*FvLDEaXsgPbUVOidgnQ8hN& zMjA_^4>7_nT!#M#?80y(7(${KSreqEk+Y{xm+h_|;$)*i?A&CDc58Lh6Ikw+JgzIF z!5%BDJfIet5R`qlQfoV#4f^lPR(5sOHvJRYaIWv*|M_TjEfL%!{Ed9Dh&s2uvrRyP zy%>DiYQhM+cetqvw`$kTe6m_|o9hR$`U5KqLMErW(0RR-{Y^M!2=&D_AX|@x#?9lU zc;{st^rdUL`He!rxZr)A2U=F=MI;2O_J4chApXr6ZrI;AJ7Db@Psi-pc<;loE5Dz- zrCd1(ePGgWWMDQV^8VQJstpo^94)Oz3X~d& z_~TUU4@XgO%ih)N`QwL!{()paJwNnxB}eEL^<5)#>Jj3B!iXC#>HC?C<{v(^DYwiR zgNjP?PW$9q9;j(<#)7=`ut{$1JeOy2H8-K1}Il`)JNzobD?G2q&w`Sh@<- zW)0{Cb++uC2$Zy9FIS?twZ*LIuSPkU%v+me0)p^&52B3m7d?Ni_2Ylprz3<*QWHwZ zm}EL^s7a4`I!gb`6dBJuGBFB!Sch`!551qYKiwfR)!{e@rA z6xR;$CQ0B0bp^s!iLayBfotUS@Pb*UP=@&E1(h)I+LCU{YT(z=je+7z^f9a45W!eXhnY8BF!UFEHB0hPGY)GM92@$VEIOFi?N(i5lD z%B?cGvh7u+^vQYmffaxG(_Z71~H^UE%EG*`DT7D+aFu9=3I4Pu=O=QLpfqY zpF;L9u;^v5ZcUe($3&28F=lUG$!J*p-04^{ILP?0+B-9F(}t-Y(qj48{yXhf--EF8iLC_Dpf{~{&S9cvgT2oW8J$k?SG9eMO_ zu1AnAD~)qaN|-Cm3m9`)q%10qP4LrDSZmaFEi4ccwFX9>X8~c$98w&*a6h6DU$R3w zE+8{=ol!X&)J5KiQgrP5{w>!8;d9FUP=BHNZZy;`Ob#_kDN%5~WY+0{pgPIML(VXv0I{V-!6%w<% z%-D0i#mQqT^~P+9cmC+%a$R?=A-_GY4)mdt+R)y6MSYK3z7h2@r)f^F4J`fe6RN$) zHJ!#rkx{e8T~Sdi_uc^)iI$HrYcKys73oVN15|mHdoaf^R`Jtd0komn0qvi`7q>N3 z+VqUUznnt|$)OFBxi<>AO+9WMn}uhwglt&X7mxeH}jT}+1iKR5oK%R&<2!RMc0Bx(A>{3h9VhUNSra-v#IAAl%VhIc{dET zUzS+8d*ivg(=${jvXL+Vj4? zk;z#l%_FR46PT26nA}sYM4Ug#pP&(v0m@z@+Ygx^SQ=liKO2)Mc^YNC{%PiFMM%eh zB!ZCvM^*8r_KjeqP#I7C?}`~5HWHV_E-%r-kh`n%J-ez_47L~RDBhq#b>hRl?wmU1 zHs-?VRo3}`WbpaXA*!x^_%!>u9=ux3eZ9;erSglfBc8?Y$MXZqMXGdUMO7 zlcu6xN@WqHHajm(V2d?vbOqgKz!xw2&*ah|ZJCD{#{iqnOy8pC&$waUc`r`RM<+$X z4y?-=Z~K87H|}73U~t4m_F_-ILhw|(Vs|DL;I(Vn)R%tEZ4_9D=CF>kxdaiD^N8?R zJ1UNu)Y0NQT^*`P50OU|Q0rYa!iY???CrG#L{OKMvp`GTz+SVEO748a-sSC&_|}H% znew?qOBCgfH=O2fzO`fmG{@65h`vkEdAYV=RkW4t1c;lPDN5|0A4<-EnSu-X)4#*nB%2c() zH@874cp-`v@*yws39d`00wV>DJPWHmBOi%u`)4&0+6t!I^0DZ=ks}T$vkcDARshGN zTTbD;Q8guERPY_xWugKMwHUcPN-;B2(KpWLw0bVJVJwqOP7RNL);3)jSejEz8|Ny1 z(y4`C?e|QL8t-y8n??=xZNUf5SVq$9cQiMhPFxjay#C-XN@I6eX@)fTP#f;kJi9OE z#L}NVlGuB?7}&+^sOFf3F#JWYpeOp0D-We95mb#&6?|*pGQ#*&kj`EktL0(_1oJ|| zm^YaYh_xA8tYipo)o_ALA`EXt*3q2TJ*zTd7Lvgo)AlCqu3GXyJXP#leN(28%%!}s z;^h1_j#(|ep|Ry~E?GS?9+te->9}=C#n$X>(TP1)nJL-nV!m8`So3PJgyF_aGo5ku zhekdBSO zs|H;smGy>Fp_A*}wxxbGf%G4L&m9bt39YK$S8(i@Sq@81dBw?+6Vo=u;+HxpiZr?H z@oUj(h=X5Wu50A6>7;J9)(3mzZ6lp=#^Qy6PFme-YPWpQbQj;M{rbQ!Bw`7OhPwW> zvkzZy(+=~&oe%Hyy`i_?G4-Vu`NU!xnNlmlZdb8k?iyrqTZjQz6pi?LajSebDscEmYrYt3<9G`V2hMp7ihrg z{v4nz@9O$sb^%^u&EeMv+Fd$^#2n2$xu3A%BC=g%+ufuPr!D}ZL^ZsFMI`=Rs@YLs zBd(tEC7YG(5b`lBoa|W6fTc6OmB%CGeQ z%?)E#_dV@CI1kFqtau_{_+HC@MU8#G)j0CDTK%UsdSO3-sBey}sxhvFU81?va!r3fVhKyb!78&- zbDTB5IHK*HP4~Oy?cmgP3wv|7u5O%7UopPin8?a>v~+*x*5PCHq87Xl2kyyM!Kz+u zwjNON-m<&;4P3REy|L9g&EzZ@Jn%TgWZGXK!CU!M?CT@rRxWPRBUT8Isis&sq`#w3 z_9C&gM`?{kznOi69xz1XeL#`m2t-cAkf3fZdzCE|>uX`s`9%hcGjT1ud5vTw`xhTK z&0ZsaYn>if8Q*5r+%UVXGW4BIAf>(QR3lD2Av+LXMfJ?^J?Z!V`=L%Yg60OPCL~BUVm72M{y?I7>hP60z(k z2xm)?B>`^3dnAzI!<;k%Uq~uhbK>{~O_ZKf0da<+OA%%V?~L+T%pC|s$RgNIJpEqpGFAI1|tEyK4po&$^!GG20b<9aG|8JPxo?4$yO+~~Qb?2*tqF%lNap^-W}zt`9b9=8k> zHc!vTfw)KG2Ab!wV#0j2%s%ACdvo`B^Gn-`r<>@U@M75uZ>_D~+PA0NzGF~byVAM? zAZBv!iu)MaVL5c{BUCmPZVC4*zDT}IeZ;a(YuXw0NUT&l9hUREQfnQfZ(%5t&R%Bs z1<&dgdK-(jovlA#Qo>tH_VoSazu=#zh{o%1I<6pB{7lcU(WU?Yfd0Kq1$VZmCor%x zxR)2>lPDsHMWIO)(?HXaz(qO)FS0qBm|=iZ`+272e+pTqHK9{Te^Lm2Bg{?6=jkGh zdiEqj!ML&Hi!Fp*7fC=)EFsU&Ts)m&cH<#4;4|$(!kV^DO2|LNPZyquOIRcU5DSk6 zM=|;+0E3{yHBZA-87eSdThA8e9x;<}hCJHwKCWe)!PpBfGF)0Z^xqrv z-Zp|5UD2p5y?LP&OHDtqu4EQ3yQ#jzHTyLbo#Lxg#WNdqCvMm^Gq=<7vq9g{vkB{6 zoj;4e(QrmutKuIlX7BmgGo2Ujw{Q7qNE5tP%y)iZ#vMw3b~^YW$e-u8kHB+eLQro} zyWX{X*WCGYS8~I}E4fClWa^%F3E7lwKfRI{`@OgI_Uo{WA#8Z~^cbi-5stQPv~HDy zzPerfop?fv8O*G5Ac*MYtkLnpjQddEpq)$Pf<*kGI+2bM(&FN41F&Zj@%7o9ah2!A z_5N$GT~|riy>^k7SnFkIC zU30ZXWSfYu3v=-`DK8#tTz1)91(JD9U(k-v6b(DMVs?(J@z}TIYGxCol}sX+i?P-` zF!qnrn^osgncbls^~4+{^Zf-n2A*ckeHFa@-y|A}=AX$1F+C;5DoCS{IRFP~NFmC& zuze7Tha|CiJiK^uDI7(c1oq2bAmNV0Ly_Q_Bnmk3$(b&WA_-}mEH+xoG{Ht$DFlSNeQc_BRVQufxtA` zZp~fxF2Wu*F7~uI#d*R!Ie|bsB0n$@V+U6UMtvgE?R}T?c;($56_Znl0MKqUmd(@O z|1UIO(Qjg%f8SX0(oM`!cBpY8hL%ai|EN^_X3Ls<hzTA_#k)sQyM6teR2H&aV*lFk8auHO58~;4Ljcc{0oabN@J(4w4Q$ zuNj+HC}h>zx@58{8>1$ULTx;FvNvI)EKNgKI+Kch*Ogga1!)~v!fZBF_dlKMJ2dS1 z31?vA_#zqw8dE)er#XK3DSd)DQaAQ_drh~VTJM%?H8p*iyK}cnWvM;+;FU3E%{_K;OvolG^|Y$oB*%V54aTsaipuIh((;;JAaTG5>GZ|HxF zoyt77)u4&nwT)2?lY1o`VMwNcbP^0p48m>1W-zm{B8Ppp$O!EFm^Hko3Ar zKpw#{f?zhI3lY>`E>rrwxNvzd7c3rL)(YWH#7a_#2_&oK55uF1(EJhAb97 zGzj7&JXFaZygj!n~|?1!M=o47juE>r%@w-Hl*f)qtmgs_V&gd zKcBQuhVBGYjbEkzv-~Sy!s*-R#>>{$+mIBuZr*)(h+uc)VkY)RRo}QB#R0kpkr(yH zDptI13w4wATs?Bfhb6Y!gVgM=57|w7eW5#?gPN!&noyo>3DkEn})) zA2YXIURXJlFXjgHl_Tng3^0o{mjI|}?=BWQ|HTgBSurjmeSZnhj_p+L`o*fz7x9 zv#RIcWtEbx1!NrR{;>*Gt#)(%MfsY5^L3V^(oM4veEqt&eBxWbU4! z!FQIexjG{ant9*0osk_DL!>?HXsbqTZddKlHSl*EX*1XuUd!)VzjjB&srq%WdC?pU z;i>F~FL*=G+far3BWhFM?fG}s6*0B6cs2UzO2=-e&=BeuqSXw41>I)RM9D2{jHsH4 zW8&4jm7It*jhhCOlu|sz6r@085y}#jJww(@clq&yU^WrTC?t}ms8Hg|F=$*h?*lu> zo`vIr(aP$ODy^)m?&2d~VYgs0$})*kb)q{;q;i5tT1L=VK0(eR4xS6(LgY2Bmh?W# z8n}c=E}DarlT1%NV^zX2-|fSM$qV*!^qF`~bJX zJN9NHdE4m1tZB}~)$cPXptG@2{z5a8z5bFF#y7{Uto`(NpoP#B2vpZNq84+-@!o9b z2jw+pGX&eY#mSm=4irnR8}%LYrY*y$d8kEH+sxebk&xoMshh(p%n5T!<#Q_@Vv0>r zSnQmBthQ@Gm9!^OF;vS38v~s;kVvHeAh!7hMH~6;(&vWo$E;`8@~7%(AknKb|GBGM zo$pLqtqpzPN&N2#cklkw4$WhYz*J|b!>8fysr%_bN7-Y5&+JU*UQ?6JT6s>jH{Ybc ze{<7Xaw@85N07Z!NM=Z9HrC>`=Ba|S)poTT`-|yRYU_TlWS<_JcyItc2&WldTM-ea zld3*ul`qXXr$+~infU@`wib&Xfv`Mt<7!l(5*rwtC!Ce9M)-{MG0I$l4b+aV&{m0E zO76(q7>N|H54@*r@q-wkIJXN@FO9C=;Dr~(Tla}KT$el+poFJ+H*pC5Lfm>} zWk5W#Ghwa~V?cHwE{~INWl`HL9z3#RE>$R4(bq%op!Q0OE$iF8iRT-5pp6gYik;WZ zGOniZW9_Tk#}m0d#q*!u+oleFI@U0!4fpoE|0skGi62}-k@~k3Qslbs|WmeuvQ0&H2 zDshCju+zwp;U*fO9!rk&xs(`5lAn`%HW8)?d*48{w#JN5(@W>a(g=mkudJ2QLzgji zQMsSi?@@GEyvR@`LF5lJsK+%cT2)H2^oUh~6!WlA9&R?GA@!-ZSxZYKpVv>h*-~*s z^N32!ma2Z^9GDBNoMXGx4ZRflbH(NNJbK0-e_B29g!S2VFYV?x*tXCzPwH*@d|?`( zH&!rH2364}8z0nN@5)vlMMzyk_W0{aF=5_keQDTaYBpNbGzI|yp%@iucCs5tIdorJWtso$&b)`iW(qUo14329vR14!gTa^MtPL$uRy(Aevj+-qjizp32w{~dj+ zE3%HR{qdg_kq%eQA1d?w&8Za|-k4!bvTdB@E~we0=W6d`XIGt8^R;41Ra0;J4=64c z>=nz;clI}5t$Pc57B?=Xb2s$++NV-g=04q4q~Nd0xtN+X3o~^yKG@%Rx*QS>SLZCm zTHDSZ&1<4ocl{;9`Yud502+^H7QYfS2kxEq!<{?E3948}KJ`Z}%OvdVSY^Z=DEPhE zbyuXWN!t1N&AIY^CDfPTC?y~$U=`W)XAtCQK28lcVHV8AAF#GuxLad8Py&^Sy6_fZ zu2S+x4*4JjQ71mB4Q94_mHjR~%Eb(%b4)A#*OfC!y>zXHTi9qZq zpy+zkh|)^Vij5QG;O3)bf%YzOoH+Rzl)70ym}^6xljugI*CU>{1UV1^#J|HcxUj8Y z+tEhR#pyY0H_V9k7~W`f`Qaw2W|70svM>-*V|$4%jR;808|x-}W!Z^6%lZ?-ch~ci z5X26)1EK%j(is8i4r+|3gYwlJ(8H?^l!L=39}b4MeauG+#7sQ;rc`mmRfE=!U7f{+ z6TMB-@*WBCNplKMtKEVQ&UV*r8}BQnUtyw5<%H%Ul%90v=Q0|@h=S!O(A3N{7qwhs zj~o72S-(-#&CX&PX_3l9T9$o$udC)e&-A5!xY2~;Z+Tkvc`EZukJ7u)*>273f+X0E zz0{ldwtoV#16?=gDFdbJuG#pyKI>4hr)?X7o3kgjSk0Q%*lXA;XOQo&%(}fRHzd@KB`j2w7OHoe6$T@MTUtL> z3_Bp!VkKZbV*f(3 z5<`-JK+y88zKxV9BY47S$eh&%RxFl^kTUiyivZ6a1rt2P@USle6$yDIx(2hC;6yrq zaOZ3b26(w7(9;cu+GFzKOJ%HmmpK+<8R0Y#LwqAXl?DY5Y=I^#kvG)}ajm+S9+)m?qSE8ecu`M2ZR z%?*8_+I8HVdV98ICycae)WTq|sR}bEyp{Z;w6~yHj};J#Ad^f6h_r`cEF9pN*}`=sRQ9&lXI^ zPZo~o_n2TV(znC@mA3u;aa)xacPJZuq0o0+Si-LrnmeEuFxi3HL~J8D>pZjt)^sY} zfEEukXH7rb3tB)26IWGQghp&f#@qYsoH4GxKp#-ttV!=P{88pQ(>NH-pR{@tA;Td+ zeE35y!hY7|B>WJTZ(TdgT*`&Ao1IQ=WnkgWrWzqB2@*R)hXBpD+}x(ghG}iFdi0(t zbR%BS2T|=AVa@amO7G-Qdj?(spn1d~5V3nhho_JYWAY?edbE1rHB8M23@;y%O`}Z+ zAq|%pal~riB@mRTDHNbR9cpjCcFN7IOV9JzY9lq4fIN2*F zw72H#X6!Sn(!4t`-8aQ5mLQAuU zYk_X2^Pb^!yb^9C!ot1?x3n1L?`_yhyHPKadNsR<)I?aTQRqcN!yeNOvqaINEh8i3 zJ<&233`~AUgIVza!Z@008=^qS`eKDiWEaI^64dv(fH|)#fLw2mV2KJ?zsT9hGTc%mObD`sp!`Tvw@y1Gt-XmDJ3>PEnInE0B z@;!S}#k+Te>C(bYFgu@K)~fN`c#9BbX?aq`HmUk;GS&0o3p-Q=Fw&BmAKmlJ{L9`p zT+N(nx%uJN@D(KlhMAAbAif)?$W|)!V6ZisPm83rwRC@-sgG0ro0HZ%RxhaNJ}|o> z_RnCF0n;1S_I$MGlac?HN!=Hl|pZVX_~4Y(^DiF#<0-@Lf>i6F`fxoNzeJ0-t%(Z7q*R?wRnoH`dVjhqiX zJ#v=_ccQW=H&`Se^m}UIxvd{42j-oIdJ=y(b~CB?v%3DVy~X~o^`?p6{8+X0>+Fu? ze6!_k1FF?*EcJ~}X*^Ly!A)J|1=PF) zWCW^3{kU%~=aZXjux9leJAbWOhxKwev+K_ru)P47HTwi~t+|;mDC>-S7I@AJjP$2I z3y`j?Qo;$(F-Qa;a922j?5=ut%<{a-;9qSV3KMyKc%WHbf8C9^xJIK*{OdnlN}%xC z6#kcyGRObDxfHCwlw9>~X=D5`%LM^Ems;tH2b*>84!v;KE-UJzjrX*f2lQEb$z|@; zG}8<}%iUyyl*)2{(?s3^94z8tNM0YoN3!*?&GL(uI~eU~4aAuB2vOj-&J7S|9s%IA z0MAi!-zM1s>W!4~^=LT?h&(wFObE*Zff~UgSeSSw*>hz{u~aXtP{D&Sgoy}BjyedU zmB+!!lBSxm9xFJdRQ(q9gi;T5z75HZo40OEIj0Ax__*#&tF4kBXu4|-H2lRWJ2tcE zoE(4ANLtXVA5#NL)2|r!Ym4f6*L}=QRSAPG@BCf9^>{g3GSh|cLG+F42yx@q@Et4a zmVMork+60!jCkn@<^BnEg2Ma>YXR`dRQiLp!?Tmcez!jy9`p0J&ug{1_vj16i*+Vx z=W0jYQgZ)D=*D-ZvWu>AauqAw?$5Fbr?)X9E`OqP)4<4;Lx^c;_>k$YU18N0+-5nAw={{l{+1pREsD6N$v_^bs`Vx6 z$NYY|i7XR-)BJ+D!Q4UN6G*8bieV-ruE2Dg(g1H3ZOfS`$R}FIH^%URqy+>af~2!- z$tCclxXLJCBkN}E3-lzcg~&x>AasDzxD=^KP_W{DaZXljQ=mzS{2S461p^}E=CFLU zjb#wpVFbd7em$2oO(dNRWAdmM2dA@U3ri^7U-6;^EE$%m6B@}}(bdZ@uZ?GOv)T>W zls~H$%|W|90J?K5lk~@T)!e@6WyuC#s|TI0F;l(8ymHAPWqioZDKVLc>BxW;{J#Gk zda14cEZE9J3@xN9IrRGevG*FB_eov**Yfa-J_>YUV0s>*c6x~jeH6<$;rIWQdG{yplR)DPArTbU{I;s z_E)Z09)Ig6l{p|$7_zo=XRHTlo#LmKbu1q*pY>5imATdMTfju5yKN16s5hcR@>Uej z{z`U4QoJ1VlcJ!RSX%C)tV!In+;6$FQG6M#Qm_Y5yeNZfOEHs0A1!#&P;NhK5_{op zPjV-OKQ8NCLRfAQ>1S4W-BCo#Q$)nvK1mYk?=_=1T^7Hr=}2)8Pr_+g*DQ51LXHhr zMYd(gHxeleILOht_qgl`7UXiX%|>+5vd>~EQk!aOpE@$Ua|@l5ZmycHI!9Z-G?qy) z?~3UjOcl}I>Ap4aslJ^-9iRCh1b3-T76^v?89M(DFf)kujO_#Z;~BS5Q0~h0-YV_4 z)NytEJHEJEJ;CGV`MJ{j0 z@B^T~H`9SY2My}i3}z?2n`0EqODondn|g64Y2R|Ow|TOE>%HsT$0tPzrXz5!?Ihduv_b-^%g@;4%G zNHE&|xo8w4(M5vVAOM9{ij@evb^=MWCIRat%<%;jJt88jDRC$ql^EO(W@d$s2yQt(hb25?d*~`u$0Po1H`Z+nZ^V?p93J~8 zCYH}C&J~5kyqwZ<7he*JE{}KYS*R`3%`uykJ9YEY9a-ncYA+lTQTdl zwnpLns(*|1W>Om%JrLY1#)`u^W56sY}Q!t{Cs>Q9TP_Z+{5S)dXwU z-1C0wC%7hDO1~~FYqmd}PE*1R31cvjEPxg}Afb*V0XP^!OmUNK$tXN6LfwRXDo_L- zQUX3No%D?(}`yW^O$MdC{-q=OUS<@#|8{6vH zx_M;bvYdbWa=zjfYTN10K>O8$ho?4Vm4AArJyGc|pSIkErly@xy9hDz>nbT-hZWs2 zkut<}?4_0TK(g6-Woe_I$~M}CiGI?AYesa(S{rX$-*28&PimK>OTqQ4RlIaZ>1dl) zZL@Z0nUI7(R;`qreX%&bWY#}_`RXsUcHfDf(!rhSyfx_z$o6LC34NEX+bLF=1&%nK zRNJ+0sm|WBR^qnz{{5s{GA>Jv0{8asL+d4NSYAQ zI^^q12HF)H(&Zs5k`*em{6K5u*CPb6?uIp`iy$BiIAYgdAlH*H{2X`v0xRA?3?Ka; z4k1;g?!wnEJ|*kk6t^MBP?wRf{X>v<{v)h2h4Vbkn+O z&fYX`7V{s;r5gP=3|fP^so33dUrPoBGa0kb{{;CvoASJ_`I;*98io<;F*GQTM(42e zxS>-_(#&2xT}mcwYtXjeUMT8YlbLsaJTv6kzTZ$^lYNaEMtkT9Z~kx4Z}f_W3y+}+aI z!g)Ys(Txlw?2yeoLb$r5Zdn=KNJCIGIdY6+3-TQ&XNV#LmJrcF6dfYIBK)ecN9P?+uPBb(hTM$$+cTe2lvvSnK~GL~(D4K`q~!3F~cOfX=;1UEQ9 zLK+}WLI`R2NS% z3uArEjOV$Z`{TN=`?|Xpndn4KI(a4-n_Ir-Ii5sBM8|I(3^;*uM<6`Vvk^$iJ)WG^ z$u|%8yzCrHy3S2-OeUwVyT$$7B-b)Dn9FC&kzgu{>~Fbnr1ft)=(U?Yj`Y8`#%wpA zGk;k)6d5op)&pDIE{Of+szqjVpMqa%!HM1k$OwPkOzc@CqG;UaDzaAg4f< z#-;IcaJmC+)nw@hOS>0rR&q5;G^xvku#5pnHplO_yJKF#CcQJc%$(sQN#vwo%;zFf zO3N6F@pwNT)&h5$L-uk$beN=(=6PYUlvJBnVce+t{)LqT^W|K) z@W_|)0WA%Ie-vnuv9v`n2D>P+j(RK%iP^s2TW zSgeCl4s2=w9ID9W!QhFi&RY9>*FZ|HKUq#-7xjY+jY`0*QKVfmetJ&NG;;o)IoaA( z>$`c?lBb^B)TPJeXOw}dOeCra!IN;94aCzV+xC6P`D!JY@?DBx6aKfPkiW0TWQ zY)D|TbwF^i=uH%rj#%NDtU?lF#u^zN|%yH1WZm>t}8+G;zTR( zH+JP!l9U;|34WAA_KLiTHtAj1?w<4D^nY1CqE8qfAB?H-?tE&^w$l|PQur*Q#YBir z>YuJc=;mtF58H?QnI0N{629PvSGB!!vkIpMufJDYW1(C78vxD8ud^Vq2STDvnVsH! z^ZxxwYdYAKI9=!|JQxhI9u=Xh%*{3}=T!QnJ>BFb{swF;zfbsIb788;g7sCzF8&g6}SbNn%Z%{X})&vA?eT5)?Vum zodCg(hK?n7-E2Gk?x8kbqgFLI_b>U^z!=Yh49nJd!A_*Iq9s!4ZnH4tUdn<%41jwi zx5+EzJ)|>5jfRWm8gq;smz;6Cb{f7+WR!HyPt~bZyR#ms_{pu z^dVEtAbC5S-E_H=eN06Lu3$xTxAA8)H6sUKD3a(I=()dmu=; zD$!WGRVVBj0NGTymZZ66mlF^K3v<=#U@bCBE`9`C@!G>)*!H4!Hy0leK`esb6JrqPA{+?wqN5=lxHu&2c^Be>DBlr61cVUc zDF)!RyXC1pQ=!Q$@UHxWbyVY!=6Gf?p$z6VAt|#LMufBB<{k5Fb`V*FW=1vr(1 ziDWStKJQphl&#`PwPv60oICFK*L&{U2uX4>WcH_R*6NI2ae2_*Gf?k}m`R4$l`qbn zw}-mbtIWmc^z`n0$b6P|C}hw`LT!tv(bC&zlf~Q(e*feY?9Xrq*}>BB!qz>&cuF7D zMqZ_(1@vYd^QeYLk*(>a@x3%?mst;yaLemH%0{hqPtRG&@VF}Oz9VT_A=bu6X(Lo< z-mV*Eq#UEvrn^Wtu#0L= zYjj=?%_w85{=gdj4r&F(H^aVfw9Wx9X0GcErb+7~2yOJN)u~8|5SQ z&Hc4m>%30h)KiT`cWtq!6O2Zcrzd-0j>)Q!EsWaHeAJOo@)r)(L>{B6*`a5Vy(n2@ zr>3pQb+NLG{9!EGf0uJK0%mM(=<7il>2B*W-l84K-?GhNoT^arF`lh&+QL4OX_bEe z(Q;+J;@V-O=HI9MJ$@64WM@xy=aika&YjpEFst9H#Y{VM`$Q&0JWjAwrMU94$J=^? zI=qJ}B+isYoqpW`I(E;k`ij8_x8RiRQk3QykWQ8ui zco@&+h_j$EUXy7}9FxZjBroN+FjTQS+!pSx?U+1c;^3*=y(r1Abzigq%+>Qq81-a= zy28UbdC59I;iW3#{p4vp7+OGcFLd!LJmlSYkZ{v+r>B)SuR zU4H?BxjEJ4V|$}vCJ$o(?AIunh`O+PGoA9U$y=4WyVT!TSPl)p?!?G3Rr4oFwBAxp zz2=gvx!iAF!mz{#6Kro{9JAK7RXaUwqumj<2d*pIG_q38H7pd(uPa+OySDLEA+p_` z3v^sF`J4)L2i7mL&7?CPbM;S~yo0@gWmA_XbO`B}YQ;$+5fllsN<0K{hkuCBBg9!+ z>%R9#iWuf4n^1>^JU&-@?8I>GHp|IcwK>E#S7u-D>Y~pvlF4MukE^G(-rmTn={S+K z`c74MZ73J6{ct*JeqZU}05V8P-HD;=V9EyJtf-yCSvwKC#$rS^7-MJ1N#(fLMvwGG z{C4>R>)8&9cp)9&bM56HW;?M{*frWXfITbx|3U~#Yw%6 z{tWAXj>%$PCu^9$(*4#-+^Eg7K}81!^Fwwz=f1W}onpoH$zx-1Mb%+5`|D^bUrH9u z&y3-XwRsuTvK^^F$Li57jui|Zv^EP)b&z`Oezam7YV%uBh*`yU6f6jP!=W`NA%%#; z?7`)>H=BvS#DFAq;u?@`!&4z&^Na*ssu%PGd+DN-7AxZxNXR6smed^%ZI$$`?Loy$ zdwfE9wTm9OchW$WtT3{WJ(XoriXD)YB;A{fsyb(PL?C3E*?J-~ubyj6x8D8HHqx}u z8x{Y2>t`*kx)^qJ7G~{ZkUDCIOvot0d$o0U%(&~5@vhce&wnpzeCfx=C$jEIrh0XL z=SYV-trN4^gHzGS+DkdZTy%6dnHwWA=uQ;eGL#Tjj=(I*py*azz2O)=Z?mALmyqyg zd*jx%AYRHYKUQaxN7CsI*u(7M_G+T9_pb}4->%QaQ>C$=Hj;D6qiBlNCmX*WEK=Zo3cGpCcAmOn+RnT> z87*aBX|gKEP8vI!zLJ_~{r6b(>&7NEzo833CZ|L#j(Up4kP4&PLWqdht zHXh60sCu@|{`-Xcw9>yG+NFxc1F>!W(Z+tesKeI<(uHC*v+h=HJKx>3EfC+c+FE4N zuq$yld$lO0s)^qlKA$eyN!6JT9gHN-b8~BuJ0D_eVYd2{?oER+_RrqSh)CdLoxazc zP~_2~;zUQbRsQx{&Z0+t3dl9`ETDH&TOeDWvDv?hy$<)sE?W#5`F$s&E~!Z3!Oi zN+tW{mhd`l?@fjw;8gpR@V+uOB+YC|6Jls_@m9REf=047S6Wc1J!+)&cioW;LiK*5 zTWuOu8$)vg7cl(C5}*9S7%C+1PO+uPX<9f9~viEW^n@eR@#`hKqwXdstZ&qK89keQA`((d0^o zdaJtyBN%CPAF&I_94$9lFS9(8wX%9<|4nP2zCN9~SuaGQcIFri^@LqNoUEQhvuf(` zY1UYB8&&lj^li6Z%}uEDRA2IZmQbM5||jMhElbq$JLt2KzGr(ec*${ zlZQ4u70>9~elieO`j3p26E#@vQ^i_uP|mx_dA|XslqI&`k5b`ltG>h$Aa z=xmJ>iIV+s=rm90;Dw*feKuES$T0I3mnCbvV@k8k%ZPTRTK_jsnDl?c*j=7F-me~C zx6_=hsK@kNSv?!ig>Jg`chV`n{J@3(Y#(Ow!0=_}t@)RXuCDZkT{uL$yg$L0VV2#v z$#ox%uBmzq{yMw7$^Mg8g87*j%bNSE5@)NUWTB|Qr5ge`>DVRCoy*v)D z1KIJE@zu^~IobMgs#{h5#{BRIqz7I6g;MMOGSMG`|6nktt@RIcX|uTme(`(u9X_S6>4%Osw?1SCc13;ZP_x{<74E3cmYKd%PAIQxjpP7h zHTGgPkl%TLVMyTYA#%Daqk7J=%IA`>p~@e$D#oKI{1#qCh|>RDCZST6Liaef-b{wR zSBl1iHMBft&0&>wT-QZJTZ{gI#Ew7q|G;+vU(yMi;gMAzUG)r;*!Yrm@R*c^nVvmJ zc0`@Yu(WqR;#0)gcv3Ll@p9<6ahy8b1<+FJg5R-1sm6-hk#Xyfr;C} zMR;hX;3REef)0o0S@>a)GAb4E5X39o3q~hPTD{0b z{?O=-TH)q$BYdldJe1hun^2j=PPLa0FaMG+_^c`xUhh%KDfofPURfz;535^C5#Qly zbqHx#v$|+mS2?c?8y9X=(bbc;)r!S$)l}IAEC%*BUQl;_(w%7CJJ$MTcK{Ro+m+bp zcaQ7-aM}LITP!KjRj@F_7^%5;>5LuU(4TgCtg?Hmkxaa#j_byhszmarHCAg#t`zp0 zpu{%3WGpSsp9eQjX$)D==XB3Ma%#75Ivz2ZumsbT)e9`=}ID5@Qib-mRKU$W0> z{p3t{Y|SGb0Vngeo{eOBI^1g3w+htZY@;(;$iKF|qu8CwX5)_>JSX~YJJ~YKMlqHp7hK#*@!N!o9!&Qa@;tZqfJ&)C-h>~46vJEZ!yVmWq7k~v)KLq9#93_x`z?=)q5l*KlPx+s^;1? zPPBxO6$16;aGC9%;9ry{$FUe;$XC!f(=5mZq|BS@=EpD0~N3gJsDE^of6IO83!$HeN2~ z&253+bbk~jF={NoFPd6y`cI(#=yO*GdlG3k84B}9`Z-6u8;YWcE_XDjy#^MPsIwx z_e}M$Pj<$b{;RrD;b=7UKJ(gOHcPe`&fMcOkRC%aE%-3YWa4QaWei<*0v`%~AaLP- z6|+XT(?Yy9Wf|F&9r%Y*8pni0Rl>L;ygxJXFPS0Gv$aM zK{hObPB$A!LniCiYFGU7HfrzsLFU2oGeH0Bdo{mgYz{iTT z%-eZKCPkSj-b(zC6eSXgyqrd$C9!ids+|015sTXFpXCd^(Bp0X?-6=WLo*E zPT@^yQeTtJo-vU_ylG2CXA`rZd-#%2@qm3WgY;w59i1=(oSafycTF<7)PLcd>R8#S zp?>|JDtzzL`S2UJB~YyGRnot}Qbr-Fr_*>N*A1zLnvq^*pWWD1(6wa-Ui|?^4HP>9Eqn8&HH0oRM~;pa)v`u# zV^-O02gvYON%1??gat`9^6#pyfi4MqTUP(EK}=cs=N~ZZzeX4ksN7w~W}tTjag?Hw zG3suw6eO+S$Z+@)SU8@4LG=)6sCua>r#6<60N`b#;`KR6Ss6qG=<%l{8x`5Ds+WHV z41oRcqY{fKV1*MT(P6?ehlfq^IG`V^NLanxAb|gAwKw_Ai*>%faCYsf+^!!<8u@lL9gl z)*2?HBXQfY!}pA?_1AW%?fRm5bIhkhyOy;Y{ZW?KGtDl)H{iWn)G0ETcEIDtd=lNQ zKfzXn3gayX=6bdh(6OeM^l`evWf35n2qfsO=S(CK;8t^fl2>?q7D-ytJcq}1<}k&k z#+z}zARY0TWDMBdv~Z#M5_g^YQ`9GTy>>=|5sAa#>*S!M8K4!s28EK~!gw1Q1mgvz zg1v}#fLt?k!CPzp2ohcVq9jd|#^0N0>X5MiUA8c%GDli(NAj;U-e%3-1f%7N=hT!h zqRXxS;agid$b`BMIfu=)=Hbl9KAozI}9UuBq6Ji&r=~XQ`gmm*!GMRXb&+qi3>- zf%na-+Shu5DuW78O`U4Gncp3$L%_!F-i!2Kt6kaOxH=twrh zc>*?4cSw6dWc_d_ZSih~Zj;hYm&6nwfEjs{Ej%&LFJGi{AXTg!Jip_?l3oJM7vxFs z9-bfKiJLr-q6MENpr^EWd1GR4n6s?)ZKXf&2f0#<@YVI}Nn(aS|AzWx>-EVQeVAdk zK!ffMI?b0A>Q@c~RFoiXGQ0CfS|a%CWKJoLkwIGu zEStgjTsm<`J2ih%3m2dN9knCjeNH6jI|L)sv88FNgPV#`{K_(Z zZWuj3!BTr!WGhx7;EH$p{E(QDfNN4Rc@#bYs+pf@J1lGhyQFOSTQI>lsMf2-jZFS3$Dcfy@&|i=G%=ekw;sQP z$;atLK3AE|lxr)we6&>P>q+7Bn|@>Eo*r|h^}C<1WM42&T=?n_g2*w`;Gxz1m-*OBERqFx%G#&+_-jk)+}pKExsyQ{)1Cc;RsXDu>C7{|wbuXK zF}hS<){$+8k0DjzH)lf|BKh#;Y`jw)Mr?o$Yz6dUy#G-f$$ytlmYMNSp2$GMuq14& zQwsrWN-Z}RRQa?UECG!$q8(=qr#_s38iqVU(Nw5h$D=^zS-USw!Hd>uf~7LL({u@G zM4>ZNPx8%BcXbgiZ7G#tF~fo9%jBqGkH7N*^==5HLn6;+)B$etRM&qlIZ7MXCaSdQ z7!WhqvkJ;B!xmM~2?#ZlJf`hkFeWKMB$#+e20u>NX%K~^q~&1oF48t4hmc-lJJ5KB z!E-_nYEzdzKZ1`Dv_pcF{Gd!Z<9Eb!kj{Fzm>7}R1f3MJCV+cySq85puP>YpOjYV+ z8SUpa`Oylw5`#is9^_>&7%r%dPY04$gP0a?W5@6`H(8y0>#QuKU?6Y5KHoY!HI;}R zXnb#7$ye9D$iFS9GkWEx4>*xxvh@U9EE{E@Vy5+LP%`bm>)6!y;nSe1JBP6Bl+9$2 z5gv5Exe`ko!S5MU>Bw{BMup1iaZPu=Kd%yM@yS%a7#ckmFh?9Trp)DoO=S(6Sr%@L zZ*W+3NkhCZG``V14O6r_1*bH{KpnG=)bWX`n<#(dOlDm96Zvnmnds%_ zT$c6o@p=>KGWBd}E&CV+P6JO{MhpFO-dtY>zUMOg1CI+fj|cKjQHmMvG{@LIAzf=A z69h&ib>aJju#0&pEA6_(w9<_(4&EDq6(bUtAwq3@C)2~=p9CGR=O}bPiejt*L-f>V z31KPxP`jl~E@R41lLoV)QvhDiHf9Z?B&fd^<0LG}8!^eS`AO(Pj?w7l+a7Qy9*rTO z8&?i3)M&J*Ul9=Ge;@nX)e&Q48cEy2=A6nlpPQRawx(KtwWwmqf1nl9FZ=d>LSYJW6`;6XaPW0NFR^NvI(Sb20fkfS^ygAZC zUd5pJB(+cme;FIZg2G$X^odBveh6M!aD04+aeK1wSaKoL32@VUYyeDlL$ogtzj5m7 zQTP|}+f(_SV`^`pV09kX!L33$Ef!IKxnkSNe!o3@EYfj!4cc@)+uS*w=T6&-eo3=3itG?Re$gZ^?YUl?nI^IU`RdK}w*DE`+34xE>iVD9N*%eZAsFsFWvY(UDcd z6cnnLXp->65GMGKB;S-&VxkhIq?hek$;CcFANFyvD3`vrl-n04HV9w6^cLg|N=i(q z^Xh3YIU={0FL?DC7RGzwWzkK!7@CRu9k_}?~+dsyKm zYaOfVBiGD1#a8e0(>wdlDRW_Hp_a&!0~vo;&=22v?8TmhFZjjoA>JD~lvv>gas=qoZo(#HO1ZU)TO}$2vVd zm)X`oW6hbNTGlpOJMEd5=gZOeEfNn=Se<|Zj#^SIUnx!P5Jp&2dC2CqioT|9 z`uQ`BtR$+H$y9f5(27{&K~^fT?RpvuWWP??2_0n4e1%ORNiF+LmHMWMk+XE6Ms=2* zA1a^=p(g!y{m^VSVMSQwr!BiqAoOPns#dcSjTxkO(zJp**AOGm z(L?(3s^4GrhX$Q~NpvWP>EU?!uJ^#peY_1i*qP`cOU{&{{YXT_$mAc0MZhoO^1O9S zUajrb_}I8u3HCvT?8P)M4q{hW6G4uUDhL6Qi`>hbkf6esf{gWO68*da(X&c{mwGN&d~P z!FTekZMvkdmYhwPF(B95-R_RZ137^v6|i2pWD=tI$TXu(J`6Y5)T^>^I@yNV%#9uv_04K_YV;i$eZt&ac1Uq;9tkQ8iQ#WAQ`@y2pwa}zF1JBZ8G;CtA-+#RS*??G@RCfr2H0SNhW17+Qscxc{SI}VVr zqB1K^pncx}S@=+J2a;X#zWpU9`KYOr&iBpQi@W`L)5jW4BQ8$>Hx=amCw0<;g zFMX%3rjj?cepzU!S8M8PR2$I!^WwNqRCZDt?)&v4iE9 z5q4_Y({m95j0()vy2i|Sly;(N&Cg`*u_70QNhx*+`O@)F1tJmp21r?eEZXsuMn*9m zcslCq1yJpbgdujZwkxEPnL1=8M>lxm=?HvOH#rPX8gvLQ>s(^KfM|2VeY~t5?J-{+ z5e+v||CjnYi+Yuhxbb_Q?%F=?lJpRz4gnpLA_v2uD-65N3mF*kztSs71GV%qZoK-RNP-`!^V7B?1D)>ry|$Y1Dzo%$ua6Mp1XYxS#JyUWR| zo&kiA{bGkInPTJ%JosU=^{tniKf^-z*UZ4S-F&33@888b%&2euI;PV(snd46>m(HS zoyu8Kmrm5*Ut(3}j5;wj`>g%OI^|M*Je;+Edm=ij5(hK-yWQY8C>hySaDBA&O17JI zk^mhI_B5#LznKbKp?oP8X1ip!3Po9*rgM3}n|bjnvxhG3Dn9%D?7n+}d8BJmgzn+h$Do3x^%BSr0(Eo*-3zbeHv3NiHkQaDG z)WwsrHrb>r(U)JU;-+wJ632yr*OcC@_gLJFmxbc^j`i;{%*!FqU|dfg-!nv7_0gXy{XY(=kHfF|wab6dR}AKijz{CMsqWO3z4r0GlsdHdoq9!m@59o zzH`hrKFCc#q|@~GZBTfRVjry!hH|yqUQO7JyZT!F^A(tn0}L!4Pa+TCjGSOC^utc` zuw8k7z_h0W6IqdPD;O_?yWBFm%jLTtuXRr8p7;+E&n?kk)!SdR)kCqKwE2la^&~r7 z7AkU3iakKT=l=>lG7H5B*-9v#u@oz67`Ka65p_t4Q^wUxEWv%t3^XU`^)tr!E}vwT z$be0Y!Z5C%pTK+L{ynEondZeTZkczBJdQSnqg)~HSx=*_hcrksoGJ)~ zPJ4gfI@y0fADdg6fg7>@!askAA=&imN-erI<6geaREOz` zTG`fbzoFEBHKth1tqR*xs_Q!Y$(f{o!}uo?x^QQp8}{#`s#ec+RqhB~{Z+;&{;+e? z#*SW4#z?rL%O49?pRZ)LD_B&wmLLC4w;k%<6IH>DTO5<+57dOauFw_$f?N3DfX)Sd zmsaOkq!}!I_)_J*Rf8#2fHUNIH^ME|6u&O)_NUf)753zPCq;Ryg4#zg_NJAwwwOI_#I^Y3%@RmjH#Z(JK+)^ zon>ojg#}_s_e>4CQgof+>oVpVj@mtVu;bExGqGD$_S=DYZ>jS?9XrOD*MjBVd)w5V z&TTg*nz!ck$4Zti(`5|J+y%UC6R;v13&qmtiCnnBKp{@Mb^1}Y_4kcGhB=|^IWAUU z)lug1oA`c}!dVYy5hv@VL`~(H1~(4Xd#<9?H9RG_ij0Bb zfIW&OkFhlFDRdg&E^b@6Yy2kQbrON2Bm3N*#~yMc>($0hj8toeUO)f$u|GUJgCpg?XJE~Vc_*k`d@~Y*#b>$fFoQ{?pBR;t%>p!ZB zQwn>Sy9D>FcC2Z4g7?^s3~}UMCPwCiw<6o!pI!=Z=qT-$KFFWVjN-BR3e2 zDECbqY0&iki(eGS$LY4IuH@jUi}fuz3yxOOYMzT?2vm%W8zb?a8yCzo!)OUsIxk`& z6PT5F<&3By<3q42UYW0AVLW~NcDFSsx#k#YAB`)j3AxmmNeeaL7eG@f_^Jn~$y=W) zrB3D6ug+U;&2*ZkIe)_)4}>t%M}O1pZTF0hfZa~-Y!`aWGdXq{30VZ9v}Z*Gnitqeo_ z59L$Zm%6EcQ?$lLy^&#nj8jb}<41Rcq-Y~DZ-#s+2TV;Joe73|{91j(DyD<0*XiRc z31osV%~fW0rh-ePimv-y{CYjT^Wo&v=D&69p7fEt`O#9c&sfmbnR$~Ps`jbcGKnsX z#6`yJhAKK8(lMXg0ApEU9iWuQKC*Epb~Fi zTGA|f(z+k+MOTSXyc9rPN4Rb^G#&rUUtv1)2DmBDtoj_lDdGmhP{Movf7U*=w>r-FB$M)zTx2x5`zQu+6vC4dJ)53%TNI-Y{!xd~>v5+L z+aMTV6*I;JLz+A$l8>b`{x%RR+$LO3TN_dmw(5JNDqfJ#-+mjtO%cyQ42*XvsS3#Hf5q?@9Sw2s(k0tLL4mJ==YO;NoJ@-LvAjwMDnI)Y$7LoQPkAlCkJ73hAg- z$o_UmS1mcmfd;}zb#*Vd_vKby(=X<@B&xO~`>vW>K-nuVU%Ixi!%ZA;d zxLj^CJ}i^nz>|PDI?)CqdLU8$FM(?J&vbpGS4acjJZ{Rxa#l7B_tD&G!2I# z9bON}lGKdV7JS`H8KizE-s4rgR9(^puQtac;2u1$Ac>h|g<=GfKj1Co`9$wwLJDs| zaI1V%zyZk#aa7b;ZI{NY@*NDDHzGt?q8}N*>|^1V`I+}>dmgquq8GdPmWLdBy{GnH zLY_n}1N@weJ`+Qh2ujxCc}?Exr0-0*b5@6g%`($M8C1*bIItB~k6asHn`Q;jQV}@S z+ETZQ_7rteW{FN=@WZ)+yW;~{tI%9?jX#oB#XH=kfz{65c@mPIZFTfLa6FQ*=Q6R? ziQpzOO8ZH*9Sul5+)0-Q=t^=`loz@iR_yA zTp+Bw$8XM9Nps|ef!o~uSu42!+H^e5po7H$b3G8EKLX^tH=DjpKyCeX$9*KG|DcObw{x5wb`^Kf z-WT$Hu8-vjdM1#5V2j$>Q7mjZW>x2I4C)hzFw7$SGr6@(Ho>!LRD9f5(54H}X^^?k zzE$_G`WviJMsU0eq1vEO#{TGN3R4!tX?o&ua)uLL$u274Sda%VVMbz;UI%kRm@tSI zv;&wor{h(2Vu(_sd(o3%CyycEUQA=Xd{|;9w%(4P%$;~~RpO}Bg&F2d_=4O8zD0=U z&54y`S+o=2{EEFo%4uTb#7|TDJzldaMSZfMX7sH(NEdYT*vzb1wmR~|lIjJ`N z)lx=X5l4Ase?GOpW)Jq}>Qx;{pA3%7v;RglY&$e;r#_%(7r6E2Gu>*LNt~O;-Q6=# z6K+Gd_0@WEQknWUCu?`VRjt$qLSI(u(oOk50!nsb3vwbie6TVG6rhsm%GiTR^9Y=t zLIa^ml^R>vKAGuk+#9k1ZcZ>H`b{=z26NRwxZBu@zzmzJ^sJ04Rm}q+`S6EPA+B4d z^iV-fSXtgsYsY(bRaD2dIuzerN{rc4r+UsS#$wP%J+XV+dA)q9QoXuMU%tm2VNv?E zrus&Ou}{7|S5AaWAcZbu2`KJ_$cX-0AG@|#M1cf53gWhTs>dpxmCYPP(0nMA_yrRV z)0!2hRmfuLSUMUE`;*oNQVcy;OumGFn+8990e;2KRWGdib3q!&jX67V+H|Tt;2{Z) zw4&r_q?gH2npICEgJTu@@PeddZNymIM^jv*ceZS(_e>D~Cg;o7`8E$=-*`2Y%#;um ziGnyrDa1U)!YiThhk_r;vx-;4O52m?II+G7N-4oOxd6PPmxN0ehsA#uu&$#Un{fFeUgR=f`OfCE?J6l_LeGjRm-{b z?rr3=1!G@prZ;vnjG%>1Px$kxlZ~xWGn8KEAT^Qbz11`$cY&kXw`_J^CqH>)=zPJk zZbJ}NeK@63+@@a|?8ZVp2TU9MkF|ULtA2vOt*d~$H}-HC#t+!9ptf`az9qi3f&Dj2 z31uI?(=fkYtGvngPxm!@rk}0(DEeqN%lWMOOnIJMR`4Nc`U(Lf*=nsB3G1fr05t}FRC46{n(24OeA{p zG|<637v{~V;43|ECcq4}0d(bd>?KB$yi45Gn|^=AHI?cnfh}+;^~Zt}? z%EY4D-}+)HazZb3^yr?dLZj;zO~hKEu6N500uQ5|_bW zaM_8#JkUAgp1C8kFSz1n{rh#2X}f)E3Qp!-=dT+bk=k4m{N$NZA^c2mbmzk>%J=DQ zp)C&fH`7JrM(sFikfrkuO>H)3iVE#&o;5UFpDp?wv*sSMPvDUPYkB3j40G6@Wb6hK zY0Z8`p##Gsrv7TC+CP=3nHq_Nxbm%cQVGk=E*=9q;Ad7We}21b>1E4gy6eg0VLg|- zdd=0NH&OyTwlf`zLri!eqCZ~MeKQ$TJqf;ui#u6e(82hE0dF#-tx#_;A3?E-IVzSp zOrr-HoJl|R06?*n*_fkI6<*zqxO657?Ok5IST+6GO31vTn+u7&ulA3JPQ? z$!*PeLL9n}i|iG+8NvaOmY~0{sy#apR=sr%Uh|hKHuzAp5eA3Q)sell3~g0E#cEPz zAJucygJbpVOQivS*Dl9$TPI8L^p?cBJd{;PhmE-V)uhhf)kzRS0b@2C+ z46G%tP~~AeXLkQq`xn zRGz%#^8w5p%( z=eJk=J#k#TEQdt$h3}O}3A3IriD^_(85X`G=!LKxabaQ`VvTq$aev;3-OSOA_)S_7CwU&ZfvNHI@Pns$TIOEJkSyS?FaehT+XkJSsyJPCo?Q!w&! zOnl-PZv$(?PMZ@vzpzYjyVyN%z!O-KmaM!l-k>yTB}T|)kxRk5<6ZHdXyw9#V<8i3 z;{)m=D{n)9ksFU^*n$6Mz_(dVP< z$n3LSy|Zp}#h+P@`l1o5xIdpeQJR>k07X>0E=`U#tY|23iKemFcc|(3-O;+KZrZKk(Dl1!Lr!X~U6}@j zn+dQKr$DO|Z^=+|uuHAf`O8Fnr66-A>`p?8tL`|?CwrmWp;sITCkvL+(-0^_9T^v< zpH)O=2K%WY#~d1^kodKc#=@fEHEV~4k0mnMZEI?UHET=0$Em-$dphzNmST9WFB1p2HP?h!gbet_{KqiOyq* z@cp0{*u~vqIlPt>MS>$^syrXoCc7yF^`oKhB^N1P_M)TpaDDkQKkfNfxf9|Ry>y4{ zm-o{4{*ol~x8W~{b8EtkeLTqcF}9qkC^de-4LrIN+Qm!i(00~1XOaL_LxuW^a&uQ6C8#dg z$w}0O=uS85PV2sW>dwI-)m@@w?>8sc7)x_W%baJZW6I9m@9#WH4%zkor1f=Ey{}u{ zW@a=O<_XI5LPJMWpZRNy3vI&bb7i(9@eb&v^DitSqfh{Ybv{OVpsswBS)?Nsy>I8S zq^eJ<<-nH$QQIAK8;#vl(`NsSvzzUx(a6L-PAI)p81+quCSO6;#lbYnBN9L9NjSnmk@;Kx*p$fH1hnDsJ$t>_KgS9$Qt||0#$l2k%e4S{p7d^qM0G5Z zpe|pAUDZoBLkwA*>o_f1(ViqPmhbX45ey?E$9?kWZuz&zY!}B40-mAID9Hui$4E2c zPkGgg>4jVX9KRSWUj{gpPW}YhMf)*7cd-eH1LlXomAoesuPkYdmu>LM^mMx^_r=D& zH|Du<-j5()d`T_>mq)Ik_c;)sd}^p8GwZ6+luG0VHQUG4im6670Vi{e{;#MhY$Mx| z1ED6uqYkI2)dhi&%Q@`>*QPhu%y!>Uu zdZh@NcNYTF>-x4d>|Yw|&>>xbYjRaIk#zj6UyckT@H~Y0+|0KUsS@ zmJhF-y2l>#@r=Tb5J?J_(FM_F2_|@6D4A z0)Cb3t)UfXe0?HYTnGeF1?_W9d!PNjd#>m>siu~R&0>&n+U%#1L9#^#m9hPGugwReg__=G{!!TaMQX<$GM106`s%8$ulj3ahO9-n9Z~e{ky#?=7^Z=T zAz>K_2eErI)%2U0rpzHT8&6QEatqcO?Ga)eFQ~kzTLaVcFM+NpRD$wxX-*Z&)r|=^5A7#9nmyzm}CV>o&jnjtsf3+s0Bk})hP4H5b%@^PCd%X$X zpF}R+uh*L3@xQbtd@Do772^?RU-H*!1n<}SPx_vEa`O#=s||IBy1w;h0YJRW zE-_y$HGDE?qaRNh9SJQPM#(4!t_Y+nHPh<7J=mlu=iP0{-_apJjFw{FW(m zaO}B@y`}?Ls@WP)KHF~$>|N(H7Uz9Fb7ns0X2-YN&ft32SsK|MO++mCS-ym0y_QKv z!`bxVjzYh?CaNN#>{LE?e%+cFb*yLxjjkvk(g0hy%=8o5ZRy}b z;N{@jpz)Ns7}&2DgX;{7(V7q)3avJEuyr654e3yDTh{T(JB~SObua+cmYsh6rq49v z3{{8LG8dYQXutX1gW%Sn4#Mq>))OT+-}9qV#t6V{{P0NdfqaexF%}C2w`5$JT)}7} zTrkZy{S(2p#txUQZWb(vcrm2e)n_^(f2@==Q5cRUbjTM;I8Mq|7tZ_bZFe;D(|ZP- z@qI_#Qfl|=SR^-bYautB4DFJ))otH!j4+sGh{ID2Gh$i4CQUSqa_ zUCV|)sY<1%AUzUKs`TQeB9PH*&&x}t;FYlLup)S{1R2S*$n6L_5)dK7 zdoEvM6}5p}9%7=0*pRpVYdhB~4lZ)gDH4cEO#&OW1|@(bkK9%+wS-46gX6Ijvr^Xb zXFLVr2+q{S-<0}=N}HQYo!-Zv`$D1D|s7e9kjbcc` zN&J(?61))aR|nPBeBLk_K3G2EDi&}?!A2-?IpP~;>&2QFb~ZC^lvKFq=PcAjOj4JntXyKOo3WfxvAk`lmci;s*(}E zIi{_~BHT@Hf=e@etj&I2^gFs919DN;-kL5Fe74vH8>!4t@9K)$KE<+_%i^pEoE^5U zW%!R*e1XA4DzsWAJT!b?I&?3*<`;)XLnqV&{c04~f!^UAD8EoVeupu%`IQulKk6sa;Xp@cayXGi ziz)M=nOZ`oF~K!;KeO&-Y3^iq-vw|1ChFnh-$uvP;EwDp)m4e~Cy3=@)6CE6&v zSR*Egm2n^WSvX~4lpex9`5Tq*?G49M#g)NcGd!f$+@PX&lIZBS>*^m2Z9TMI>$e@X zyrR`LD&HTAZc%E?O?4kriDYf*P}BMPTI1~u_(yG~Zi77r#2nUPy$(fnaGZW zfV)3qy0!BTMI%Z_XjIkhH#5_%$TepDd9(VQeyiSNo+y~l4kaCcLA@cY23DKN=|nQu zJy6o2N1*b~sFRbd(csn9|Nhg>+pQ(ToaxwYn5V)4S&kVKm*Q9Ci8=~!e`6xux$?Ar{-|?iclXYUo4P~qvdx>HE;no8 z4@|Dn2btPY`tlfSMqk=*sw+Op)a#Q6^q%(ztS6X>j9fj5bFRR}8>c?1oHI8a>^By`w&C_P}0K?kay~K9Q z{r=*#+7^n0>vY{$PaI9?9;F^H&aoez z)bDYQJ*&3Cn|YCQ^b$(EkL0CZGD`N#XGiADk$OpSu-A{?v#?(H$p0llPI$DG_qZ)) zK%PK|HvHrLRzU1P8qcK9lVkFLfAa*Sun2oe?&?ux1kICHO|VU)kz`n&2l9@W77J83 zZ2129u6))S9I0F~6|-Y6B{nnZ4@UfV zAFHO)bN6mk;rScZhqqtVFzvn9Bn#K(hKvKRGQ^x*Uf6uyr0d@L(BXq@cYSTs$sVfE zfRoC9zSjD`?hV_i3_O$QRJstalzn%1R3o{#FNCg-T%i;nGR7ODZYIAQ~515NXCK)2q zNO0q9GIB=jB?PpUnt==i9YaJ=_ zzf;wztyQBklt2mobpCF|KF2>?qt$P;ewb*t^%2*60ET(+DmCS}WHns_^vAR89DIu; z>VsUJN1|J?D2ZCb|7R<>qJ5Tljm;Df63 z-h_GAgIf8&xS;hhqz}H-o6$S>x>x4nmuQz8$t%Mu<^$^&SK{_Zcj5 zd*9k?)w}BY&FYnfiW=Ucf7PGij+$9AwAeBdDTk%u-VlIu&sAl-(= zXn28Rg{e4@qa>V6Ca2C;qMN2MR`bA}c0%b*Q9`*L&Z)s8rXHCjgRVZtdfwXjr z87S_2==~*qa7#XV!z4M}h5NLMUiAkz&ThG>D+=dIZ|%zV%Gy0*@b9pfqJPySI!rs* zDe&T|?~DJIK!_o<4_}f=&VbLIv&A~biGCDc9H=nSx*~(|8xbCkV>VU}VyMl03Ri z0FeaC!^9=<`1Y5xMK!)J6%Xz?_*h_Y;;ZWH`S2~f%d^qcc$_W+l2`ppSFc+v%syE; zMI$hHe?{efr*UfbX8SXh?Ngm_T;`QIdXt*mt#Uyw)e^ICmUmZ6d z43}(QFQr0358a@*ELcv&x$k7%j%{4uV+SUk!jxVEO-J3EQqfEM9!%ejXmsIR#!cwn zp!3n33V*S7hDB0)OX~0svw+XBqoz}TdOn22Orr-0S>KG<#(K z%|-7bwjt-%_Ahewo{wo8)&S z-Cu9*RtJ&u(wSq0t}v}ZZMvr}>w7Ynw^=`|TT51DnIRCpU`)6bTV>p@m-WVVdhftz z6Kk$6R%vrj9!~k|!}pH_f`P-=b@@iFWB>cg?n**WXY37O5>|KKN;a819IC9GDsMzp zmg%(NdbrnOSDM=q`hUoJ7x1{s>s(lS_BVTfv-h6aduH#MJ(`Q=Ds4$4X(WxLktIvA zC0n*-kZswPZGi>8316^{4F(K2#$W^H77V6^gis(3A%PGQl0X84q%&gGFZ{*~h=ckRW%V=h-Z+&aMYpr*^3#yRvWzBBO;M(4RPhC0- zrWhz_W_8vR7$84OHAO7L@xqChGJbmHj=Oj zGk!TDOclC$=Fk}Q&-Ps~j+sZ~fIYwZx?z^_db4OHJq{Ew5_Pq!Otp4bvS1|(@eSKc z$;;Qa?TYh1?qer6CAoS60OPZ*CZ7P6Yg2SQX^h3&&En2r?5V^qcK4OiigC7nNmKCE z`-h7ViO8_t+Rr%_B{MdGmX((}0d?81!<3Hiv;CeDotahr2UoNsHvI8r?7CfJTHl$f zGoT((JCbf*a-B$2jc2g1EX7^F&0TDoJu+mOp+?ryckuPM3s&>xYVXuzFYtYH=<_x< z-PFv~PfS7KE+g~as5}=ihi$83VoLidC?L&1R+KHjI@BG>m920D>+RrA&1h^R1TnO3 zp8R(D+?Ts${;7ZZ4Lly&UXn~UXO;GYe-aMsM3! z{|AZp3?C;t1NC?6tNNCNKq>Y4vZEo0{`R1*(A*lZQg(|i7m9i$-I7Il-7jk_9+%{# ztN2jf!w70y*_Un%_?TY_YJq51qnt}?vg}tvz_Atf_vXM*hoTzYRbA2jVFsyE#V?2L z;Jq4+%StSy%At5vj^*RJU$wNf?(<;;TK(MMJsU$-%wU!2t9w$Hty@v@1ryV^G~J;Z z(5O&R#Yf>>i8%a=49_AN{&X_NaTDG;9yPl< zn^YcZFXJ65GT!I-(J3TDrOT$fq%k`hkrf8QE*L)?tj=&?xfCFZm}06UU`*%%Jvf89 zKeo2AkhvjfaKCCdVpVl0*cS?+J>kTafXpow8HBDXF{}rMpFSk5+IW1rg$JzVr#DMT z_PFeqWZeqOism1=z6I&9n#y6VS-=m`)w@pAytRk&`29K&w>m$$bkhKsM@G;)KINA< zj{sd@CPGnzaU-PTi8M82Kr%^^Wl7|TKC3j8H~j%Uy$*_`Mm%e<+Pz5qFs$0V9)bt~ zBb~4mT@PWrHVeXWME5#0M{~2-?XHKhl!Y~M8?zBaHFu_0*T7dW;6hN$`*hw5aSU=7BOT$k_CdKi%AGzDX4NpqCkj&*Cw41gng08 zz;g>?2t5hWGSEPPmQs|6!*D&wIKa+Cl^%>bG!0OHC5bFx)&8pGL48T!RfHY-NhONP zEM8F-%4>_mu>zAoHEX7^tJlD>TL0a$gLq+aYZ>2K9hAh=8?dlYe-}>K^vs<;FX0%Fw;W1HG+m`88JZ z>UEa-eXkVMFbcEJ%iu*yhI-v-0Sod*_|;#J|4dUFx2gV~AM*?Rp4PGnA=rm8M1A8F z_ome;&IV(t_OzjwA-=@a2f)1uYna30V8El$Yrf{GS!}m@>&%KgI6b4uk!47fAuj1+ zb|%NIeb6?2&bQFh2Rz?}4$ZqSc zA%PkeHyk4MKna(sgEW*WEQidJU<0tFz!_rZ7WRYh3q~&?UV3tT1x!n_H<%PG2mR(@ zYCco70$MQdyhKOGvNL%WYxkU zL$;0Wz@59glKCj8|Bva{Y7k?DD$pEe5*+klk2WF7qhguiVBE^xna(R+qcN{Qd4gx# z(ef#lT|rJ)^Vfr$E-ih_o5J{ndf5^q?x{-R{fL$w?e9pg9SDzDQX^X#W+SL24_5*) zj$K3B&qg}gH#@n01;+VPj}JYmarW({YVkphX_u|&1Y>PRWfS~zLkUSMoic}poPiSd zU{66Gx`Iu|a`pt$Udx{CfM!=NDsGPwo4>vJ#pc+$7G63x-hDIPeWBFq{;HrvHno9W zk9VH24{vxZUXr#}E3(Zdqne#s*TmwWSmSpw-64+`l1yEb!yZ%8N+-SAH&8EY!s__?4HT$lZf*@0k3Tvd6BitRy1;b>?@^JXDXv&TIUFswZ)R~cJhZbTm3qrG1 zOlF}!0A8c8F}e)cY>1yH8>#0}b?gia!y^E9#Z`e%#AA^5knO^(>mSvza7A>H@NF0= zKCTS7Q5OVGcu?}f0N+HvSxl)Y@kc>`K8LL%c*4$Mpmdw$W-}hY!TicdxS5sS`s3A{ zJuwF9D{D^eZa;c^i52#rf;1$2*?Rm+Su3WYrq9Ye)x{W=lYyl*U;En*&qkBzTeZK} z(ruesI_y!5HpoL8XR^L5ANM&@wZLBcG~6#?Q#+#)B13=2*la?dG;RrKD-!O zp4kkbfj68p2TRc(CDN@WGyk#@2tpAL3vZ zk>Vn$Lnilt(_AdN11K*7ODdPsq$-ZNUO6X|xl}^O4oC6xsKvRk2RthkaHxzf5oZMu zgevy*7^v41sf9A57=~1%^ff_7k(_=oXA+?%vwV>fuF8q!ni{fca` ziDPR+;Z2vdmW{Cms0mxwZHCaUU)6m`KTSWGqImua8cq9{oo6B&WeQ-zj;`BY^NJGGhG4Oz9ts8x;l-p ztwon@a~i`|xU@SGNwjqy{w#!Dd@)OT-HHcuUr2>QMk4y76Q8gWTI5SBB~#D)t2^2T zt=R3p|Gux8FJdsy$i~^l4uGc4i56fcOCMa{zNAuzWNUTiHH(9NEQ~PXR<(fxZmL^6}Pf^>XHK|ZqYR;tmGK7fqY}^+b)6w*sf-a)Iiu*L| zkpvgSd7Y02X5(hQnTKMVf;UIvr4}RT2F!ulzlS24qQ!5DS>2<T}7428yEzt=?^+q5wqD>Hkgen4sekk)^ zO7-zk9Ry-s8F|-w%V5BdgdXm$pnywGHcj3ZFr;b`LO1zQR0t4*@nU!hJTpBAToJsvUg^K$ZseLoNZuNi8oT!e zP2(P``5SWJLsgFcOqOf>x!U8rSC$pf`0dhkE^n4>c8)p6ux#`_a3KJP*ug_8d)wJF z0X}$z$4fdof>x6vnR{NipUu6KG%nkUc*kBwOfuN(rV{%J+c9AvIUCgH3l=8lBVdm! z+Cnd9#bdPSwp>E-F=4UQ3OfjrH};$)_cmfjmV8g#Vh<^4Z^|*YE_Kv9AlZ6OSDxjF z`L+k%33h8)W7hki_i}k4Ig;b?>ptVY&##Wjy`6txKhNavt@P*7YE++Sb?gbQHEYWD zc*V_Tli3!-dK#1{en;X3X@uKXHg!Q6U@n^IGLU#Q1=!A}^~J&{8Yv(i7VMV0Ak~Kb zXWQB8W`@}`N6xUf{BDu)1$V$o1-tO8AXOtwGUN;39nmi3AE9QgiCMArV1!?ToZ=hk zU%b=sudp^+Ln<2i1DI1Fq)+()#;6vP42qTx@m6u{RFV?KCRi2?C}XAz6(v-CszQnb z^IJINW-4b!UDzspD-Q)8Sb+xisd-UsuUEK5T}-sP6hC3$st-z-qHtF2^fANcdAL)R&AcjZM+1XX5Ic$zkK(&wNN}gQ8-$Ie+g%Uq}CD&J8WvKHjbN(() zJNG1%N^Af1|6t4Kx=$y9H?K=cOY>G2OTN}9+d)O^tNj;Paik&1mj^7*18=DXn|J(J zwz=G@mSyOJIPKQ<0q$gzmRic>`uS+ZEh+(hVmbl|;;Jm06LNGzq*D$yflcF1&tYpP z04G>~Xq3!NS@GGFh0VEEC~OQ3rQ~T=TGGR}50CG1--burih;4G;)RET{2PO@<|loe zZ9NQf3f5n+NFO9HVe)}Ilw#FZ>o_Xn7};3AyrmVgC0OyO1up+4mVOy-(vk)tJ5M{A z1F*pt!Q~+vj5e$mHP08pIQ^4`Uy}V%h!lyb7#XdXBuTf)gx&}24KC_;jt5u@9z?L< zD3PJ1S1>O48dwIQx(ZP~@k4cv8zm0}2!;0)*$bruloH|b0B-e{5*CGL5Jgw=ON7!$ zDG1MkwhBzWDl8gai#~=@39JxfJMf+;-Y+IO;*N*{E`AIziMJ0Qh|wMLb|{!H7SHjM zz7i|F&Gw{dKPAqEorTkW6iClYdL{r!)5l4$~GT1AI)%M^(F|~@8vB@1pAj)snuA{FM)`xwls23 z8^9#%^1y8Y6^+#@k-$`8>t$^44;v*fp7h2~6wq4HdXfWcTWfE$#MM|Q*^9)qk+xt~ zeOYN7n_-*sU8mG6+g4=kmd8@qv!`k?e|}WCwbZ(S6~@9omYpx`U7B&^V8RzY{nuE- z6i5_*j0qg*4)xMWK^jy#;ERvu!BgeACvKC|3>fQxyo*A~!Nd^(tn#MgsAc zMwZQUV|2`iDx+WR+KBwAXEjC~e1Wt4ynK6@9Z|x@?6@Ae$t#sm@3(O6u3m!91FHO4 zl>hnCyTMdTEonupEun_`b<9|k56nQLoD^8EYj_OS2OmUCG9W==h&VA}OTrPNI7vo@ zGQ0;ffXgB8h%X}LRXlQB)bxWB68bh&jHe7$G-=5@A`Vh=i*SneT-~SIkOGFYL;0Li z1=$A^ZXsf3r57OtUnj389*Sk1ZR0bj1laT3*C6xShi=M$`N7hwQVKn5=d(N44e*|`-A5&aDCEZgzh~)2O3qmBkB>h2P`Eb2wikC=?Dq^deI^v5*FJLISKmT1eY*11AJ1%IbmUr`1OPG2Nnc8Nnh0} z;8z{3gC!E6rx*vcLQe%)CCVUFGQpe_P7bXZTxQ&R#KTK3OLz=YaPT`w4DoXWbwEfx z>DkSH;I0A#E&O*^-xTueDfVQ)@v*dv0{&CM{YhY+(1q zykG5Qt$t})`Z7NEPHlFa*)$`0fz4uc*71yz^hFd3IEw1QV?BqV37_V#@hcB7tQ?r- zLn;eLCaYZ?n7ZZ4VKW=vqeU}^W1%3ZoK?#}vtt$u)@(b=%pDkbi)iJcq#KBwIL}kM z9dv_5MY_zCe0=*{2Ev!>($2O$q38$%geJ;nYDs@#V~dg5b2 z4AhC|0xSWFL3p6liwG}tU;|Pvy}sap1C%Z56F=1OTHpHHrrG2IZDQbWHu!S*7q6>5bAXzBcg@5i`TNz6=jK_?yBmgM*d_KU-) z0*c}8kdz4#zdg7rxHw8Wgq8rZOLCbi)eq^Bg7`u%tp%Tu`@=cZ9i|w6jv|o|RX7x0 z=o8#kW#e;9S);uA`;w`gU#_hU}2xw z?}F%=0F~qZKHoLbND2dH|Y)Vto6?ZCVUH_#R$(4q+>klGO!v|90>^@$GYx z3cZmT2w$#Q?M#BZz3D-N`pS$wFjcY*=12X1XvRu^T9)~*4q>dWH`ux1uHk}re&ff6 z2EBW3VtzhT`?p6thM|Xk<>5B|M3p6isBS`<%) zs3gS?!rc`QVMx?PaY!DHsD4-qVPdNF)?o`>1UlY2R3Uj`R8Ylj!1Lg=2tbLTh07(P z9i0=^Ywh_AW3lC0pHb-{)TtN62;^9j|KJF&RyV^qb-tojj>k~)(^4o z4g6^IY2;-C&{>dIO|e(_4g@88YzNc7~l98#j8q>OBhKe`h&D34ECMj_n! z8TYK04+7=3qXSD-&+m3B*7 z4B%MREd)07vgBlL4Ffd9YbPJ|AX1>JSWh0ZJdUN#DL!TE-k<&zEAWt?jRdz{qun>5 zD><&N^Q6@keY&`}vU!!^r*q<%!T*%}3^)}u3)%#OzJ=r;h(<4+YQ1P(O91tx6Zr*K2?R~_NuN>F)(q6>yd#V`PosqjsJ zM{D_i?lZ76_ye*|+X%$2T4Z3fl*kjdYP zwQXD5ti`Ri`^*Xpo4ode3+#oNSsc&jxs!Ph#DSox`94@?Ccf^R1EbhOtMf<2mPo7A z2#$ZLV%V>;vz8eK4Vl$GQh6f?ZV=mL&%f3f!hUP^wu?mywz-PAxLk$K<>~|kk}TUe zqe2k`1eqo!lnDB2tUAZ_L?4#JV3@go)HIcFZCWitXQ#>rKgrm)6w`?KMn8D#LUq}C zTS^Qk_wD-WCtEdTTQ{ppmlakjy{^8nu z-iZcuF)8b!l3tHGxDkR;jyn;>?~u+4nIL}6BzI8vS@2X_n;`$&Yf^t8ZbA z>0e=;-2LojtrKZ@$@xvLQ|cdsK*}KlPO0^D?P6B1bZj1HpAO2Ul7DwTxI9-z_prrp zpBufW@wF>PBB8V8S5al+m~c7`a|o(AjHx}&7WBX@8;1in*zH2u7aUm1gBR<4xGVtQ1Ow`QID!rJ1uO7m z7!vRmaKDJ4Bz~{x=@pPeIoN%M8|0zYLhym;PdH_`I>aPV%|xF^oD2dN%9(UrxIbuY z!&a!Wh?NxxdkB8`6bBBQ!22O&f{*ApX^RTgvxLqD41hu&nn46v0RH-=wvkrMt8bmV zwo$zuHoJ5UfJK*d6Z*3^x_BBJ`_Ld(69#$bD4dsz{;}3yn3?&BV}eGE|DKqDa*YyL zbsCPWTj%RkebV6Fs3&4k%J%YgTdLRfVU-(T|2z2Q58g0Sz@|Aay#d;jo%*lH=<(Wr z7N^a}o?m)3Xlf70A09+5AALwtW>K-pA}i!&tL!LV{8%o(JEZYyJ*qk)| z8KA<8|9n4VM>6|R0%Hef)$W(5TVTb9&hbq1-Ntp{625M5>DOSjPIl))&po*!5w69n zJQBTto>c~#Nd>HN#M$uY&s$0`@rg)Z@Jd!V!jcc{QTZy;7Rf>-&Tm>tN6%DlTGAI; z+Qtf~aC2-`))@#UdUgL!OyFb5ys4gtBbcPXIce(|yE509c)LyGN+6NrS6s>NS_96q z6dvA#5%LK(J3j1LR+8ELsA1@FBi0ygQB|`dWT7y9SagR6=zgF5?UV9JvB%BAYf4E)Z^HGty~7In9tyLVXK4_Tr*GWY*M-plS-{eefq;Uz~(+3D6NEU_)ny zt3J>yk~M%y^0#CK9)haCr?{_+Q%m9mal-Y3B|{KeoYY07Mg*b)ECYYTRZ#GtHU+E+ z528{sIdr5a^#U~l`lT`*z9_y1)A3C|ek5hS(Sj-jlTOxtxEU$=Nem$Bt+nqn=}ul7 z){9-zeHjKVCFgi3%2#%j?NaUMw)N#<2V^SLT>K$PdewV85_XI8uHkfQP>I>n-n61G z#740vBHCGHo!YindL`qda3VZ{5*S*Vv%3G?;{A4i&P6FdUg^>Uy^h`AXsudap0)Xk zNHoUXQ$~D=pG)_21#TD*vE&E6SajMLn)dh+JcU&@rm=&~vYlzegJw0WrmT48sWPz~ z(SG$82cV-gkxDSx@0rE9T_cu`e(4W0h7vs%Pqsj!Adc`cS^}G(m@TNX4Y{rq`zWj# z!4NmSgAm{_&}J?fd;INcW?xqAGHk?`{h-=Ik^uA<;5z70Gk-t0@wh2W3&MO570Sph zAN2hjKB{Zd^+1zu5(>s zfYPWhL1qb>s(t8w4(+IY$3DzV8{wFLO`u8I)UqGigtwWstJuhDm0jJ<+4)`lTY7y+ zFV{-w%&)o~zlgVM7$oX8R$dbdb2ZGIX>WSSId8L>66C*i>DD3M^Ht26z?u40=lH$7 zZEoF2_Ef=0-rfp*wG-H@c1BM3W3DZr*sJ&h%qv6Y z>MZKTDS~Jo+ZYizwHjeHTxPeq>x|3Qjlt1SdyTv_4YLQy{@E zh&#;!jnU#W?AKFl8~~9aAap{Z00A~efEvGa#9UEor<0)qI4~HIf#4T#X~|ntsZ=03 zWKMAV)MclDL0`tnh!Pt<^>ac4|+ z+_8g3l8w(B^WHi&2sLK@a_tJZ-x-4+f+e`2#vKcaa$rfIw{m+Lbyvfj=guECmkNg_ z%NBp8NjBqO!){V_cpHdPPyGJzDTCRlxx6-GZKw4s5+bM zv5LiHCn@hRS=7{BHIT&gH&fj*mzK$VT2oeZRvmTlW0B;40&S8XmQI{kS_ z3WzRdD-f5l6lx zFGY>15w-SaRNL2KWBCs7dKN3Uq9;r%%3K@}N`PflgqpFHVMG#5J>2~wJOGSDpazN~ zNTzZ9fyOWz0pCGjf#QSU{)qgWas(l!Cfb3*kjTrZU^5Z|fztpSq}me9h>Q-N0X_rK z2StV!ide7{zy;B10#-#!D2msC->8EWkFa82GmwgOGMRF}FrVbF+QYwiuG|vL+}H)hyLY5Jzz@IG1nN38CW z|0$Bv^TxIJ;_+6MNM1;^tJ zcQm{YyCBy=SDhjOa0H!zpS+3~c0dfMbF3&F&_n~i3(X0yf=(QbMTufRQh+*Ygq^^d zz#V|rS$8SeC4wiXk}%+-@J86Wg+~*UkRp?)_25abF|3*Z*{iDVc?!NYa8QF?DY7)i~q&hjktRY_$?nSs`5`*q~rSK2=;39eY4nn(wK8Wh>&H= zccOh2&mPmR8O8H$*=DDHh2tDfj<({P(B|U%Wz_i@+mGi$zwaQ!s1jS@T#>j1IMLCk zX6@&n;Qk1byTl)`N(R$&vuqWn@aCje?u`a~9T>Ir94wZlJ;g{whg67qBNajk6nGZf zVu$&t4LMS)HeO&T(73JY^a4NATMR_(Rgg!Le1mOcft6Nan}t=fJ#MR$G@z;HV%W}; zjh0X}Y+k$KZ7b=m(6U)Rk1;|veG0#e{;1o`_;ifvM4n4ur-%)q zoOSWm6e>r~@u-Z7D2v~-ZmKzY1PMjgksJC5_Z51dX8hd2k|V#;ePm}qpB+ak0;%nd zwGTblfIu+2dMl(IJ!pcvX{@mMrPVUG55U!I!f!608kB7mt8~v1Ehxbx)UilZ2Tnv}ArL9N5fDi~Ax2Q>DSTXVA$7Z|0~<&JF{&gS{YK@5@n?91!015 zM=RFvw`#(b`D0_$g1 zw+9lh26mJ%cs9cZPaD3IMRqik(%-_s8A<_j(`M_k&Va9R8#Gs$nO#$z3#H@xNQc;w zV;Y||QYN_RY^oOk0UPI1iCO+bpde;p#$v7z)-p=H7UDlHZgTwOSQ-_(kl7 zD&o`~^fwNJvHrP+4=4sK{%%nR%!^HQsMUJ-^;5P-x=6T$qMD>qfsdLJ)D9${PAGz! zS2zp!Xxx9?bJWt2T;Pt2wZFL8^ehM#8nh3IxI{7*s(=Xq9CY_7 zSk!Mc;syR6MkwmZ^jN$Dpb0pCN=$%E$c+#o1%U~Vb;*tZQ3POydB7z|Ildzj%`c6P zWp#PUWf+uS180~TvKTuqJv_`_?Fjt>+kv`mF<>OISf5u^>~T`NNSGfnHc zygAI1yRI&x*^ytyTNcu>O~+hWI43x;E|vJFS$3 za)yVEHcZ{OGuP~HnT|#@>;oBDcddGM5c=7MJ#uG0klcL$)-{WrZ?Z3lrE#h0xQk&3 z_&meItW-ZkQvtYhbw9&;fkM*m%WG+Hego)fz9IjGuqP92fy*$Pe@nw@Fv$a=p=ICyv% z@=WwJxD1l}gDPwR~*^>dIaCBl!Z-QO)1g zb=N#fkq-7;=2fJvU~v6wFStc^C9a#r#KUU>FHN&Nnk~b%3;V&pVQ=wv`2efkQOJgZsrH}_88r6N{xAEN4`i)V@z-{! z#;m2TJ&@<#8A^KdAG9^LHi2fz`86v|_Rz+CGpuw)OZ6!`csV;GWuap@hPp<*w55;n zQZj|$kt+V-w#Q5AtHYyCI-(yvdwv08?O;)iLL4h*riNFp-L}iYQV}+&-L}lQcMTSY z4y8u1MxAdr!$yX0=;LgxUt%+`iJY^|LGTl{-u zEePYtMbZ8t7lJglZal~^(R9TXNB02E0=a? zf;b;&Cp=Vp1Zu@p_+kfY&sNxG>FQtze!q6iQ39NIf&CSgmSNxQ_&w%+b|&Ks1%n~0~*~khMeVQ5O8M#wI&m+I0owYUHqzIZTrnPqp_MTH59<2bft=!%1sJ9)sG0%}m*?JFaO&x0XxHZ?+eA zgQ8Jd7U>DbOXGMNxGD&Z+P)QB=XSFfSBJ0P897a zx~%oEUqNYs5&^_3k(Xt6jtatoB4D3qKa z=7`!hj7{VvZ6ijy6--Kh{GFC1Dy=BG0!W% zj^{9%`!UUtJU$-FDM56k8N?D;8HCX_xvCQFxs0om2wW(R%cn3qr#cfQZyk~n6AKK4 zz#WS1Jih|7|H)AqtF?(WsUkL@775A>nmJp!9J|^MXeKjjkkCT9>PruNj`8QpA*9pN zUun%1Ea!?9_7|AzM}DLn0vB>72?>kVn8Bk7$hjBu7~?n!pBrGcU_;LuIrz=^-+;gC|0G>Pxi(DRPY(tOsHG< zXk8hhTKFch49%k-z+^L`kkF8V8Y*_2#4ahNLQPc`Vo{*S#xU6qMgn;?`XSdtS(3)d z2dXwEOzHl zGV`ZcqCU|$Xe-gaCZF=({KyfsuVyoTznScO*sxsbZ+)qN)twj%nWbT=_G=b_%%}vL ztw9&jo_i z)lA&S`4d{Y)?gcYAb$eU9{+jvNCc|{(T4KZdZDe6Gz=M{X-8JK;hS5Um?u4h{&QEU zWZce{uj5|r(Mf&6Q6OXC|Dw~SZ2>A;HE|AFplSCCB?0EVdUk43|GHc4a zRirN$8(2f523lh%qK%=emvkeY{JE@Z`%z5CA*20;DPp3$Tr6p%M5LG(W!D@Or~vH2f62Gh`T0kKVw{LsDP0Jp{gX6Ghj*(oWoIzs}3)M2P$C85T2l!Tai%HkBG{r zFaZP?KprZ?&=HIN8D%}lUQkIvD^I)^u&j}$GmjLE;9Q#f3(deSz|0sr13d#qM=wt( zwM9&B+bF)lEP<2%W=rbjTlfh_3C6LV$k2Icu<_0efBUJzn^;MB!kfU3gu**nSp^}& z>V^LMVb~E{mM>J8oBCwHv0WwfA*Lms&P#=zn2|T^RjPS>S}(pQTYMJE^X1-H;16%H zZi%t7EC*t9(w(gKkNfc)9nv~hJC}t1^BLna|EvzBkTai~IfJf2-_0h9r`X!W^3t_w z9_+f#6WWtmw z(1{~yKvclWEJ|1C!V7yLC`q*n%1y*dbm0>O%7uF;Y#`2^J`lhb)E#`-LU0x-52`JB z9C|%Hrc?-)M(CFERy+XzBZq<%hqzaZ8|9ED(c`I(B76>oFKYP0w5Uc?fhr;WjNq_d z%F+qCQi+E$A`b~bdWjNK+d5+#lE*n)E?r>#m~*%Gc>7stVzfNzcpaNp`Xb@6Q~clz zKbF#g6HBf4<8bEr7R*!>@jD!tU^e{t@>Uwhw{mw!+na6-K(*g}yL5XW5rPe$?;CM5 zj=?84{I#)B-jBJgwv$jOhOSV&Jo~X)O=9W^C;?0=xE6JX!(K@pRGs2BR(R>4iyHj- za13j%3_EU8;UAly5f9f~QF1cccVA6vzUy}vbcT6Y|12Qsw0z@eqc0gnH_q!l=QQ@i zhP>(|Gga67@Ukl-QA=k1p)V;MQ`J%J?fDm?&1kxuHM~SXO!XL<8T_M$GiLU>EovZh z>aVv=gO{F}zU9sSXgT}C=`2yX(3va$_7V99zK>9CO+%rq858)+(W^h%aHn8(y#pKz z@K`6Y(pWQrM={q7WN1om74&fNKc9j*V|XOOk&faDNJ#@&fUycVU{w{h56mhH*&tW} z+>#iGCO!QoABLVWXpOu?%qj!9P<#OZ>GGi)SVzzFtav>bC(Slf@&fq#yLSS#TSb!W z50e5aA!qS8r~vh244ZJQn7Ed%Ly;!-az5o}W~}>REfQZ2EDK0`QabTw$?jhhHkzhh z%eDmKYGf!I%1O!TrueRBirQ?{2Y{Ceo!0|RW-59`x|r(j2!-4Fn%3N^+s&H>t4=cI z1!KCdNttKaXUwnrUTlk7+7F~qBvT_Nv|LKBZf^*S>vV@fbMEn%I{B@8DGG% zwJ!yF`Mp}Fw$*iHDS(BP2@TA_5L0fmn^1@exxs+{M#C4qc$47^c&^XrO~h?~G2?qU zQPT0AxfvrnQs8giKtD!;zlmW^rx$%*=pm9@!D)**%{q3eAZ(zPD;x|Q$ffBi1Ou7| zAn$>>dR+Ht7`ZfB|G=RRrNx&DuqdF)D8NzL4Mqng!u2GWUIb4^u!MXB84~?Cx&oAv zQ-h}tZYWX#KMSWtjtm`V>cvo97IsC=MbQI9&P=foFbYnnzfy#-#)?SX9Rm<#J8kT; z0pQ!sM47CAsm03TSu&lGwvDWpWxA;q|M z%nl@i!9Rbz*PJeH$d))jP9)W)sL`ibd0{Hlb5~QB#X%UD#WvO`amX_0^OEjiJ*U}- z2y{y9n5T#X6F}NaB(eIrKxS~DLi`MBzfokO`yYeM5Y=A$MeP%y6>G&X_(M?&I0g$5 z2#>p74+odDC|JQ)8={D~KDik*aQ}5{* zg}~Bvkm^k}(Actv=l$(rpiFa|)xHBqJqb|hs|>{lf_j!;yqg`v*FsMR`j0@s2pgYY z&6M{s#D1Om_B0hy4>^;m=&Qy{Cs-kIlcojMFTuK{$!eyY;r_gu{ZvHDja9vpg>dvv z(L4dlL2`zTEXzbg{Xt_=(Kwf`+K2Wmt0Ht6;Y8GjodC(|KslbYfiWKIa zN)V+{)n%s({TMXcfj*GseKMGOnIC7lUQXhf#_|TaamJ5X=-|a3Xgy0gCW}un1uX zgg}HV0oD~ihVLS<5wgN72?jdqdazESnp~>R)$IvR2LMKe#UpNka6(Uo56KrH5yK}? zy1^eX|M~|WcuoYl1x_PQgGc=(yttT(ClG{^JWyosawN*NS71FT>OT0tjX4RLJx1+0 z>24jFlBNXphC~l(8xDP%8OFZ09>&*T>f|xX%QHNz zI2Icx9-^hDFxUgq03~g!DwgwQj9hgrPa0F140tN}4lsk;;>kz=HKo9@t=BRK0*}w| zVE}EWXLLJI;h9(0%E{<9_HB*5yh{b$l^896&WTp;m<6K78!FaXq3YMIMzv}%_H?VM zD26qSf9q5xuoik=@U8^=BSdC1({rnNWldP!^a|a z!RH{c1`H-&ML!I?#-lK-w`g&c$-_4-_9ww@p{xy#6}Va8a2Ogr71~O826!!jsHx*2 zydc?~N*EvQ1PX$vL*qicKg_hNKnfv*%;2J=e`@moJe6cc`Q3}(K-C@n$K^LwpWp%f z*6r7@=qh>EIE*QG{HOc=t|{>LOE~Nsf7TRq-n2`aGG=N}`diqu1$y6;XJ$|X!0~<} z#B!fh{Wt~sFBDY+cBK`J201nEPh<+bY8Y03P&%-m8T0I{EVs5QjQ4sosYtfu@ZfR@ zlSjkIyT(oKi%mW^-Z7aEpvPFWFTcgOP-1r6Mz16K&Ar}q$^(tc&-B}tdXW4!F*D;2 zp^m*WuLtf#tH7`ZQ-zX z=HStRR%tbs?!^gLR|HSK19&Kk8Ho<)M2|L1qq27g)QJAD;m>}DjmRAkO@+J)jj*6G z2_6HeOo)5YdMg1ZR7Dm_Jq2XX_z1Z*kzr}(5^=8pLqIn`gt{oDXPEk`Ch9P9yK1vaDkC;^-ZG@+&hlR>4Y9vm^viR3>o6ATv| zr#|0D#^f=VN1tJ}xxtl@h_#(Z&|(4+hD}}k8_G`^zlXoWSmo!8|Bp6&C%x<9`}isb zp6Y)r-OElKF}wA|7YfMu?GLKqN6_lw&Um>(KMW<|t9au{Pq`)*j!KW}t}{8y7v7y> z=Nv4Li`Qy#yE64TNETc*n{i55g4+)jRSkJkDea z?#D_(+N_W=qFu6n0(?f@7*%yBKchqmIjNu$&w${R9DwUumZzb@Q1Hq-GcUzJu+yhn zZ?;&|y5*P{R09hbGL)h%%>&*;mZ2Z>$2?2V9?6~<_5$rElEbRMkO++3d<*6WB7W8$ zW!p-sRh=%SjErH8g7A2?0;4x(8g5+emz41I8Q4ha#{ zVp=jWg5)9qQzXNu^fe4)lj4xTR0IJj9@7m$Q4$WZPwIp@Y?In}?d@#^=>UqpPaD!1 z1h(2P>HCqOY6WE||1|dR_yjLJyOOa@ZALQw+M%9nW+%Z@1=kj+IIT%vsI8ZWSw~l> z@eE^^uP}F9T`Zh(cyrA2D`2>-!te43caGHF1DbyVzTPdDu;&mIT4U7?DHgn4SHlSp z>-LVTV^tznXsEITp*KGlpdX4$267y{bipFsH;;V5a9%q9M$ zqF|f~gbB}@@u0qfYSxs6;yi|0C)B951z6-Ce5j{c^&8O* zO3*;}4L?uV`eBX0{9*7fico7;H6WQ6R{92 zbz#_Wdw^$<{XwFDX>w5@h4Bh&Vg$PS2WS?Pt^?Jtk#+V7Z(C0%`zk~+5?xt}m~=xf zQ31r@7z`iJy*PVthml;Yri6}Z=`vv*j;Q?kfOGorR6XL;x(vOFGSNQ?6 zW-j*da996fr4hkab?5c~;T>a}I^k%2?{=uQsfhI23l|9d%(ipd#*7E4UFgsYWq5rJeWgFA+jy1qMmz$YwkU@xTMBBLb zf?rNes?0m^i2Y;i2bq?%W$R11rh`aVlE$Qoacq_Z4G@>3QZ`EiDSeB!%yf3snAAKp zFLWt#!P8>z#%bBw?K>kwhlDNx0r|I#;J;Xu=1%+_5hP^lI3gr^y338^f{Cu zE4SnKzk3&p*H%HG2pJ#aA0L@#nMX6V-!!c|MmeH-D0#x3KAmm4p)X}x+<2G86G-BK z-lRJQV=fjYgg?zj!6C1VqdY&)D%H%-9hP~&aKuTs>BHw)X6bsAyvzF6*yoY7 z$nhi(wQ{hlG0DuP)g-7Z1-8eYWyqP8oSn=r$SCp>v*z3a-w9H&iTbTxn}XDUjdgRL z^ON_Ih2Ak{TGDkqSuv88V}&bw3M`J)*Nlf%BpZ$i{kO@%7B!arYDXFqc?`}0zL$LrH(n_Tw!?BPr?Fl{IF27x@3QgZdx>YV67B{!F{F;C!{h# ztXVt@d?DT#(c@5T1E`=ogz7B4&w6qq>TfV^%x=_SDZLnM65iKBb&aTUcz4u@#8=?K z38dmS;0Ai|zdqRAc^KkD=h;E9JXq;C{6yP>_m@mdKFav&+8WljRb~s)A7@x$?G6ZS zQ2jujJK*WPhm_3B@CSi-gsH zG_=CgIgX`$EY9Md`6!dIp)AF~#^#2v&ot%eI5Hay%V7GCoEv2pWL6lrA zsR2r%3JV$GN|UQ-7M$A!_b+bjs=z^(OW1<)QUL?t{KCw5dasploeYIbiiSpc#K*Co z4mF~tqPIF{)o#6pHT&4;RxP;`B3kz@|K^f~p(HN&Z2=WL7vfodPYxijwa_&nt?6a( zv9w|SvzP?_R>KEUqXcIR{3v3&NN)h`29TG~SAZ2!j4L{@RFDhM85EfeoKv+Kli*}c zR0Tn+63j`#l@kyc;TvB>KOE0mM+JmW72z?-byBOk?mh`f;&bX&;RWhDQ-EE`WN}-l zr-bJsN)5dtehOC&ALH_n1xMjKiIY`trqa7XO&BjI>P44kMv$l>CBF0^oD~%WX{tFU zZYWY~YInhDQysY53CpeQ;WJhNK>+$5d?0@q?IjM?bJ*`x3Ho#_wWM*M zj3PL9w~w(cT~J#w*sHC`siA78W52e_5{Jj1zbB-z6ZWd;hPdwTv|LBux-@znrW7Ma zNLKCnop_kZ=h4T-PuURAF0*Yr0D!QnJ2?ur-fUrC{18*p8LNE|d5`+Y14(NV5EHlsZV zT0)$cmKUX5<7SI%p)y2BZSW;?Hx0Y&SUl?ul~4dJt5>ykGq(Nn0lwl}{(Nbw`5BD( z&CXalDoj}HU;~pIEVxotXop7y_~h&8lNhKAw9j*`ji(lf#KMfMMm3{XO7LU>v-M;IIE?}82}k~8>c z@f?5$^~w_v48G>UaLGl(Vxd*!bV^11*^&W_iP|NEqks zMiB%-C2xgOGr-wWeUHi+>$J~**|q(FQTOStb!FXg;xBI1V>uR$aCOjhs8J3C+H|xx z0J_BKB0k%XKKK2{tha`jwpq6SZU#N+8^(C>MMJvQ$Q-ZTy55++=Z?333L8{n)80b^ zTjE*AykXruf<2%8Z6cFA{OMEnTp9BEPnfoowOt3le`-8} zE{E9wd?;!vXfh(oWn_lX!tlFbD11icT>L!RjWNA5!&-zO=T z2z=t{AQ=t)LuCM+(BHHDKy%-$QYaeF9O>?FaH&19!ffo0Pf9Q(&H9?A#juB-Kf?I;I2TFz& zStRUX(KmL03&x%v@};DFNb?KAUO0A%u!lyK|04H0+PZ7nZup0w!?chd`1=P2uTK`k z@yPmRej}Xj@GZn@9oS{>PfE{fwcA=o#m7x?=@YrEj*KIUc>>zq^*O{j>S-3BburyA z4~FNxLaC-+mBH0SsgBy22odgr&vHD;7Q4lp^3M z5Y*_dBVysYhY<+GQ{s+-nQ@8LBNS5Tfct%jRX|%7z2bl^s(5tG1w+`6EgRw$yu1%( z%Xf0iRrYx8>*M|=b*(S?{p*-CNLAL_0^1UbKmL3;W|)3m&ib2bpTjl@+zT4<`v(nG z_lv)tcpwh#pbLgH!dTz``!BT)e}D5Ig*7}TgKpN4h;q4uP)bXXi>F$Y6q5oy5Llp1DJ^Hv5BW zci!ziRr}g}0*j>YkSdP`T8^s&N5wT%}(&ut;>qtBD20s+_S&S0m){1XX04HBc3>x@kzzLug+&b)QhsPD$(CIw!3^-lHcQI=y zkN{2&Ac0B~!f)VbhP<5*6KY~;b zp^bEm3~prgqrM1`w5Nw!{LB)WjHsJZq{k5B}Qj~4CEc~oLcutc}DzAHA@7eFn-ZOj8%$}J&-y_XPBWX(>TVrb^jV;-dErV=iBV*YZV~oHiHj%-A z4ThNDfK$xJ0TM_cfs`~PC5fBRl%{EtHfd=tNt4_(hu%X^b9&My>F0A!+UA^-wn;x8 zCil1A5v0A(|F) zR_qGQ@(NK1<_=4(QV>@HYblJ&ycB@|_@pQ?R}$mIOG8!r*p#b2(D}RatUVChMBQvX zJD12qzHFM)$EMWX_sy!KowM`mOYjYLdY}Yu%d*TnM`L{iSkDf|USl14w0W^?=i3jN z#RNmKD69Fu*0ADCvVnnr*nLM-<%XFzs-k&9BBkCNSG05ivs>-pDWFRAI6&f5f0QIU z1IwwkHa<6d^&&k8bpQ-l3ma zJwfLpB+;NFc}}3R?s3-S9b_S|(X{_J2Cl(~sH@F;%y1nJTrB zF_4VSV4t(hIQ?}vcY-}#zUFDTQxxHlCk6&w5CiSYj4}Zy$q)paPvLasBN)e{Sjaub!}ho z>pSet#Y*p!{O#?ld$Z3X$A-Ip6BITCYqwV}%PqE-)+gZPe=wD}p{{ywBXT$PNABq@ z__1G748BQxVS|13f3hi6bB9?bs_s}@?d$wmO*kvbid$~FrmCDVW*gPkQ`z*yR`}pl zbA3uNf$cgKH~F5#VE_4ZOhHk#j}8ni9{#)c&s%q&d)1xff+rNV$F`2;eS{%1%4AMe z1z8lyP`83Y-?8R3B!c1Ynr_ZW9!gm^&=ni48*~K95MvIwjC^$DEMTvWxj9rKD@i^z znv8$XW4f&rIUIP0q?oXi`onroKsVM7$>;$dY7}{uk0f;!AUq~m=y0Ali*WK1c~juy zXliJWGf!ZtJpuY45j}@ojxN?ke$s+v3dK8z*4g#!XlI~2wuA0Uj+7NTG0pc&OYf!0 z!_5?1;gZAmDa>gmV}Gml4<-Y93--r5|3wX#j9VyYjm=$WSr=Wkwe$Cg)GKrH*$Fdm zjn`xWn&g3~pki)+m77O4S zH$4@J4#i(pX1x08%Q*Xw4^O&|ABqIk7B0h9kMF>I*Do={p3#7QzWm+!r{jGbDlUHs zBDC3)d1K?cbn%F5smWJ=)tF9Zr|x*yul;Q#R<}dh-(3lEx3C_Iq_Dh~Zn1z}S-bj{ z7N3LS!p4Qg;pG0`P@ntg(T4iMAM%m;LYfZMVqy0~1DBuA`$;T(<7dBN{;8Q+b0=}# zFTt<=b0cCjjJFH#QFa=xZaxgP>677L{<~#`6a1cNyGa#vjCF_XIs~2=*W4xD(qj?l z^Y6cF?*T5>dtcBx&>dQI-da4!j>JJ?{h5%MbF6vyb=+W3FHYpPc=&OBqfka}+x;L@ z1~;BIU^l*bX+{sc^>#jfZ%8~1pK|pI?HO}|y@zt=fA`G)=bqNjH#WZYqW}285{B8Y zud44Qe>@#d-Dg(6Ao*DG_y7O=^_DT!G~8r-Y_efR64Mh2V?LUQ_-@>ZFykf_d%7J< zMZ5xg1jEXVr7Y7juTl{<(^M-)C6=?`^({~zE4(#hlGM|W9%ORsT;fyqTBGGJW z#OwTX*!H9uCuUWOMq{_}t0_P1Mxq&L_Lc*Vo7f%mSSJ_CG1NZfWMgro5GyCK^F`mg z#)yT>h0?lWaeXD>GSAOSH?vXp50^48gsc7ea?EzpBRM+~<{5gIBITTs9epM@;zg5b zGwP)3L=;Y6*)SY-!IanTin!r)ZydFX#4E9{n^#(3cC-ihtQ_Rl)(_&jq;m3U@43)3 ziGmw4tx_&+{HnS8o~@JDZmaXDulz+eq8heSj)V;rJ;{nkx}G9N$TZZAu~f`rIc>r_ zZ7?CuFg%~d_LR5{_84Yz{KklM4p7X_S9?c>Xv1={*4+uOwtE|^+S%6_r4vdSH{Wp3 zPT0BnwhC3s5|ok5#S*Z=BAnlEGnY4x z#m|tP9fx1R1Oc(~QgPADaotThF*GPt<{ZH@(|T);j$tn|1^Mg#}~$yvt1Vx2W7 z3u7ys(g`uq9PYaOKk@w_!|$krWBUhqz4!;-!P`t>wo9}{8mHa5;4eDbd&_6T-q2vV zI1=uR-;p>>n|`>4(mkaOzqkROy=iK^JV=8lUKEeFsuxdWPtZwZ|o^Bq?pSKU^-P5;r{tXy$W9PpXS!+7k zmv)oMpB!v%+_LD~{_E2;T;wYk-Qk(CD`PG(_Kkx!Zk?%7&Z9Eu%FXu4S~@8Q*=^SB zj2tkPW!&^qJn=F+9@4uWD60=`uD%%V%~Tdi@!}HO>igX;6weda*2qx^WO z!Jq=CZYS-N423BG_Nh!i>n#HRZz>GJV`#d!z3IKks?Q5phs0FhlL z4(JUe)K2jDZHXpVwInz_bH&#h{&!iFECsmp8G8pS+*H_t_y9!=ERYOtCO(tDq0~}k~ z^^oPZmVR;~k=^_Hnb~|JYV2hAI{PQyU$3_o+Um+p{uim$45<1+2IHlBQVgR@#G$XA zU`rf5gQic*<*Vin+QsWthWNpVW2jxmKSpB(%WXE(fBRh+cYEZ5djMl7k!&4Kfg~;Hh8x8AH zmhHM{(I_KQn0~f!VC1N?j;hWMXt4n9*&`-nTc((KM3?eHtUp9K^oONvGHh{wJ5fAA zDzcVxc6!)%)sWGbTN+HoCz=5{iJI!m#ZuRALDQ$1x4{z`H{2h)IoV2{Wp!@JN{2d6 zFgl5i8^+tph~O*t{^GFrWBGoNNqH6?UbLSQC)H5)_%U_GSd)jB{abJ}KEe-FD6 zTipmNsiv;JXq6F$k^WiRNkKG7izJ*CP2^vJx#0*+Feza?)V~{QK=fAMdPvo)yPe_W zrDm;h`7oT7jPjzRnFDv!!MKj)pW}<^*{p(#jeFl=jD-6uJ3F5|aOEGduV!P|J;eZ% z9gqg4c33+N)6bC|JB@a}ZraaK+hcm4*E>*x3S`eW`lGioKB?$UzmkhORQ5uTCB2og zBF8p}$1VluFD8>;S6|w3)-Fd&el*gbEj#%_=Fyo7%%Zj(c*Xm}_f7rqB-1JEO7G5O zEx)PMKi;(S^r^9CfQqD5q8boMB)z2sWSX7>>s^W#>5y(5a~O!Es8*1_gy;zdyTY$s zD>MJQgel>(h~KbC{@?n_x+hvnyvW0s3TmWYLmuM@1Yd$Y@G#_$S{`kYMO#}OE+D}s zX{r2{uU#7hja~})g*B)$TDL1znV6_85igTk?U=#YR=bC)le1-Y!$c|j3S&fDx#ElY z=l}9^jr7LO>)(5(nxEV&EcP9R{yqj6Huv9pSDY{ZNyqq|dGv8&&>o6DX9pW@Rq-PQ zd+@Ur%RYBlJvCKHPCbS4nqjvZ^lemj(R5O#V*M>-CVPeoeO^5^6A95cSDo8kcHViJ z`r%nd+B%hE$-eYUv`J+K?BiCsY@dw{a=Hcw)?SAXpw|jy;pk15W==SDWu&d;PrB;N z?^j}j#Fy&AbTKCRdy}*A8G0>ExVrCKbBXW0m91miQBj?$$&t_^0BaH3%vV98&C!sg zY%EK~CJPN$i$?kMKqx@1OL{xnu$c7=3`guPJ!V>=>GwCH4>_z|*eF#1& z;==n&iHA!F9w6ue<#s+on^g+7a8LXH)8t(Kq?<|iaN@t@pHi2S>fydQ-(l#~0{*%> z-5X~c0z>7hPb+`2kPJ^ytq#r@_wJmX>-^=rHXLHj=-6I++Yl7Upx_T@C|1AF^v=wk z8MFMEB~08ER;Akbc&Xw3OXQG09r(r%T-G#f_~14M5k%DjJ=5;7QmPa#%!hN;uhJ;O zJPjD1%%EYMY8>^i=7%Fux1y|+rTTU`kzzg+$xkmXnkm)Tncfs1x^I^{fA(sM8LD;` zq{-g@X=%N6k2^h_4Oo1AxzzSR^=y}h-*TCboY+l!*rOQ2wtbVC@^Pkgd`??8z9tcY zFbj}IU9YwiRv}-dcsHJcevlUV@iJXT)A3%wSB&qJKubJ5X+5B;nPu*S%}=ep7v_b7)p=N~il~=@l{U%(38U zwYk=lo}y-KTjhCU`^p?%jwQVvf9t!`Blbe`n5&BGli5M)Zv$*JJOlkNd#G;bnK|l) ze$jgkI<+sx5%)#)`*VW&g?}=pGQ{}=>AVaW_ zgXSr-AN8~!jVD-L-L->XGG8-$nalI1L}S>W02%23r%GFw&N?ZTweq>vY)DewpJ(Xo zVOq_`$xySj97Xs!C#iiQqs#hl~(QJA)`d|@aE;Qw=cSh zf9m}8Z(oz!+@#7_ACon$$w?!gS>FslQleMbh}fxI^us%8bS$!AC6vutM;#S+h7v|A zlOow3%APllc^T5MLMHpM(8f|cVkjP-Gyjiq)^!ZHq}GF@dwKv^?JT1|*rgf`ZBSu@ zi12~1;TIzof5G8IhMm^**1PfBNMJ*$oaKaUhNC7h=^HQWzd0GL*&DVsHWe-BOYLiB z$B%5Xqlvl4Pi37X?LUP?c=M+I0`0w_(NH)NibPk!A-@==9B92Wo3g_ExzYEcXrY=D z3q?;d$UT(s$q{3>03Pr#M!NXiaRyI?OK|c{?88^!r=~!CcF3Gargci3+dq+!6HLx0 zR$aoNh$)g51RYc#6Cq4d++>$~^=8c+znX8c#h&-bydP{o=7wW0R)w$}!vMUmOY?%( zi+7foTOi+BK&PAb&gcy96Xl|7hzsK%x-HND!RX@)0y93p0*N70uY464?vBb}!mH<7 zQ_JQ>oZssKq<3cTK#Jg>)^a(|3ysZh{4P_5ooTyq-am>-fHIP=S=mOm5A?15-ak6I znCUV7#m=3-9vrkcTxp+%`7GnWIE8Aw#I&8hRPx}$Dg47cp=i?hqtj-5vVrgjDSfk9 z@Tcr*rSlI@owS?30n&SJUOJk0x%_QIX!QIG&MI43TsjNExBTR}E9MElzqIf$?%_Z|M48WQLSQ{JOzH zWHBU-SQv3UH_@*+XfB|WJS}2`NJ}WDTAOSVCA_jg?O1UEg$WR(wLQayu=$~{C&%8M z1mHXy-u!&J?~9WTQ~VgS{`iKSM_VV^pJ^O=+CQ>Ux}!d_x!S*>b3K&dT=o8$fOAe0z0D%R6KUj_VN{` zT`Vp!)5LBsZtZz!GvU>M6;Ioznm@K1u-05|7PV+JReACen^E)?^Ns(bDD%Wu?e*8p z-wgc=XY7;6M#n0Yq;Y~C|IP(B5GPX8}CvS}|RIScls2FuhlZleHq@P(urs@TD6F0;1B z5R~mbx7`el*zWXbGFMEVoB{rKnx)FlV&8i=s2@DWM1bnBO4wjl$h8!*=*rq(}5B2PP1mG&Sm;v~J!a zrV-1i&llp7fNV7wCOXq0n2xvkToJ+cw8`yeM3(d3OX&i!D1q{sMnyqDSEWi`C7!a(2(Wwtz;j z9SrUin=ZPSV&-p&&F3FS^YF!J1cDOxOktfCsMR976x@(@*OF*gizyMit9>}=7(enp zDVA!VD-Ks11yV0*U_*7WMPd^qx?i0JyQ)>-bLoyHcO-oZrOvk{K)&N*D0cP_y@K2%xJ#bkTUc!7UKLmv?5c9xq6i4{ zeTo*P=16fVIC8W4^M``^Qa&~I>FXF9;w_7Ky;HG4zj*^&@*IDr^PNToQ1A4|zGC!a zm8|j^0(20HZoVZLMVK1v^T^D4OJ0%)Yg9*E@v70s!3wC|tzT;qVfr)ORx%90KsTKQ zzSOTr!em@J_LB4r-y!NC=3np)!J8zd;slF@7aJk17r13kGDbqXWbKf#_d1UYJ?OdD zt|Ne&?Nm60J}7m4w;o%+P=8hZ_3dRc-^$TK-S+=GyXF}vlj3ao%z{P>Th&Hi0(&m_ z(O2M^Cg_nLSaX@4i!S0oAh=r0+Z8^0d)^{5lDO$b7kBVH+RG+Dw{E?A$liAKS?^}Cc!QH$~tvI$h>jPui_S*fa zlB$S1ZnD5@Ooig9ci8T))xmDDyRMPiS7B=I#osVGL#j@Kwd{oZP1jYKb^T7olA+sA%vW3WYJF?*nYG?)kHm7O<5l*> zN8huR$^^o8tJR6FSo|*KNeSc zPp;&LKg-Uos{*sa`YhG5Y75MKb=4hxf>I@}E7*l2|COanelqj-@j~W}KZ$?LS~ok! zkzK)!_{>1D^ZmOZEifDm)FEGMjjFyoxwL178}_=(m5aA!MK$9qoT~e|w?*r9?>Om7 znZ^#t4v-BZJ?tiD>4DwL8oj5Bz19HxEWHN@K=5t*FfZy&C0$_$f$3U7K7X?O%vxLh z^X=gwr*KU=G4fsf8*4JingFNf&4SF$0qV^+wftnZmVb7i2;D+Xp4hgSkHVEOI! zCcq=!aQgs-5i=Z3Z@QePqrD!#N;VikVF#o^jr^$JXgz<0G1)t;+)-ka@@iZx9MVVE zd+2c?{<(qVw5%FQ306b^0Fe!tc;czTa-Ihe*3ZqP zM5uBY5vrD*h_dU-IDZ^7301JeQnQf;S&lrHw6dYPCE*F4bTTF7VjxXD8e}4LEsdnM z_pC~cly2*#12h(Xny(|7V7Okg^L;h%s>Ef*(!5ZH`ftj`pI1AprIZwDqeJ_4SN9qR z%*oCl%z8)eziz#K^*T_3ur=!s4mzx#CzWWbJmr+b5VX|HUi{nY*H{hQVC?2(dMlRq>6>({`4wutSSu6!-GozzBCNOQ)>+0^4d9n&^xM5ENR zpfZOU68LwC|rtRzwRSS7(z);0ogNjd^=a#{Ni39vhRAM~t zFg4{1?d<<-CcoB$vvUe+vSnLSQT53_XOQM_W#67qH}p2#y6iPerks%j1>-V%&T2x~ zKTGqH!xUB97?1T?)!lBk*PUs4*+f2i-0{kp=3$(q3UZsy{o*?BTNkj*DxTZb+bD1A zt!2mJgzd9IV|v21s5#&X>H@+arlmE!;^Nx-R-zFA)Q z!yt23-95860kx2t#+<~c5uIE73bR#h_~8BYT0Xhv^WeyOa0Xc|d8`z8y5GA*J0d5c z?B+9aYB8AIioXa#*dp3gYjgtlC=HE24|w`G*n;ehi)}_$Vs??KPBp{;Bf&TAv-~JW zRG(z>8kyk24a7Jga=9{xSpW{)D@`~&AlPlQnr<#hE^+hkSeCJ+l)~Zo$xp~$h_P6% zQ1{%}fhppYPQe`|LRVXQTXZO@{yja3r_^%&LgS+GTeOsa>-p-JllEm>mB4*|J4u#u z)7qvgKPJ3>dtwXop5oR;#zVF1SKrVZ^^SkBzp?(Z&Fdd*t7d7}!OK(j(E;@%+FgU} zdPp4A>Qjjnc>)2b^9RgkHMS??YH8EVBXsF(TTj&!AkGP996Wd;O!^BGz%!4r7`-`r zHWK|rpZd``#XR`xo?>ZmXx)*k2!X{~J39cbEpSZbu7rnLxm5UCG|iiOw>mWFL~^zK z`ktGarO=38kzd+jde?l0E!|?PzXIn|__s9>Ua{3!U&$d!^~74s8AFng=7obJz-~rP zU07mQX;KwCjuP-64y(X;0t}FF(pe!VUpM*5>%@j@HxA2M1IPTgl?#i1#)T8~q5*OV zPN;|+CBVyL7Kn?)ql=dwqm^7L2&*cgwPh^yq`hx|Q4kLvR<+XYS2SxX-20@NY{!1r zZFEQg)gK*k2I{}?J8B6MgS|QO**7|qo^mXEeL7uRXj-S;-ohm|xgDzGv4f4&TOda^ zf7?Pw|g^8Gl6GOctsQ97`V>s0Y}%L9DEgWFd2*9xqL zW>7`q!pX{W9+?2@#E z@yeixhsOp8)*JInjeGZDcb?jPxtn3$MB(b>@Vx6kDOxP<`zTD3fHpFyo8{P`oZUC^ z`U;yF-Y3(r#VW6!LcF5I0m?EsP9mE=auoedYkW|ah<54iREsouS_@CNmTLY2CyaB} zeNvlpomF-ND~vHHfgD~g45HPg+hg027R=1-=Emk?+h8tH#!Z; zfRXGUZEs7|HAh#ey9VN=&NJ0Q+^SBGk?NFZtc{^oHEFyry0(sDSbj8`>*aN3|6t? z>6ZN?IoevG%Q9xQN$fLFO>7K^N8U zdel`MQQjguxe&d;LYz=SEXhl|$5qlA+^)csI0Zqoy9$JHVN?=^*?-ox_x;{yf~)R}zZY=%iu#3=ag7mOv^)2HXS8DP z+o0C1l>uiv{LFx1VdsZR#lU*Ed?PTJ;J~;*Id*?vub+LpITm<X%-kD;A=jak*2e+NYkWUVpH4?_KnVXV)hyPWkM@Itv6rJ(22r zRc(LI(n69Ik43eoufJ&LBKum01zE7|Ib3?NM@;f@NJ@6qYp=y7vu*S^U1OV$-+4gJ zsgK?LZTNEmGmAcfq;#6P>Mcb&e23!yB$9peaEUcWgkpRxygtWr^+&=R;@oami~j2M z9uRg ztG7p!6&fMP6YEc?U;KvAJKY)lrJk#HRC+m?Qmf&@>glE(?Aff&i~<6zJ5CAo>6+Cu zx=4FWB^g$4sL0@N^_aIkYv26a2kkM}{;=mc=|nix`TE)Z@u~yiOXZ@B$FuA0vz-Mt z1$wQ(P2U9d-3cEL$EqF`v}#3p>0&DMj=n`Bt%jnf!`mMW3O{%+N71ex%Fe3hOTnsZ zJ!+AmGKIg|kBt@F|7*m<29c8NOuB!~_r%WT2|FSng{v4pGUPi$hD_KY*Lc&+@x}Ay z%aPF7y8!u|Jb?`~*HIg24L3;mx(Xq%k4uL=0_Ao4QY2?-lhR15izZ6KcO)j(8^L!S+9@Lslwv+gX^NEqxM_;%b`VdPAmpK(;3izEq!yMM*SYBm) zDj|mP809Q>n$0Ycvw*8tR#ZbcQK35SqT9?MI+73aOV|fhqQCWb1j%mPbL$aoSun^NL-CAzW(9U<#cE{-`R2I z+)SJO>uQGinP}A=Dd>TP`>>Hapv>ke1_1uX^S9Z#d!o#oT4X1*^}IUh`r||P3?6;1 zslv$ybAe53w&0pH+OMUSYG)prgaL>bHBAPC=PFOe4UA)_>{|#GE#f+;8Tn0qy6enAH$#r2U0z`msth~ z%muEz*RsrEiq02U`Gg(`_vS0QS9bfF|AGc}BUWNoF{QaxQ%YFe23r!X;a}t`55xut z#E8lW#4Dl_vLRJnrtpZBmlx@}j((R8!X*&X-hhKH>5w)ZcmuJ^SQshSqh3Ps;qDT` zqahJ))zXtu8iuY1Ffm`(bwzD36|y539TdgE>|mU_h*o!HJi2B4k7yI)8BzXD3O|CupE(=;|EbB6^aH;c(q#=ntRrkWmNaShC^8LxmdGqw{ zYwgg+zF6&VN>MhOU6Jty(zVR|Zu|DMN>-}r%aR4wIkq?6GHLYu@%leiih=G zx66&&)&De9P3%{}S=KwQ?vPC+WGN4_Bm#1DFW9Zkw1JscRn(SRyU8T{WOQjA#Vg z6ca879$;EHaCHhZbZu#qFK-k3;meRGA)9LJgXb4}@gLAL05W-{mOK|jnY62t0FUua zx~;Bzn=WSno#0xNg8J|oB;XVtbywqb7n!_Y^euFH$r?&!9?B_y``u=zw%|CKEd#N0 zmfEthl7DVEuO_|xnSrJ1t`7;SXx+fXn(a%jn#`z;HB{!LWN)+{EPBTK?QG8@)~}SA>s{XO<--pBp#OPb zB-HmlM&pO?trsQ>w_k2-;Wo8i`#T4=CkNh^{5NNyx!|UYzFP4j^aa^i#HM>XVE-zq zq;qY>Ub;{xoMc>LZzOqZ+&#%=j03&#&|U25io)X_b(c2vhtr?QsvkTYccOW^%2l>x zZ|@t}UZq2fP~n7PriZ#{)#^d+L*_tCjU@7O&8fO_rqr-CaWiHwe{6Fc6}{p;-B{vl z#1b{?&^fIcA~dn?8iK*KkQno$i4?T;=m9&cOVwHX3wglvZ+aMwgKjk>dBcQ^2Sjrs zArMyHuFpS-nb;x(t~Csz4w{H^Az%q6wBM4^ErbhQGl3Bir9<>IZVG{~>$XVgwAO$g zpp~M4t4>l>kdK6g5|lpVmQr6-kJ^7>@1J6E*6ik(d8-=C+W%{2z=qy);7~4m3E(3* zYlSb5o4fkd;mUD8a7-4`3?}|IX;yxrICih+Jguxf>5O-lllQNG{?Sy@Z@=-s*ACyM zVjJsayVU?GvF9@_lBt!#fYl$qnR1EB?WQ%aeq93UG=Ry5-b@V&w5zW7dm^_jK#wbo zUv1wp>{S3$&6_4+0h*I`C}#gzpEX*yuXk966&!nTN586noqiLp2Vrj~glhUS;pIMc z?M=DixapOr&15u3TaU8OUX!#ljhTU~RrtP*S#{)VJFS94ugA$`^GA3WTe|$(LrSOn z#9mW*5(~F_ka)=*CT%P)h9geT!%wWnDBq;c7-fd)cKY`}-^~Sy|N4=-gpndatr`=p z)ifLYM6zsMThg{dG+0}yvaZ~u6?#}}8VMSyTLl=tC!3SdH%D_CLZP=YY zbfarU@E%>w)M-rhBov`;%0fIP5@dY9Q!!9Ai4hYM@rgW^!^ z(7~W{&inBHW&9)a2TzlUORKc!1{cQ?dz^aR^{=%kbP!fVB~71GUc-HQViQ%bEfpuT zzm^=i1CHtbQYxIRTz`7~WkG&;%8bXU8sJVErhmdM`|*%^W+6XdUB4s$T%P5h|8Q%A z{LtRIuB|q0-MURGH28>YA~Q@MhJ3mUCwVAS8X1G489*gCi+esy?Wk-xLX)N6`|$Be4R~6{@aacina|use)^ zw8^}c5w@kH1_*vNw#0^LKwg9nQh*+cs)#k|q5$$(xJ%r(3m8jCvg%``jV2G1Y1QB0 z%8Va0)WANzV5V&Z2Q8;Cw8JJ!g<-IBZM+xMPu;(<3zjvS@~VYvnP}XO_4M3j zn!D@G+1g0f-E?K1>Fx1;rbs@elKY!>YuR{TDq-KX$A0(azMFcW7%LCI!*@L6{+$f} z1V*aZ)o6sF3z=jEaPPh?FOJ06Fd76IHa*#$|9yj<3wxP5)8z}-htqGrhOqkaaDRO{ zIejU+nNEAL0#wRoi6}8-{$e&~5joH@UdtDAc#z1EvU4%eNcXI5-0&NuSN-Tv{<6r)mimryr>~~%&fD(4dZ*emL4AA9r%ZR2vAtX%Y)A2E}22^z9 z^B`jRtP{iu!1rpCyoRS6MEF` z{Z+W64;0kE_TH3jtv`aeIeDNQ6ahCgd#O3cIXqp8r{8TZ2V2H0hGNv)>(ZDXa>o9L z=+SHM%!Fg6(OM~uCy$kV`{X*Sx7I#-<%Y)el6%2=x_ZfeRNd`9IPi)O8M|*V=>_HX zPmZ!j{8Tknf}gCGTgllB1K<`5Gnw~}+uwPlqH4Q)?Z$muoTi;*(6GIvqLh@;OJE52qn`6wgRY!Kx1zgZr5!7?*HRfh*Z!N4ZlP2ao}cGO5pCF7<)x0KIMC)=AYw@0&qY9{?z ze;WU}Wuoy~Cs{ks0EeN0=6RJc%EmprJL^7t*@!hzZmMO5^JGT7o6>fR0RNVpGMD2- zcQ;c8N?JSZt*IshU#l$Wc89_b>>!o>?G5PpH;%Eq+#cFtoufiPgW=&ynm*)A|Hq8} zf88|ZltRp=EWeW5sR$jVrNdnxR+;Pg15!CC8a@W=n3w9!*tcyPVd+`peD8pHVrW_q-a!vV}w{#N+jJ?b$jUO1g?f;r9d;T-WP+XL#&^g6Eb=-PYga zL39D8+F{Oe#M`C#^3F{8`$_0~rSU+`s8k+yecc#Yimn$-L|$ZK6_=+n}_RDy0rwC+Nf)=%Kn?fIEpg z;F@@qe3bf7hF1x1p65wus5DYFMoY!kq4C^a{s{Rkukm@mD4V9+3+y5Q{W$hbCr^5Q zfwWYKzvXwZA^xp*Lw;T@|8gOD-~0N~;Y51Z8-JE@o^sdk!qLpT)}$<8R3^oS+9<=g z?6$Dz>btTg-6yJE8;{^>2DdYi`RrhtP-gmpy8WHr=)xS`C-%-s!B zr%g4paM&c}*{d?OZGGO3#DJ=_8F4XmSH_8}>NHC`rfy2BYphtf!E(Rj@Eg>ic3i4Gv|!Ky)W$RvCV~@ zFh6Z(9Xyew_-grmGg~)W*6_|`bi3_FBGY&#cctlGmY}mf#8`vfb9NNp1=@sJy62u0r%a9%jPsL^UVh3ZOD+;xfFNU9sb z=?IIUXq*f!1Z&<||3OM%>N!#(HT7l2$iDICP>Ix7tyGYeTFTxsVb=#vtHBYkd|u_k z4VooJZDXWb_P$Rk`{Pq8-@c0Njcuw}>|0)oHE|Qe4_yI|6qw%+ewpPl^XzSI#P2xm zUAXt@zZ{$Ctbgp;-J|x#O^%vQ;-$DI+lBM@Okjh&MEs$dJRMIYX7F5?TK1xO zwaUC)8%i6o>t3~feMOmbkEQH=hX)v)cyXkMv3l09b#(a?S!?_MZUn*JUcj|4HQ7PT z_3K_4O~=6U&&Ab#N%4c<#1BTWj&dh#G$eI>u{JDM@+d1v*vL4i1M%LL%Ytr0)LU+7hY&bzp3$+u$CtGa)pDS7@qy z(1tcFQB>C{l5$~OxSJ%-ycFw1L>@-B$ilnMkauRVP+Cd(P#B`OcoRfWqGvLLi-=DC zDm3P0d(!4Rw`n?a^}sS~GE1+l-S)gl_BXF)41?nUTdl3}T@`$&W3HMq=WV@#=8p+( zRC$0!zKPdH_vZ`w+#Y6UC9PKHFXtR@=LfjW}>7g^3{sn z`J)~4_7g9B(mR;lsFE!F3S$JC0)=T9o2D`W>{gx z@LXDJ(Gpw~z6v@zR)s2nxHqAyi?OoAYKgyot7}hl=6F3HPSdd7Ov*JaR%*dq8~q8* z#Und_Uvr)R3}@fC!MwL(pZivG?~Hk^v8#~(7NZ-Eg(9)7keFGxSf4X%W#h)Z!=0Zr z$k(3j47+S|R6*0PK5u$W%N%An<-eZDTeqf&rM`M1P?78-wi}BKB=0P# z3+)xGOe*H$H;3uPM1%IA+0hB*u&X0iOhqy^lYPa(1vYY{)+-EZ0-=fe*D^;W5f1#u zsLy(tQc-;qgq|9XXGb4rdUrHr`xVp8G|mOpchnl{M`w#U)>-Z9Nv-W@lVfLsGN4&M z>F1Etj)fchn{exKc_|!gJdING9OU^iFtHojh{Kd;tup-<`)A2 zxw*)RN)8+k9@McHGE7zAIUZ^;>&ROa$&2X_os)}6$b}xEC*r~|E|hv$sI38{akHoXs2#hm0)ke1u<3T5s8m%&l~PxS zVrnvK<)MDt$5nA+5VKe}BCiXs^#%3tE>6+nd0V}|^S~U=DLZVPH<$h9b3&LZyXW24 ze#xN8ztK5*dF=tOnHX1kx+_(I=^Y;?fqOke)%|ibF;B!}yEGD?-vHo7F*sQ}uIkopvzc=rxq*?L!Rb-M zF26?DBj|0bDI_1eJTxBLQ}#nl-k_zl@M-wENjnj%WeVR@$r+ueNpAiPJ(n@V?f{vq zN)b&W6;hla3si%*=?@65wd;sv#9`@Rq-zFbXB9Gw=;uWAg63-+mWeTvK)>m#c#Mc~ z5_+dAo-GV$B>eeqORwmNj&!v%L)UZ%(Q3(9-icKTL}gGKbQDz~CBzEU0%YQ zsv{b&Ym$=^)8IMt0~4W-Q~^Wov5#D(ULt;xRhGY}7UzTKCsqE8SN@`-me}kK-+Ad7 z*XoOyW~5}#K+jW&8yaRVR=R=j|7DV|TlXBXKzchHj_n_^r&H@8T`fqz82?N!G76^! zVc{720?6n{q?}B4NHzVH@y)%_C+G!wfI(sPWKYaePEXEQ|Ht{JS{qLiwl^6@q?S=A z=E{Ku8TZt3Bu4REXd})tA6`)TOSeJbm);u4} z;Ew!<;J8j259HV;H|lOBYwJi^aAJvPup3-g^bvLG0*y@lUbQS5rPN4*ZfNAQqCxt@ z@jQE(%6h6jNiDVa z!v&^wP(y}yz&zgEx3|arOkh3JxtG|M&Wf2j{b_)8l`D(sb=RAYwJni7bJD$Wf3q== zi;P^Ts>|1@ut~cB%jy)1oK7Dntex@)nYe2l2}TZO?vM7w9JElj{w@)(TSYbY?rLH~ zAsE|j?=9KsLABD1tiN^F&~8;N+&g1c&gXMmI*0arCLY-1YpW1o0H{NowIcT&3EciL zA2wNSwOGy2cYu|DKadaDBqpk}w?VZRU^`%0nFGW$>%Q1I2w9;kf?J7cs?0ESf?-CN z0EyVLbxLK94VN&z&~^&BJ(k)W_V0a_s0&_15-Zs>feV8+8`K@ke}&o4z2R7v5Quq2 z7}i4efw4uFr!nx_oV99Qv)BWm%I?R|#GrR>dD z2MpiXOs?Cp*WR#x&xY;xa@Z8K(p@XcLhIx84ZA#g9gnvRrgEkVC1SVmgX_*YYLWFo zYNb`amjI}D$6##VUiCS`wuJ+|wz?rXM#;ld$Bx@Gp)WWalqZdZXT7BCex=xya-X#e zrD%T~`xDzzieI*!ey_vc`E)9FqZxd9(Ww%@1MSeX!ISvka9)xP- zQ1u|EE@y#b&MO49`=fT#gb+rBGH`e zYUGS8^OIuXP!tSc82m?Lnt3c+ofN%`YoS__a1lhaXj>?hQo6>bVu-{l3J}$enuy`0 zkWH|oLoz9Ycg-BO4`@|ieNk0*nggB+tl)7rhb&AP>ai zvJ->H@(SUP1zbFww9%iuYRHu3LSZcGo?g}iBv!fuM6cvoW4V@Z%+o5bRZ3X?ih~bTo zPOIyBX_hDb|4?r*tso?*>Wg#gg?xSZgH`o@!oI$VM9Olj>>q1IOJjRg^pLu7UA#2q zol~bO)%s!f)KDVJ5Bag4YH5NUwk>m*#)wq9Q7u@`56^}Y<=DFl!6Kwi6}`MSv?JP| z@7o@BT2A;BGX}YKndOr3=pXg+YoU8lhWCQ@(%AdQ|KeVFB2M;5w&i-`rN`wr&~1q6 zoB-AG*`zYw+e{_ACRqcsS1f!&)JbM*n;mNd?stkzG!^nQExN-1BP0u*YZs<7?)T~M z1HlL3f}Xn5O);L!O&npXrC(4_GI)xS=;ViF){TbLUZub0+tlr3zj0&DrZw+c^Y;c} zo-TR86vc^3w4?!bF+7++$qXr|NvH-A6hyKCzL@Brv>t`SCrfQj^at!yD)}fRl1G&U zl0vI^LX09$V8yteMxmr%_WGI z#Drqh1oW4Kgoj?WnZQ`9RY25R8n6WXty9DqBKzekNlk0ihFOfPAT6zd(D6xo=lYbY z5Z0GZEt-};b(^pP?c-7owNIW4Mb@X2$S#@8q^?g^9>+L5U#|Cz5VgOEBLUw$zf(mw zTG1u=H=Fy+j5`T?%=S90(^C+-@(WFyp)rYfp~IEYvU@In;PdzWLo9aijejnjvRbiF zc&KMRqOut0Kj*AR%7B|UF=a4Fo-ot9mgtgfUA%RGuSfD>JXqf##)Z` z*pp0X@{@U$^k*hLJ3dwcxjTEYZD0O@-r@%)aKsInOJgr5vOqcvLR9pR5DFHHwjCXl z6@ZR!r>tk^eH#tU4JIb>u3%~jFNodVSPTNTrxE7x6&%;pB>J!Idb#0x zL7@p^_Pzb*?Z>WrvDm*1v2u^D4<2Su>8IGU8v)bEuK5Z23e2hV4-8$KwU651pp02D zb7%$VrdUJKz}1M34M~wY;bu!LZ5WnKq^Xw_;iD}FoN z9S4gkB!;Br597+kXly_m@$y<3TfAP|KYqzs(IBZ@TK8m=*kV29IMKwYiw{CBp@p8y zV9$wuVZTbTKmb~dss0P(#rO;3L+a%h+{StJ01|!%8i2an*ivRGXFirvXRgF3?H|kc z9f_n8A--X?5>K8)1=QKYz2UE4^NW5e`N_k@(j4tQjo!UIvFFv18w!7*)J1I1d%5q_c-6jf#C>)o=VpR{P)rutzDA?kobgQzmD8|}wrrrQlhe>~Gv>2X z0~jlNd%;1`DOQHT@d47IQUwfFVEm>#YPskr4ad8uAVH7ZP6NMTQK8hRzK~DSDc2?o zrw7XMni)S+YcbHZ+FWEW23IVvJ36IatXBuAl}Z-XkT1s(-TR{bVYoLu2ilAp%C=pL zWV@VnXIUUw-E@aVLM>$N%~40khreTf75pT+=1;_CB6FS0${=7-BvBKwLNbj6Kd99U zE5)N$&hG=Xn2^`$_I~*-ZBS@}0Q8diXzh?k=q-@|swZYuzZeDW6Y2z-vDyhEb`OQX z>WTXoE2@ri?T^{=kB#hPjHv> z>-yh6uKw{(YMtr$aj&7~Ug}cln=&JsV31WnQaOGW}N@zPiZOc#15(HJYr@Umk3uz763U<{5{NBMmoC6O2o7p>j39a~SC%{IM_@n^QCtC5 zRs%r!Y!W`8i&#dx!?%vRR^57dk zuFNuLV5X-Ru*5D7CgWi1>iI@>{8rlH{Mr(J2ZJ%|y7g+aObHX0%s3vUwpZd-;S1!f zZ2%AETr&z}E|mDEfrIVutqdkMt~Yhy`Bi9e3h*Y<$kf%&0Za*Dom)3T)N|Nw({u|K z{Tc`Q{8-6ZRKryjPAsddHU;kdu8^0C<-#8>=V{2WNWxQ*lU~xl*MGFUus}+(kldu| z&PZ%>e9n5XQLw*riZMQ_dIl>s@k?a~gxCc`Y|%3?;9yX*Q>By{Psi_YGZn8&)n;_Ih?4$&eqC(^jgoYB~i77(!P%;#zn`lh7WM;p-80vUddIMzXXBaI*+211b zb?u?(B?mxbFsoixut43$tDWmCM~BvspCNC?m{TEM1Pj}?6Z*V8>zWf+lu|5sXN`{I zQaf;I>a*8Yjjf&6dHJ{0Zw;vzTXAdXXMTCp|I8=pGW**q%^%i3QFR(D)iTBFb5~W* z8&>SHYSO3Fz^>2A_+V|@t!IMs#qH`(d$aunOtp1>^1?Ir7u|R5W6IF@R5G_W=$xPa z%|3T#RzdxxGHH&x>H?z@`okBv@m0CNqsNGk^UXrPjtKj5$#O%@+tKZ+T~=+!-(B{S zUY<6p8@svpxgqtF(|sXIru+Ld>!!<> zG&z({jD55i`s7FD#P? zX-HGQd5F1@9wd>gq@Xy|>{wpxA*$FNo2{wC7(^*zkPe_k5{(HwC<-NBRIFS#YsPP4 z*;e&LlH9h0T|j!gRlpV0X0`DQ!;9i-Mdg=TO(i5;)m?4rh|zG+yOo-($ii*{?$w^y2n(>>P?gZB;mhKj|?i%v3L8L|^`>oVt(Rq&R~Xrf%Pc8tYi zR}MF==0MC|9PKgP8NY4q+#cBK(c&P6b-KOSnA=eorD4{85Py5)?bq4gI~&i#G`+Gi zlCY2Ng-xg@9NL+k7OWH-268q5{2NVI`?gZ)qTZ2NatRKT-hNqBHMeBUA1&L~26q3Z z@Wcf+i`tt;e3+85L#jKMZiB{Lde^LFTkB`FwA7rK+G?VFNVL)GXAlIsl`{Yk3(;i~U>$Zvbl~9T5d(^QJakqJ8KE zn!XCzU{ys6Q4~>PY^OZpUK+3wrkf@lqgwJI#G!I#WOjv)AF=V^EZtIOne+8D$_f>FmMX7&P>br!x_CF@npB;%ze2PeiN_z6tP70ru zz7!>({;2&U*nEj?fdizvSPmX1;~XBPpM4;Fy8*kQ<}@}h6G zTb9u`s;ax00F{kJi&yfbpHD>amQ4nj*C?A_Y%Bq-Q2MjWZs49>*Zat{{on2?S5&NY zit50G-!qt}M~VW-v|!Pd>s5uL71YB{jk+vpg7kaav|_-6PlsveCw!#1Z<=*M1q53r z`m+W=C*8yN+v?9gM!j{IthK;Sv*wzqHS<(me@6UAh$T8kItmHe%<_Ytdp?1~gv0}q zqN{0rRIOSVEI1apR%{vdlU7~7mZ$qC^gyAcf%}!z0*2zMpY+4xtl!Kb!re$vatX;_ zMdhraKzG^cHaM&w5u{wct;12&PP!?_rNoDp;>-a|FuFRQ(JVWi=SYxAMl6Q`U6)5# z(yfMty%v<26USB2e=aPcEkEmf$TcTf^XOIW;uS+2;*Y(ZfTgGC8;6z||7^czcb;iX z^F6;r!AdC9Cy9WjsF@r0c$qJWr2lvzxuSx6=O;Y=?8Av-=LLKAlIQ%eV0=vVj(_&= zY{=90N3T{ZcdHY57Ux|5-(K|~RKz2{KcdE_S=mwC)HyVoRm1N{N54QKc`#G+Q|8?n z6?>=@o;tw@hg$y8;1e-3x;s<#n4=vXY)JmlcOVNJE?8kZf*n-MItcM$*Lc`-83i6; zJLUd)2y0F(_5afL?(uQe_no)SoZrkjGv~~lGjq<&8O@9|l19?V8cAboB#kZEk}cV? zk!=}-5!e`m4K^5%!2}abaKH&}41oj^NJ#27kdl<7G$l>aT-zi~v!qRP?>22{OP_YP z>9g%7yKFbROF=}e&NnWWjMs4*v&JWqf+dvb)% z1k-8QpwBeJ?}haKJ9cOLO!p}Ju$ckHZn9bjVT>~pto+Lyk zqAr?U5J#Bp9k8{>!(8Au=;2SWi>Zu%-oW0yU2UJz3X)+F+A~a(IAy{CDf3o^kwc1R zATu$aqXlf;L{m_=_jtT6&n;4GEQi-ZeEcrg0u?S>#44IR4(&!cQ@c7uuM3?&8 zH|y;x?Ol^EcXR#qA=0Q7okxTZvKxZ-zYsJmhkIg|f^TRx?YJums)M?6E45Ocg}I z6FZy9t{S>EzQfd?E6l7srq0H#N zb|jlQ*rlpKy{ZF29KOF8i?2X-+ugb6_h*6uV<%PHLVL7l;=fM?{!6M-nl4VW{LAt! za?WSm7o2OaAYHgKmYFnrZjH2+-Ol0F=b!4^c*ZUswpZY2c1z-!$tOwsX!dwfb?)Q+ z>_k(emOW>BxekJ_Em>uhqve5}Xi7RYx^Xa;T|_Q(0}N3cpQT@Pb}Da~nPm0=jTx{q zF!GxBol|x&@2Yf9z3THCuh^1nH)D6L>!a2ED8k56Z4(~=FB75))wFuTyY_zG3R7|l z8fVt;xn|VJ3{x;nzH0?YGBcu_;*Sfv-u-#dt(P~9XlK-&Hygp>c;{}Fx7H2khp;UN zuZLk^*{lqfZrN(GD=ii^>-F6F*;{Wi|MXceMVw#UvA&_k?hTx5Gy|4x{sEoW|YRdw(6dLn7w*w1vRS&i?QhuN7uF)q8l%dbo|Fs;*9n+K=1UWbbjbhT-q zIp4`9!Vx-?P(4XU zRFK)wLA0b(dM_xli*peuEqMYZS}PoHz#Uvo3xSV?T7d^b@59s<8Tar#qOq-SgwChs zd8GP<9`JrzIEP;h_wftIav2h}B258IxT(-QpeSMebDY|&l<4{0vuhP80r*V<3GNhc zAylu(F~sXg@BQXT$tFwKcuvb$rodSN5ogygVcx=^#n87`&3E&4qX2N?(`Sx~+P9V(Sz% z$TW5IlI$ZkO+7~E8yDA!SCiQdtVN)#8{`j^o`vtC442AtcKI}^E>m42G_tDm=AvCu zaUC+3s~&h#98A2Jf9T4+W1FtDI|iS9^ijN|U74QCJH-zUs~?_rNvJX1GBoK{?D6*F zBPv|(g+r_6>Fu_N34F32aHTPt;#x@vmNXWdlS=MLT4$(0*sv{@cIDdINlJRl6v!_7p0AS!ZYK|t331kF+shhmNv7NYDmD$ zJSLnZ;psxF-ik*7Q9J-a2UQ6`59N|NY3O`HhlHAv5ea+i&}uq0kUR-u1c-?S@3djszp5aOn_20jTU)?~Wn)KDW4 zUn(o;1n!M8#{|}!P$g_oi7k*>+A>Hp3k4AD&FuCTl33IR60+@vdX&N zC2Nr!9U6_q{Cd%xd@4}UEB4Ol_R?`i93t_=EhaOvD%*`6eFE@q+~_zLEdLbWD_nX? z4HQ*&Q$2Op^{Jvyw~LyOdj3IQ?OPf-$qoXIpt@MT^dE}HGFNV=oe5&ISwA*zS`XB~ z-RCiPGVZ09_H|;fH0_z`8%o5G&ntiGk@7XBdcJNIyUILr1Le}mG0=1sS2LAJV1x6I zc&7G|NIOHS_UvM*b7LeKfh}_*ZlpH>~%jRp$^G?#t;E zQ_XH#*OQ*Z)I@4~{w0X-x7k@p3|FEQ=!)2Cf7GfF51Xd~D#%2$ZY|lfJp0?7=8vAB zgE^g0XX-zfG!QtYI<52q=A6P!cbVA8v3p8UHf%DQ98f$y0BrVEd0J1?G3F6*+yLG1 z1CP(c6E^w6^T0w7+qSKW$hVaUpgU{eMq-cVJD}bS7w;bHI6N zq@EGSZ>l@ntG?5B8x0SRSGJM{rW{nUVAZP~?H6w69h!#j9;v(#iAiv-CYi~zCc7$b zuw1MK=2F>Xy{w|?d4#?xFz+$TJ8Ldg0yC;ZsTEZ#^LBkqhb1tN>91DZQ~Dg~7xp`- z(~*v_t<`7`rj!c-W6^)%0!`2{yPD`YlxUh`_wobE z&rIx~!|e8>k_}~Rcg`bvNG0o;?Y^1EPOQ+?Z!d3*^xQQT*6Y3+_G-o0blge(_*!i3 zJ%YNWVG41mN;RL81WPlMPBhj6t+A$by9*F)3Ro6aHq;?>*rC`UOTXy0LdFo*5MLuD zAUGzq342W?QcKr(=KY%%DKMz@+Qss#+j*AciUTXU1ofwT!!nB0yZ8P zpFS~DFS_MreZVbd@>cc$@64lqY{v%+@#E@nN5-OyO_beHEqso z3{lQAJ*tW;)9k)3xNio7&ZPp1{VZbLj&Wb7<396AWdm8e>k9 zkm9M}QjNs`;iF@;m8Ff+3fmN0_rDXYs5m~E-VuF1W83DOcx|WnpFM$MlwOh145!w*C7!tXT%kVC!gmezRBYZ9_aww6gXiO44 zvX_}6XQ-vWs8x!2=5g3^6qKN`66Slr>SEAamrImzxz88%l_%g8ot(L=3}w>lBIrnGHXZ7aY{_g07sU&N*>^J@()uD__~vgHr<_*TXKfR)x#r>oUM zlq}}z*Ee<0`Fc-TIuLs|=ggDp2{pKL(r;>oohAu|f!aHbJ}=3mP@S!*-f*n&(DoL?9}Y?K`2+}U6L%EARqU~sjql%%AkwzsDP!mLXDsX z`m>#9kUZKTw`CWWRkXnTdOX{yyQIS_1K^jdm9f)Y_zD>)iszZktWa@LE-)8pA1tmE zO%jc^|J9sF0#O27FK+g0C3~TJJexA%7U*UcP?GCaZ(yK@MgTI66*h~o9gAu`nW}Z^ zv@zZ;eMKp~+lEc_e1EUzyrgH8sE>^Lr(_Pi`=lD?s@BD-CnlU!RLyN$QN zqqbWc#qN&=#C$#Q3eZr&A%^Ah7FKuL^rhLAv`gB5Xrct9Hm=& z44vv>oXL9<*$mP1WmJMFwz|`KN-X_EV*>GB10*O*tM4Ry76D*HiTDMGL6LiL2U(tP z?RUeZqiKZ4p}q;jKV&`nCbSnn9^AS>j&R`yvtdPI))2!_w306dXYSXi19c2H)xwDZl4JER znr0(!K)!uSi!FC1&p7(=j?~1}Alcj1pZ+KjxA&^mou6yEp1Eb~aaNbZ^uouj{nV@T znYXE9wXIjmH&6la1q4c1b@(VpCMevz3EtzTUKL z#*;@%4^z;z=jc0Ms+eJIbSE-LMKYdXMuOtIA+v`<9;DQ8$DDpf8XYs+hc_uLTAMa) zC+|+#ft!e%chmdZZ0PP@6V8UG=aPvSJ7*)QWceFKw<{L2>1!gGCH3#uO8x$f%eKP` zd5`*2#a#TQqWY&5`ax3J?LDM&%&PxL)eth6Mw!)SU6eljB$FZmOAJelT)UMnEp|KF zS-L`1iUTUabUZ9ZfVNb2PKi6R?GbK*9_a`unJAioTXW6QyJW+kFxhsn?U**KO<~iK zK?FKPJ_vqTo#G2Ite1^>gAh+-L~wTx$#SHq$ra5Im5X?ZU7bK?$#h798(c0|$j|Ws zM57a-^j>BV3VCTl0EJfI8U%o_dtKmt5Ak6*PDWP&DugbrZ5iN)YkWhXHi03cI{+tM zSduT2`v8TFHp56Xgew4iv__ik5N=*56nDaf8ZU%H5)&VW2SgUc1EhS=c+{K}Bg(>a zR*`?MMqzo0R*$E&-*FqbPQVgM7r9qaP&7v&5vBs9rjMG@+%X;3?p7ikf6ZKQy<)9b z*Y5J%V{nMuiD*ZGWw1drAMB$EEwg%0mo=}tTxap+noJ?m?`Ed91DR&j?W5JNm0vK+ zh4Dd5}tZwJ#~6Z zO{6)J7?0bz_z?{Jczdej482vI>9bYmmbb&gym!cmY!E&hWfS{(wwJU>yQ@C}W`n0I z3lXEvF6>AQ5)(6ek|bz7YOy~-r)$suE!#@gz2ZWzQz^z1^Se@$V?jCf8LWP(LV0#& zrUBNsIv*lC65ldcJ4aVt@!ma^bOwctrzWzW0yH^3IYrAY_~v;gt<8t$50YcIhi`rY zW)Q(k7Cp!FSFZbLxBHn7Co;;wyPU3W>pmy00URe8`;h;aul#tfKM%$!Ggs^;ol5sE zFlU*r=VMkim|Jwukl+^*PUe(S+0mzbg4noj7NY<+qun}h+j^H3eH;Em_G)~O*`qY| z+`Vld(b#K6Ry>Sjk(-d9HxyhxmC0!&FWjLCfin*hArz_8>0`lsl5@;9rN0=+FG<3z zSo9{kB9^vA;0&neI?{#wMEr(SghY>cX4I&HP6+JcI{uNL17upEtXOD(ItT`3ljMEq zP-tBMwh*GAJvc9dVM*X$jub9ykANacB+83zp+Xh{qEL}}4RnS$M?e^VT286u=0uSM z5{b^pcSMOmQ(<}uop0$I$f`H;DcaNXdu{J5!gSSKvhdP#htkx5sP3RB{s?k}NexIj zkA%@enV7Ite~r!*Ni7s1tjU!RSZ>qMvv!m;im5haqOnnG~)dBL=2mQ3_KTgRdyX}646-WYTw-mW78FJ%f7Ok z*Iqy2mZKwCXS8XaHlB==jfEPv>e^$8XsUYH_N{EDbl-uoEk`fVnA(J6Y^7Gy-`ZbN z|9poiMY6EFz=+e(yaDA3H3HH)Hfz1~GH+BdX@dQz6}z{wlsZF0`Xs|K)zXp~&w%KN zy0VG0TXU}+%kF`D5_B&cu+lD?!N~cP5 zg5-C?Mh9t}CtjAX$-j&HXdxDnT**9!Qj4JQHP}L~Lcqf^DE40jxjKP~uKEH!s2bBV z(W-tmTf)W9LzW&qNb{_#Z z@EvRIW=L=g!10!dtVPdxfW#1M<58jp)WoDZhPte23NOnO3oi@#pmE8_@+cT~rOziE zhe5xD%Sxsaq-IBwsFf0s-d%X^1A`$>Hx*TJ!}sj`G1e4i zd#JeDP6{<$=Z7rQSivkM@a+ecCl>RM9*%~ zub5*a&*u8>=rDhvC8zaXUSA`rWJGP@TZzOdKh2RCjkWJP(5SF)3pT1M_aV8%i+4_I zvE8hvhGL33MG!ZxO~_>o{XxF0D-xUbF}JA4 zsV5hgsNls8;R47M$I?@C=aadTQ-o|n&nsA$DPeK@*_~Fh7{XdvW_^s+b#OzUetcwr zl`ctaO#XCq@Jn7*g&mg4qOVw|waxXXE72=zrdQb07_>GI7_bQhLAD>>5_6E@{^CLN zRp|{XzVS(V*Vaok64tE7h(fN^IyJypUkFtMVStbLgY*d6>8Jl&P)MwWPC}&!(@Eeh zG$MT91h7mtLC`KC#uur(EO_A~L-!;YF2q%aL^L241P*Bo;}-Hl(z7NVYXWuz_92^+ z)e?=Ah+W)~gc>L~xqbv)o|B)nybI#ZgGGzTOoe<(g5Mc0? zJBh-jy?8A|kz#h};kZOJDfB^7wvaF2(P?GTW_jNhhg=*z2?K&fBnJH5`cffbXme{@?LV?|$=T(_+CRF~g?Y#uo za8EjMQ8)MZIJWheJn&+^_{j5W^uz?E+MsJCGraoGtCyZCZqBh|{;Z;{r$z&Wyw{T! z9N@Ohp)Hrv?fUSYruJXxQ&5>*p8G^_nrYj_VR|@yI!FU}<>GfjP+&jXWb%s<375$% zh(zIyAn5niqYX2Wm{PGr5I-@xphr@@mdZ0R*ffYs0tQ{vMl3hoj{;t-+~e6*x|93` zVO4`Wr` z2a$@mquGPjP@C>eogE#ug;VyMyJzgby5JSd2YhytRvJC|JPYx72E9i8LuIlsso=lQ zvhd8xZ;H4usi$I|6O=<%CVMM+U(2EUl$XUV!2#Ft#&7C>8Tme$+*;eic%d=E`6;cB zR8(UWIix722R_0wg1g~IfhQ@O{%Ukaw(5$X2_qNUK_vw%X?&1a>$>po2n98HCf|vK zpCEoIc;MC^JfrDrpXIXNMAOs$mi0rqxOP=39f`(=lF2`eXr*-Ojzhk;aAoA}rkn4M zum0H!sXuTdPO?waARkVc$!z_*reBC@*?h{mK^ydGuTA#m?ul%<0+y`Yt`EK`nJJqL zrVO~IW<`_fy6W%LBgdm8rXo>oAYnu`>(bX5h1k5qF`z<@Oqp7?a>9Pb`8o`Jz7scW zH~wU|n>6EII+p-Ey{{-2#U9Wj(MU%j5_^R>0S><2ZPhL}rW=Ni!l%usnMtj_YR0v5 z+~+zxM<1p(nPR`u{p{fl>+G145HCtv1fw90SYlYhHd#K!MhF6>2T=&FTEj|sV(GQQ zwp`Y7X;K+m>oXL#N4~^P2rhu2yoDdbhL15%uKgxj()aE_efDJg;C~tlY`^3RXZ&H0 zRn_O!ZJSNCdVFd-WjNO+1&v3UDwaI$d-H13S4rYpzm_LMD$*-bi-@Yo1xYPo!;kU( z#lWfMAM8on>JhM9vcpx+LMfz1Z%p^Acr39Y_n6<%Rcu`<`!$+rN7kn{JLSrEA55)( zlzkyD!eemW8qkc*sdujaOVRg?t+yn>PsK;HL-~g6c&{-h{`tta@dRGmLfhMsR%zsv zw9r_eASR~NXw>J%kwb4y(1vL!>3boz9w$#E#+e(9-Im6fFgPXEBXKScL=3$2g@(&Q z#A=JNB&EbgK3+sF<^J)QbR)c2Zaau6a(L5vRZNan;YG2|!Nd^RR_nTA=1(>P8rOZi zVm>>+GZw$~L#JGPS}Hp|w`k{S$JC|f7TA5aWU8JhnQ#EzR^@{HDIYiBD?zE ztT%7^Qg6PH{aiot+IZt{^^fYZmwhiBfFiFn9g>!=tQ8&cR|cK8t&Y|(jR6`AxLX~# zUGBKtaAs9xIH6V9m>5q3?L;E`E1Xe=dn>F+#A9+#BV-dKk+3GP(&mauz*hag7^+;XF2A9--i{=ORkRL#-1tC#SiuUAhKL1pZdq)+-}fzW7}^c%M{4M-mh3F%K{0|bZ$2o zUcy2qD8+Y6caC6%*2r#4IK`K@0s>T_6*D8vG!cN3Ltrm$pKBAr5+(o|winHcA1qQ93i>qtk;awlSj$t z;FxRz$$H6mYN@Del#ANCdUDaYRq)zZ7p=+ZcRq4`zR;0W(ZJGj4#KngVfMk=?$p&8 ze|y)}bNP66ovs~=4}bWNr*SHg`NXrKkyxaQ&thK-AgRJDCHL&;^kcfA{hQxz#`wMBk7O_nrjbg# zH=!GmbO3ZIX`FxKjn5E=NblT4+d0`0E7p++0(FwOIDofQLUiCh-%5)TW8b8xCYe?; zw-Vw~A_&plGl zU45TbwFerCj>eosqA^CnG_aC!DF=30K2%;oC&u$q$-U{p0BqLT5q6)oCrrC zaTSrGM0|~=um?&$ANR;J3z7@NzwvZWHhj7Ms{Wp)RD|f$W&-E5nr%OzSEp%I>dwqg z+@wuj8iggIT%{n=7l||P`%Jw%qj{GOPkpfwkf$owQyW#@IYrN(`o_AOI||Wlj#|L_ zPL7)WOyXuD7I;n>1MBY&rhYOwUeF#(M2vVYdiURM+fK6MJ@-CKkC(&VQFZ1XPd&O| zvV0}w*C1qx=3vX;bTciDwmQSYFOP93*S{(s7z1+ni4Al~1;rnGhOOOE>FSAp3jQ*| zy`RuOfuFNs)c+V>4mTDC!ZIsH*o_ttnIisS_A_*Btu!dNo7*1-vIuTCrvSX*JS4vw zrmF-wx26&Wn}j`267q*(H8&p>6m&s!Uol!hm~3&T!GXx(hSrzk6;#2S5XBG|C|`sn z?dqtu%Q|mO6>F|~t?oWCnau>2I-#Wed!sR4K!*iVZCSE^yU3XIuKiU{Rpth~L&GYP=CQHZuAVTB$>&rj*4;{hd1;Ab$z&gAbl*lZ zuBERh@ph7{hE+UM2erY^X?%|K{LUS$()%=tP_utM{XgF?ibHi^P?0>{z7M^&dFJ)LN!^tOI2TttgkKi49(6ar)gp+E-H)8 zXme|~?^4#4vunQbabgzXp4-*-JXVgoEEx>3a)L@gXyFtUsXuQWDsA8c8a`d=5o&259A?DVGoLcD&*~uqlydWP*BBI{3qw`-(S3{S-xv$**^25qMmLt)+D*-etfE1J%_d%*EYxGn&#mb1>OgSAWc5-D_0J{tv24#og6KQqsqN zWAz_0?Q|u?%1L9QTA%Ryhd#$SR1W3T{WE#z?8t4m8|GdAmBa%d!FT`ebT)=rzU7lu zy2kVuduSq&-jW)4^5c3RahhwPROF0#p}~bt_pzzNiKSP#2VH}ijbJf$n;CU3Gq`6u64ZJ!BTC6RuPM_); z(WDvTcwg!nx{%J%kGIn{AExa;K~4QBqd8NqBp*I+o7T)#?IpGvPn8F6v}*+R>8sY8 z=7v4wk1O%_YA~7_$?+zR4CNk0pKIpy!O^7kdKDVB^6KkG(tYU9cw2cZ+Ub9OSUGwF znM+#Lv(sj&vis?(TmI-)^U4p8Vpip>V%Wsb!-p+1xW%>sHj}?eA}lFM;p|0ii1fv= zgmZ`D+-0a%ELMon$NESn%G4r$9Zr^C9#*J%m_77HETzmc;*LVSkdOhhyJp-Bq=4h# zpd?ntw@G8%Sg4^@y>IgUKb?pw?bP4FT|TXPHW$q)S@QJAfXODQ2hX94f5&@Z0895p0X5+_MJKgD! z;=j#6MJJcjG-&;Oo!O-;x``*$V)f?=BE7#TsDp{_?=8LQ8P?GqozxEbopRrGW}kmw z|1LYeOeTe%=8mHmr~-G9#*t9w8O996e~5hwdlOr8plh|huw6Tp-B!mo@za=x4+*!2 z64r7&XbtKn`5Tl#a2N_8ISMIJhO*zv$H=ju7tQey9M$S>bZzHouD$PDO8v~~ zI;YI3s_8czn{k#h^Nf0u_9kj`CGQ3Mw4={P{m4ai(YAi2p9WU9bo3a8&?)XFlSpIS zgPZkU4?^$0g%da3VZU_VArH*@SJ!@WdnT*3^+ut&vTDm00d56`fAtuuSZ1 z`ytQm?|pFdY&K>OE`Mor1mZPSX61yQU;U$(obPK?;72_<53yO_3U~79Xu_3kH?;k` zmd%oEc?haRyhSVcKRk_=n+ahpH}-ONzJ-t_+Lbn9u<>X!jk{PgMbe(coyTclm+)GY zoy2p5AvLaAQWhL3341v$>|_(LCh!1%1FI!iO@?`;`$F0muyK@TW0DFXau#(3W61Sb zL#!OH%-4KA5&406=`XGELf|_09GQ8UVIBy;D#&|ZhQG-YBia5!BK*@J$YCKU+AcI6!ZNSV?-D}2-A{$gMU+TWO__Hngqhk8bG9GUh5JG(66pQVx( zv(h=o?i;Vv>b7@#aRRjcW6Dz#tCn_W5XYu|p=6epe5fy;u$wcLWd;P}{4zX!I$>`A z#MwdG*;IFG;5p;htaD_oe~^lPsFo-^=f?{5m}t#m$Fkc5&-pU3W4; zg3M?vmy5kDBZ-oSeop@${Q)}prfb?h*!Cj&X#`DuLWtW_oL(5#f+ffqkD*1JWY#0> zC72Gg(A@XBHX`Y3NkB{9TK|6mymAJN>CfEH!Lp*I9+~(n9{-k?(gz5}rRcn_y(8 zXDISw;ss;%x}m6dMQ_DOc0^e@plj{<_NbXOQ>#BB?$h28$*4@=La+X8XICm~`ZbwCc5+pYdhOViq2peZ+HJ9 z&_CXlvh>6~AG_{iS9@&ynSm4EdYARHHy3QT)OAkZ>VGel=ZTGHnZI`!{b0nycMRj{-^g}W2ZGX~RetCeRL?*4?^idMDf8Uq$Znw)X9(>_2Y@BuT zbL_ACPC9kbJY(kXPMQnp7;g5tUL&oa5mGJF_*^*_U4MYUm!R9JsoLuQ@TRokF2C}n z(Ivb5RHq8+gZC)!R?9xEE*)>hVS+i@@qM1?mM3rkyT)P!@l)I!tRp5&Y-LD6E(n8r z$&K@|nbJwde?(GZ8$%<9)dPSqLJ}fMZX@B-(%UTZS2m z@cCQqapX=$z0uYPH#JF`m@hLPy=-5w%|qHN-G^*r=$hC~CaqHP@oZ_-HMYLDx#_p7 zw~iN^GV2(NOFU&4KR|a$I$@o9*{=XOfD+U2i(2Ggt9~b5jxN`&uLP{DNW@R6@pLV5 z7gurGBlpE)i}4seQbEb8?qgsi^2o@wrk{C}dGvWJ7JJTZ<`1BcK>Up7J>V{JE`4R2L%+tIdtN)~|XSk`VQ+riSjvOtFc}}{zz(^z>hHsynXK|K!NB23y z&?S)!n(RHTUAlft?2`o+{My|lILzsELB?&Kq$IuihTWN-Byq<^opo6bsvM8UWS~-M z|HqML(avn1*FH9Mr)umRA2Op8&83ydl`p(v)&(Q6PUjjmI_Q7qcgJ^?R+N7+pL$oZ z>oDD`=O`NgR`Edh6(4gNwe0x)HwM0T;6Fdcu%~aAZ`mB}H|_JRtaRN;T39sWirsD{ z8S{(p-cOHpBDwna);igFLwC{ObFp$I@b>`VoJ+Ko>sY= zT|a+O?86}dJH{x;{1W#}`~vp$89ig5AVE2TTw;cN8a_jm&qE}{H**1Cs3+_m>A}Au z45^6pP|4BCj5Z#4Y!u@#1EgujA`8PaQ+=yjK54)Idl9gU9o5UDe;A^dY=~aEvl+|k z@FKTHHjJh+{=4oT_VS$r9YCMF)2HW#^k&4d`n40u_WhsP>AP-s;)WBk#4GvslwA&1 zf0S_lJf169kqrw+&!wvQ56;~AXff@*^Ty`T;Gvyf@`kxCe>V|LyOE1$v({}Ha)ynLaX+0+$D_{#zOKa{jOc~|?QSIc1;0H;x##e(XLYCa zxYB6zR&gG^`dTclJwUlH<9(S5$V9i8PNIaf%?T9xj+~&5r_KMpc-EJn_Q@?lM#;BGoafp@*}C;BsyzebxE*qC;*?Vh&# z+8zk;E2L|eQK%e>6XA4Pc(zs3`M)0O+L>0xyZ+yxtl=RsdaD70emI`d&_zbrS0p*= z{&ApLvx;N|h93T_UGow9of|o-C5@24vP#*TBOSMRS8x4XFEqlX&UT23ojx@Y8P-w| zg(f3j$LOaMxm;A+qX!YUeO$XTnk7frUCgh3QgdQeS=;UekQ4 zv+Vk*e9DYox-JW4z|Z^pBAC%a&W>IBFQt?fNp$L&n466pH-8-aYvVu0YO&Q-^>5KP zApD48FaJF~06*cb%kH0}Z7*}5iOIqWrzG65n1bn=(oQTnW^sQK-HQ?Bb_rGtJs+hO z*m*F5U_fqo6VpfFhI7M=N;kWFQv!Rb0Z8#wN}qDm!%{$4YvDm+mcv#9K`Y`V!$y1p z$FPMMxWolm%C!!HQ9=#L=1fEm?#bQ83Y*odtt;wpD3P`m%nDJidzQNezd0`}WG(&n4aFlLnQ4}RVVSSO*3#=-KFd1G{`0zM~G z76Y*|cen4|HKnfg=xkDI@Wxnt@?0iO28Pur+TTj2!KoM2a+&P!%{}lt-E2Bin)hgx1kT)Ex8F98Rcd*?)DC-X^@Z+jK&}1je#}CPQa15wjV{^t>^m*= zj7o2>&hB2L3XSCZch^=_-aX-!|C~38F*gryx>;Ckc5E+eAMQR>xo=$re4|rS{R+7N zlg`-dVMuJ_4z9&zomxj|-$MP-vSQylH3GUA2P}4RG~i1yZW7Z`JfJXeI1AJ&rOuR}p)1{oCV z{c#f4K`xTIc-HOPvBS+Jw|}LOA8j-`OP!IJuD$p180=?$G@5ueri~_M{x?@vMStsW z{DuBWa&W_Jdf49fuC`y}tWiiqia_DP2}Y@)LJXrL0+Np*R|brgG4TmGSsoxQzhUjC zRecM)QUojsP8B7UHht07P&>oNU>U?a(y}w75gCSYq5vk~EEOeCFhNS#`Y9D9o&gwE zYXzfv`+)c3>UDScOAJRdB0Ku`E9SPWSBg(K&6%g;x_a=&9-sB%)4MlrDI3?;l(u?` zZ9e4^Gv`O2Ol*{zIf0fa)00&hwpH6R4d1ES*Ozb)1tEpvk zk)rF#iB8C!R$!QSok;Bc=^Axi#sO#Rv_e1$J*?un+jaD+KPy(Y6&m)28GotI052q%yPR^CTQ4yqH%?N} zQ5>Bh3mKD;x_9_!GHe)VZZ6>f29`K6=#?g}FofWY3@O-+g&{f-Ax|b{KrzqbQC>#E zb%`w`pvgk67h3MBb|p7e3r57OZQ7LYXTfvc$zFFQUw)yZI-xdS(AFJ2UU~QLA3mWj zHd*1+mnrSK#Z+(e9NBz2tLdwsJ<`R**^2q2-D+vsT9=B$Innj0s(r#X8<*~lZK{yB zcB&TKbPx<{9~AaPzvB6Xn^v>jan-sQjfSbL%qFlE&kNK_A>lOYhd*up*=?#aWo+zJ z|G-K-qigoMKH_&RJGQs%o+fDRJ6(^&PE6Ukj=t-Zoi23VN<_a11LG6%;2J*_nV&5O z9Ca+ku9vxfa;DNZ^pE;isq+-**u1UnV(VOx{IDBdZmC3Ka>sCeBv1sY%U9q8@nEe3 z!@-d$kuZ)kU2}uz92K3=2@*?46c=6>4%kp-lNBW#ZB#Pat#D2T_9aOmPs4f3amq-7 zI8wqsDQ%;yk(%1oO^a2Wie&ilWb&p^yYV`+HJ=>p}P9L65i;}th)Ps z>kFmgtnygtoEwm9YF0g!)gr&wSp5lbBkT26=cuo&l>PF{4}E^;K3LJDId;KT`?VV{ zF7D(C(62{ULs z__4LG`gpY&&6UmO>t?ysS)yNg^KE5S%j)_|OFMC4kfTtjg@(d7vvj;@44m2xEo)JH zSdCiu2dQ;iYdv!!?D(;8@K4}_*dR0jwcyjn+Uf?nZwFNeERV^3Yt+uIrGcy8uIy=1<~ zA#&Y;MB(pkLUWSU=I{ol5R8kF3&Z=dSAAD9;OtpY)%Tnqki%Ye^Y|uCIb*zTaG`a#z3R*-Q1!>%nU`o)iSr%T#%KeJaU844>=GJVXKKJ%ysZ>fs>)fq1GvA62Bx&LXuG= z`O+c|;_Oigwly+FWvf2zXQitcMpCt=ed!%lcgKyHbP!Eo<<0b9%^Rqj#`UK9-8VND z9AgHgl+BqmE4nl1s@E@mrE){>t0UAWp8L%HW~HeA+h{hG$W@c0d!Nt6 z$#p-Lv5 zpo!2y@oiBk4mZqtiK#?W!*rEom_xrrge9duaWNr&L(w>IIYjYNq1y3PAZxXFNlB7T z=)Ycud{2#@Q}ZH1M>!<$D#Yqi2@kN^<@{yH45ZlTFa`md@zW>dWyB-8sAX&ei|Y zmvcXhUf=6PbVkz~mDT4K_l=rEPJT`+)s1*vsbkIJ!9zo@_RiML^4uLCrk6S2Z^TEg zx;nSDZtfo@kdAK;H(XL*=zT{!R8qEe*zQR9>gM5QU2VP2uCeV3_@=WXMq06$e{j_T`SEoP6z&#m5Wv`%)sh;*eyoxFy{ksn?r%@8{ zoTKc{F2TgKzwHa+1F?%?4g|H}MoM1;2Nhy;3?E-`h=7l5qbWvDK=)d~MpE~ekF?dN zC5a^ty4sM;hoJ2cYlMdPs zh3@7x-fJ-QRa>s|TKE%7An|70$%SWn@FP22OHRT;g#Bjuho=xpwAUapxPo91bB zjK$5D-C``AA650&JZR29F;Rtd420(wwXDY1qHJOqIPoy5>sQ}NfJq3|$ic||M_`^vlZMX&2&T?w3`Jd)hyPSmmSY--G2nt?NqzndHDV zWmINnayW*?zv3fB|2F8{2n$p+XP-0oWRejT-7uERB&N~vpi-NQ9ND|oaPQ7Sjc?r? zkoZar-<7d_`oJb5N!^UKzo?|0Id)If@K~iVlkZt5^+K|0^g7+`C#%}AEAr|`&owG% ziqQ0Ys=BY5NZ0>xh$fB&f7kZoLpx0DMsup-y>!g1-(V!Iox|&nuv~;LUpT7*ue`eQ zV&%MD`jVQeGiX^mkUs-)R$6pC`^IPWS0g{ApYJ!?ey^=9K(EGRU?8j=tj!CEr498> z?D5(p-v38oAsoXK2Rs(`O7#7rTHqV}pf#%T6ku0W8Y1FP{ejipjPJ~$dp=Td!Ue58V8?JfK zj^{Hfwrk_cjt+hERsM$#`5j&&)nCyeuq)NmMx{Mn-1(s&e!zaK*7il7D%qKTjjWh& z6r!n~WHPClUI#0T%wYAOX$^cp^3=-88AdD@jb13HQ&BVP_|zDl45%?g@7LH%mQGs{ z^+!nvcJDfM^w|SNr`?N~ae8ke`QN=E9f_AoV%yga?e2AQc{@8YcygB!(-R$PeB;T_ zYO7!HSQK-xS}es=+BN#GKfYwC#tA?7#@oH@)@3;nh=j^jU@B1*2Sb@)=GwW z;PFNfjvt8)5Z{3H2#rgaFO!@My`Eud*OLHAO1}J0EE+~vm>UEqG#FvFo%cbON4lg! zG#;iTn)0GxYD_-+uKs%6=5#K;jdOh+E>I?L9qnCa|CG~vjd^rdy<uS_RV6~tg@`Bkk`s&VNrJiFrL(dfiGt>o_(B$ zl9IMc(D$pi-9VTHuZv$`f9dqKw)Wl1L93)4$z_92AKIWB$w!WQ``mQuOnx+-8Szi+ zCX=11ILo+gIEFgzuKLP3^VS8gvga-18}#h(X1}`QYBj$Ecf?3`wr7{te>B*>hlS%r zx+&WCjCgAG$4*5JYb?C5@q@;;KD^!t+Iyrv zIaLLC@;g4K!d`5VfxkiqWagxY+LFwiga=lILN{X5@wYma;rsa|cu=#o{!+H6k_<`2TF$F$h$j|XxA=T1jJc<#OA@`#ZmYQ|M( zi>1#g^E=tj!~go&B0B(HRC?l2jZ9~C(zGLwABPd>2~|mSKbkyLw7ZTcZ__e!H$1YQ zttEE3@GD)OZJsu5Z~I>d?XlXS?P|7x5x9!I?&{{rV$BThVMT_VZ;TGP#_GR0HdM+* zJJylNeYw2b>RtU!n801e1uoBp7u=y$zRhCP7JjRN!#}RanT^;i zTI~tWMw#VIr7x#d%x~ove1?ip#|#D@qiVD=3|o2aS{xr;ZKz)D{MNvH@g2Ww!OYzglVUex+irANK>HDtKc34#?~o zDxa+HxG@41nU?6@g^Ibw&sHcetKQPX<@}0WcZ<(L&%;=VIU7=;*){CB>|pvBa}@?W zkk_{TzUXqm9ql2>Ef7j{v~~WM`#xI9+A07%x_%H;!OY|&3mjre4!|@3)9|%pJFEriAh+s-`mGV&fTmg7%fqY0BgN=~yi19$t zxh51M>GTiuj;&+310{BrrA?Ww~0_QLIyJSs`Hw)%H}XeZd^cs28hFrtcnUsU+fH`g@eaup6n7f&nBSd+ zHfwTDC1Px&$!Vo$?;f(t=I`9@-nZpYflUX?n+vkW^#~=EpgC5s_r&6z!0OVRQ#4x49V1ou{=gOOj?SOmV{VS2nNP~Ijs}R zl5G zd1Jg#G0Q@Rt{$zzojZC@wj1P67T9cDQ!)<>M6I9RUrFTPFsP`hYN=KuEa!MR-x?fG zpO0L^N6Ri#sc=3dGjALV)sR+^s|xQ!EX!(biO$hX?qyG34|f~97OLMkhruH_$mTdc zfK%j0D1NA?;4=I?k^m1Aeww2ZrI%_ZATPXL?68d4XTwtqcTkCngA&uBI}ztDMjL?$ z)YyJENQW|&wO%c#`4?8&zov^Yd-3kI|j@k=;?FZym2xp ztIoGU-)Nt;U5}wDG6MeAEim-1zuWJz;#wpzpp33nJ>Bf>_ok{RDAw4%SNe|SSPe&6 zspRTwJ5QH@_)!&@g}#-By-0IB6sz`(c64LKX@Iq!qL2c*8*mu3%N%3b9=x6&4MX>KZq6z1IXIfK znQzuEeVdfq<_UvZCFOPEnce6;vcG*SV2&6J(Ca?=JJI`|>|2 z+NordBq{qtFIKbTqdwz|buFroGC{E@7TU32Qi9seM`tajOj6mxyW&h=$Id|RMX|+% z27r1-?SwHz@w@7uV)B=@%~f4h&TiNhOA=pBj8>GxR*SC`4%e-$WBf~}ZS-gpXXc78 z9nrOoR|jG5ZS9S(lP}G(EpZBYc!u2fw}`<2L~>l*WvL=ieQI*Q`8>4VVxZ}F4)t1; z9OEoi2>*idu$Qp09QCt)>OhPXHFK7fqIDp^$}}=itM}b|FCI zhvZJzJifScgfwjD4h2Z;zOed8BSmOUctnpbo*V&`dSWvYnPdQHQ=}5@1g@&n`6o{R zV^}?7a)!e(WhYlJAOTfY6>3B91-~eyO0kYZ|15;X_L3@AQXNHaPMH}pUy-eMKQ<%f zEK}As@L>(;MyeZ*W0RGG#@1qlDY~j-&cZ0wq}DK3Gx`!2j|MD~=+F{wA?aJ!Ue&Ar zz3Y2QU;mn^eXKgz6+p@7u%ffq?HF&utC>l?W%C@243+oqFD6p|5De6e@2HJ?sGo1z z<<|1l&Dd7h{dTJ7u7299S?0x(s^XU7ol|9UNu^!GJy-W(2GX!BZ`ft#CrsVFQiB@V zaXTVEKok7r!!Skn-I7fmp1U}2adyA+XT`oZnXzo?cVY>x@2e^COt$vFSbO*QxXSC^ zTYL61v-iy2GkeeMJ@=7jq>(hT#?sgtOJhs6WMo^uVFQ8)Y`}mWI~cIR1OrauBu=>| z5a1*PXh=g6lG2vk8fcP+G#A>Fnp4^)ZQ4&xnZDokjG*cJdH;J4 zZ9p2$>>24THh|I0TUhkRA~`lk3ZsCs#xiVn=%L3&5cKBY^?_7cKb zr@n31xp}p=jhZsYEOb6(wYl*F)m*g9E4H+1n|@IpRf8X#8ymRPiKYhn)vdo=%-lQ2 zdU30EZ2n-%I6{wZ_g}8EZ}`cYm>VCO$Sh24**dqd;p+PlpmVF;uej$XqgG+tIWM;H z)=e9NoyK-`@u>8YzcIBKIE|rOkz^sH-)i^g=tZzkpu5MKOy%#rsN>C*pUbf%wsm0B zZQ#P!dvq+;Ki*u<>}7qXn=iNu)dc5G`l-^>iNxuqZ+EJd*uV$b?3W&8WwpgG2X`xd zPJ#tq!XEac4E+ASf5aMy-UJcuL+MMDfF9!r@>B9({g=lIMDRlpwj95ZyS+qjS`J9g zQ>WGV5WbtjT=dMY;5~c@4`Cv!tZ&bekoOTpTCYD5 z*T8?Qnj-LC2zq*Mcz;Si8Zyeq69eK&o))9iF%gKvC)D9izvTuqdFixTv&kV@vs2Rp z(O~Z3XpCFxjq28=BQwkF2QSa&le4cK3)Q*hu(Xv^DDR!IDhDoz$5P!L7zygQ$B)G} zJwwsUFQk$?ZF?fatTa8|D!9s0$G)t9h=`lha?Ci1;fa=}?bg4%aZmhE)=xGXCeF<|>7C5kYA%toE;wUW z50eNa&YW`HEf?F#+gr}g9q`A>a_5ialzrRuN+Ldg5u0O&Kug9Nwaog>`>7g1@vQlU zVT+3T8HY--scfmN@7m?pDH$YEMPyTd982EJn9t(G6+Q}&<&RDevj*WqzJ1YHIJNdv zJfip;Zz;sj1Btfd?8&yzR@Lr!GMJme?YueT+ly8lm9(k3I(uGe>(D&mv*xq()=6G@ zP2UT>7$rG__9hZ|n04hKd#0kBSEY(4&P1e_FgZE6o-5(Vi7!22Rj9*dK}jf2 z^MAY>IWgD8fEgU(9gM%LB6_UT=&s|;gCvfE4nd%5bfEihw1ycs)~#aw69;0& zfj^;LuWG_32N#>A>rr~U%5_gx1G|{Yyty*-{GDpY8r95izV=u?kq%q)KANT6V8~SG z1#PL!8vbRkQ0f3CVF|_me9C+TEZgqeiYK~}S@S>Y`xA*wS+g@kZufMn3)z?A|@1Aal%nCPY{#yB3#VmR4xV1VH$FDW&kE$#w`KA_z})c@{s@D zzb?J$GFiQP<(UoBn+wOWVjH(F#7=F0!E{i>w#)dc9N4rC9BNfYMs8~S$n^DJ``c$uI zkxZnW-NpdW)nrwzYhN7+r*dA*S+jWlt_^molA-OwjZywf#w+>Az`)fXOx|S;d~fs*G;F{_s?#=u}%f)+QpY z>(_Yss5><~>Xf6yGu^3L5HX@TGo=!RXxd4~qBjkWCUQV3gnbE-eIVSK_#2b(qCnZJ!+cl+ba7k@QlSGM&1eIs2N&b zQnZyK2b^~?x}^DxZ^B=Wk?)3m6(+c^pcCth($S;&1j%`a$#3{(s4C#hGf3~_6t}qhc&wj&@9o5mw&RA4^}n0ze>mp{Glflqet9|>*;5^?r|iI+DWhBA9#A!u>Q~r%X$E@(#1<|8 z-b{8stIl4s+!rU0)DS1Be@7iJ3>`ddqfoQoip8E%?pURq9s1a%*n;Q9wKw%23|2@>Y1 zFYpABUngW~Mv|J8L?-xcK(@4z=*N3Vi2{VKQ%mL~p9aIhX!IzZRMExsh_d`ZC~ysX zHO=|zi>I)lhH>|PmPkKHcNR4oFtDmgz+lY(`ru3UQ#-r&{+V64uO41i+AtJLFeqQ4Kjq^^r+JT=oJUgR^(lhOylAA4 z#?Pp*HoY!iAo1e={+!)P_Iqo$tX0LKS#}j)=0DFM<5047w!z0ZD=QXFcM9tDyCLg9 zM&qHE_0&~6Ec+e=Zr7~I=G5MR{xaLgPF+>4*o)GCj&AHR_f~aL|H6zneB2I(Aw$DX ze(`f^@)NEq4oLi%E!!nD*BY~BCzG}V)_%-q6Y))kwTxr&QH$KV#eK-V>3qwKPw@P0 zWwf56eWPf}ek$_k3Q*Qr#xW6L- zgeWBerWd9x2~%?LK#^Cz<|g-<>IdIUGq*F^n*@2K!i}fU!hA4_^`Q+vKRP06v{J*C z#{`kA4wN)T&m#F72?y3Q8kLP2Va|6hGD<;^aLVBok@8F<2>-{)@p9R3q6X^?j+%bI zA6aL3PW=x)?A{kS7%BMu?e(eokqkKRArq~KK_Ser;4qg+CB9o6OAh6{VltkNmbUOIQpT4H8TZltfpqA}1l0#* z0xNdbh@fK0fQR~`lS^BUb6{<{STbU?eHo=Z{k&OpXi*k0-YnJa;3ugk;xGT_cvxz; zGwjqf{xQ}~Fq;=OM<^Ip+Q$~Xbb#EFc9KJ2t<)2>e z@7ms3pK{{Y-sh(}wYjPE`O6a>(~d@+*-w<*Ooyl-8kdq2%g&%dly0WLAbY5I9HV#e zhxiD6(<2AOeGKu9Ly*K{rXe$0@MSpc4kctpGjyjII-sl%;)cZ~a3jAejW4$@ePujO z8#ON)!nsTK3ppdVwQeNUpmr7SN(wH3m3P#VZ1UAA&q65BeJXlVT_meA(-M<(AE`V? zN?-Yy$vSlHNy55lAx%pSrVY=c-H;SksIrhQ=`hw?+&L9tC)jUW>U851cI8cZG!Nfb z`8Jalfoh#}{g!Bl?Nr-7EyV_vWIXjiK%S$nK`F#uu*{bq0EVj9W!!h~E!3Z{I?7vi zcqR)!M!nEG!Aq{7;BoHI?%^^`Zf4Y8XQ?UMF7_kYQiv=psomKy$i(PEjm``vKGA*7 zt&PtIY>k_?*cP%Wu39*M=sNq?Cen-7*|(+k-`ETiBT`6xK3$5Pr-r*fA>VedP<1zGJYQe2C*3vMQTBAZvPO$GM!$`v z-!Pn4?Wqu*84L4P3YP8{2n^>~mP}ULJx}W@X=#8#$5m_~bj; z6VRWqLuM1LU}3(oLX+-DF1560VQ}lV*ErYR1U4#Asyl4EQxys^blh$^pKdy}^wY1s zkvGKL6YYDA3vO*tM)ngQLCrmhEL1=+Z^4 zRc1y{lyVT-!$@sKyhyKAb`&p!a~c*%Dy3Yos~}u32IN{VIU=8xR`#A1cu;`4$cEn% z+#<+C+!I!#8#E#Aq`)WeMgWZbXbNorp@Fi3ntR40Kf}7kNW}3`;*;r~oB%DU4r86q z;6PwL-WGWsN+DlS>V-n8{~HUxxuNn?P0T3+xu9bAh53R#wu3U)LHF$k)IfUbvHNAS zj7omU9{W%py)BF^6)7{;!B*$CUc3=LK zQhJj=zi=k6I0^20?qTv2@h6-X-*dD;N!Sub7^HrWccG{zrzVwoeSB=*ZE*YIEu_pZ z3eWj0<7?BpX^Shtk!bJqUl3KBml%*xcetkB<*6v3CoAv8YPwhCU->jh#K~9|P*!j> zj2`2q*Tsu5KhbbT*eMqxY^kxCqKqEL8YC6O#~+o^RaHJJ@o&`MdL zhq_BIq^C}>iqt?y&lqu=FJGZXv-L;oj&G$8BkESjH;FiEbTj4pzaGvrMr19_$~YF4 zpxB^YH&Np?u9!29l%i$-Td4S*b!-em8Gq*_XNsj%zke`n6ymp(UB_+TdtENcWWRHc z8WgAHvz3!wBHWX1V~_bowvXJI80j2!9_KHiSNL>%;DiN~P~(w1Ra}PEmIfT*R(lzUf&N-}2#sMm{*cW+d;UG|TkMrhgg^7EA^tv9o&K zY_Uwz_=`4boD8fJw)2%tn(c)_)c4PZPG#Ld5FVpP{@wA2Up{&oK0?*)Yvb-b(L%X* zWCx;8jCGdG@Q{6S)~uYfJNxaS=rZob*qAy$QQTY3d~y#;!0H^^HqiedT9obKtAojl z7XvpwaAH_FwN(6!hd$%EM%~_aAUUd{38r7c8!A5ZRAn|WUR$O2E(ovwyVg-v>x|M= zRs{tu!|yxxLBF+inttDPG^xxjkz3zb;l8-?4V$M_V+s1M!-m;R_OkZrD2ceelz?%w zmsDmgiZd%MrS0We>V?DDOF%7Xsqb!?vVav4><}|wAzFzfa^vIHi6%e=>A8mR;N-Z4 zB{0ZlRL=YrJs<2q%#4DuJW=9~=&rtEPOAe_uT+9oy8(zE`Q9ie?ONiX^hBKWiD{ps z-vz*_DHGw`w8P`SbOp{t7;o+W9sg~&b0~{v{m;DE$d|kS$laUue9w-he}>E>lFaE+i9s?`!&>ftHQC4q_;=b-8{l1jlo+6+ikddo)d^OJR64T^wN{yi7z{ z#=q>J%d2Di+KWCiD%~f`@o^`SsCY?a_zNFm#O4jE_~=U6x>Xtdw0Sq&%N=*gE$>(d z0m6Q}oo;peg=6_@7LB#gHoi`+(R#?fWW;3=ti30h@T%!|^d~CUh$t&VpH(+F1{J@5 z&s-$=9SRVt-2J|L#oB)y9!_OHxn3;~Ezn`+6!OPBvwuB{7p%#3)wOS8V=E#F`DGiV zDf;gpB(E9354^7L%La>70^F?xcu62}0zH0H_)*~~Ij~hO6S=q^zuA+7m+$^Nzq!h4 zB1Q)D`rrA@K-Q-864ds*$l7)2w|;ez3K|GFp?Ts?Z0P^>;H0_wIe0t${#MGA@fx1J zKu*oG_1BQSuY6#hm6tb|0@~_V2+O7!S@{m8_rJcW|Mf&HZm~YDCj%c*3A^n+Icwuyy@>{GSRrw#%_veCkfGv4}m>4p4EOe=^%8>hN zeRJtO(1-?np*t9cIvS?xiV|gLsa2!jEW=v7jvaQQ^xH$SG6jNUEkFo6lL0gG;~v_v zfXni@&WK(uYzy9rNkNjpC9#TUP$SJs$q5{!v5xL*6fE5HVj4NYA@gCR)(*X^(J#M4 zonK@s^6oOp$a9k~<@*y1i)Y@mV7K1v)Gcrds}xsM@iG=p5a=%ZAiY9#9ErP9bKlOB z-Kq^+Fz=V&W8Z7va^u4D=}au`sTl~f_5Jly9FZb>Igyxr+wB{x^9AO1wkdP2US+Xd zDq*XL>d!RPt-Bje7)`v`Ibj}W#JZJDStA3;e^y=e#Vd>edQ2DDPj#A0O-7N(r-r3* z4Vj0GANxf$TPP%_84+SVt~$ZS3ToJPK>IS#M|M3ig1v-Tz)1Fjuy&2iURNFKodZ#t z$s@7FVb^ubE~fZ2Ji^4f)(ov27rcXXDLzo%|2RFm#%pVZ*0U+!IY>SIB44>?yfpS3 znUoVAYuE>dgZ*8CG3|^8k`9rq^Pe9ve*-Qjj(+1J{NACy$NHWps_7GiH4*&NI}%+r zjB(7mn zVjiy}W?__MIG8P5jUF`MC3M4ZWz3|<0&!WMi(N}0QZvjHCFKK%WwkpQjBF~R)S5q4 zA7indI{0zZeE+BOBo+;|<}!bUt;Ob!-{H^gr7}2$eC1cWRC{7a)83Bvchu;D?_NGT z6SCUQFSonDomrduSeSQR|8Uc<!51=WyY@|Hl6=JkOTC}g_-EEcN?(^@QqrI7+I5$Zx({3%Y z8TbC{P`YC>e9*qnx@`b3%t&wMvlA`#fdeX7L0oX;Zk3mnp>(-C*Kl^9U+Q-yimG_I z`y<=Ff6t?QAg6*YeY&v{TXVeaUOd%QmDgl$L`%DPf8K82!R9dDz#ehOH5Qh>hOb5b z9+mNpAe)C#A^cliO~i8mLAiH(0|g{3as%`4HtjBw3U7fwK^A)=U(Q0<$pT4@ba)Se z%=#(q*c>jjZh840nh?~sQTKqsifz6;Ofu8%9_yT|j2pWjw)Z-<`Ad+fF_tQJTThJ^}TnLHIOnCXR`nF+){&v*-J-j&63?OeSOA7xo_^lT7|mFq$mQW5_=p8T`c*hTA8z8Cu?^|Y-XoO7D& z>Y`oXZRVX7OF3=F_P?bLQx&kGgK#C~1%|@oYj&wtI_4yjw~YDO6bmtP)D>Dc(?n~h zudblL9ePUD*7NE7YwY+CyZ@~8 zjq8gEw*_4ovi9{lzg}@JzQ=aHku|IB$#r*}+fxBcfu9>_U4Dr^h8D_shx@L_#(q!R z7$HY;WzMF@=V2sD_aTtc2=$ArA^yy?IWEpw!h$aKU@&rq65Vv04a^%Ek@f(2>XOn# zBV-ApVLW3**p-pCgeiOKUGea)UF3g>B^p+lUBTJBwv0D8bDQAd;N<^M1F+`CyyIX8?r_4!*1N!k|Hjs90@ zQj!y)(^;U6PINg4y%`#Kz}eC`uU(L)V=yWSNa6~Fxzk1?g+h`ff)Iz;vjWMt@F&_7 z1p0GD&9(_cL(q|eB!y4hAwQ4?7V$0G7{nyyh4?h_kW%ZG>5o-A=EI2Or;b`PbTSps zD#yHK5LI?IIlG(R<=X3xu2|=NFi@uswdyQ4SK~?hlw`qnJ8s$5XJ7t-XP@*k#*1#_ zykV6q?)RPj6du-HyLP~Bp2#m)k$wR-IGg_S)$|7z!-4)Ks8WQBsU09*gi3KXV=rdk zTS^uR<*-cJ6kG4v=WQk9rk`5v>Svavek_P=KyY_@-1x9n%SCN(oUUs(n{uJ1=PR*WhYPm~ob$dKC-@WhBXRf5I z>blMfx(hkVtf5_JXlxddP8K?4Q*7Q??Echt;zR$sv10Z|qC;8p(=4WO?BHoS2yGNU zX^J}RX3w)z>FAc31xlN|p;@((D;Hol(x3k&^Yi$X*}iQk^c*02`+zXJ(rCbeNTom! zX9eQEG2UWGT~h2l}fR0Pk+V%IirhDqqoQITGfYv%}+ygA#*u{Uf=pvzyb z(3|U-UiLH1!zY&8ho0YadddIwL6)#dohQ`>#1kVSphXYkkbj zrs9k|rW0|uUW~aXT8_s6SewBN%UaS+R4iYWaVf?;p>l(;V3$3|@#haW3JAlx@OqxQ z;3&>(%hp`YO6qq1#_!EpKX{;8Pes0OH9hKM;h{g4-7%_s@I*O$!zRvN^vr&jp8i_j zINMQvrSFeHuK08vO$1i*r-jicJm`r<$t0veBW-6AJ9@owf>;14uZk*2Ot2u{2sofr z{X(pQGi-=E=%ErJRC){zjBjl9jp(=>YLNV_R-(9y5O;F;v~fxmNB}H`!v~OLMvrm6 zicT;X@l4tRWL9gHaumlV209u!F%?f8slWT-(NeMd*x_A?^4Ol^{PnYgLo(IW*l%?Y zG32phGcz{(pI9(|C0Aq1L?YUlH!Nbo49Ei=NbZ~2dKeys2~O`Q(Uu@9=LeGC__bQK zv4>{6E7I;un+Au2J!(BYQFw8$LEB1^7ODSZ4fR*l-P6^=*~;d3v7r6*dD*3#ww|-A z%`&ih`bgP)nJ~lpDE10lH5|MdrK#91jFnKyfyvtxV-WbNOo&d=X=$WvBp<0l5}dJ} zGM~b?vL-{M4L0^>*13%t+WJ*v2mu?9*?rcHfUBOe>#A~P71EDoJDFAipqq+*b@R2V zQ7yaQG418nvKgfM^UB_KDjPf3t~NiCQ7?eINWd=5Qo%03_O|Jn`(O{{ zlIF^-jEY5PTG~-Kl@4-tBfXFT@Zg7JyE10Sj20)Vg+qCq+n%t|yV-TRtCN^l4qli& zZhXPJI(7sSIXGcJ1Xjw6!+J&l2KPLKuEa7@Dv2;OOX|QR!HrPTplg76KkZN?QNsA> zxS7UhSg(wiU^yDnivN-nh_@CUcQIJGq7c9{#-Gxj-Wk)h8VDwZ?{h#M;tWt6n;r&+ zY|HC8G21Xk#)2g((wnwe>M5nJMCeUDCDTa{DYrAo;9uh?CmNcUjI^;j&wuT9i_&p; z+zEW9Mydll84|;44c60GR;$~MW-c`PDrU7B{w!%nsa;4&@Q)GnaDO8SY9f-?MkOs0LKUA!f5A;!S(S5*+FQZ zt{~79w|1;;jz*a%kzMA!nnFd-imf3cEz+I&uD)OD`|H)5n!A~kiJ6*QA>0g2n-Xfs zlI5^CF^+kt!@oE}!4l##_=&DXVm3Gl0ksl@V2s5D#d_!kucN<{T#0(6aL8>bL|_)Y zi-s^5%+EDHU@78*#E4|b5MYht>-GF%m@LV~#BhMvwJ!BEG4nzw5nJ#^GCgS-O46=h;h!jGx^;k^d=uhe3hn03@+aHq=(riog7_ zqcA~+r!RlgK>wcWO6kxaD6&IgT{TQ5c2aT@ucZ!o&2GM>s9!D6S7Buan{dpA@!auP zx_?tF8~t}@IJ(a+%r9grZx#K4nt>7Llxh&>ukQPl}RUC*VjZ}P;-th76&yq2taEThWirLh+O^REMOk%l$R#4L! zVJKP+2X&UO6dJ)D{{}jftK>r(#0!RhSGj+bR^(|)^Y&ndJH>MM@72BFpESRTerE!% z<~6b_AP`&vb1N1Bz|=^DL#>NrlJr{VI#`$_ED~PuGWe8f$!z#JmLVB2sWUEuB7)ce zvS_lW3S2D4N;DJ&2ux8H=wJbYq=-~tXP6-gaBUqXTRe(|knIvgsi!o0WeLzNrcLkvQ;YjC7Vq!Z zuTk~=o9{NYAP{tS_|+k`a9S~1mzc;`*6p+XCI#;0_|)vBmnBE+;S=h$H-{9+*FB<6 zeY<*r+j-C}PQQ^tSZ?HWwRP#JUD~y|f6!d9&bAT5AE8QqU@v|niXP1S*^@Kcx1BD# zkM{koF$sR{(8aAQ3AEDnv{`yR(yLo?UKNY~|LDzL?Z=Z2YQq{}AI|EU+DLeMZ{jt0 zJJF57a0KB}*T9wV82_)U6kN8ENW6dJM1QMr(t`VUW2wO{ zJGM?*K?a}}kxnP!o)Zy2nut?k7>Pzv(JAICj!pfg=uzUh%&J#K8F?(@4bGiEm2~@) zgZp;ma%C%(jL=z8^Hbj1^wq{p|-O_ma_GkJQBGDfy&CJfWC1``xdq@?3-w5ZD% zPEa&{Xe5$M>PBKwFZDpORxXtD7A2wLh^%vV-0h_9seR4kY31%`++ISw$)%Th8P zj;-EZyoet*v}_VL5YCU~(Yggz+F2VLqIgn0Pr<)d75;gg@J@^QYYNx zpqqvru|~bG53WzuW+N$YZ0MbSvb;GSq%X_H?uuCvZ#)*8yk&b5vS{t#MH?qB8QPHc zUq4yt{`tryyHbaziw(p0U?S!DQ6p3E-R?_~Oll~SFmt8NKmMunSO2%#R6mMiNK`6I z>GRemXIg&e9YcVB8praf?T0AwS*g;#f)%bQM@=U;;_tntMpMgZsWgA!nt4iuad+kT z_Wu#vx+Pk;cGIM37|}V0@2O?k7g09zVGs+0YpoNG)E?7jVjY zX_n@-B`=U2SERoZ<0Y13k~+2RM5Hj*o{6w&7LNM&7MiIvDOIbw=90UAQB}(49wnUZ ze*3236MJbkZ*M93^>^cS9ywv9&sa|>>qYg<)Jx45l&l!+fB8qw8D-yd@!aCsGP6np z+reHlHKz&qfpJt@Ka?3BL#c8~7E~<=w`2wBTvOFe<74+SYg7G6|A7sm+g|rt(lgJpCQ@PE9vbtaGs);%=nRUcvKiMs z+;Ngj`opKA`=GY&Qu*B*8O3s)^!BUK^|nYdyp|K@Cy%g6Xl8?j`iT2`8HkL0NSCA4+;9sN{=(E^IjolNAeW&`~ z)c57S?`sB057P3dCM)a-G)Ns&a;)^IU?b_cVBG|`n5h?Gn+Z-rDMR`IrvPQNh45o| zNS(_`A+rJ6BFEzz)X#Xiq=#TF36zvdgiDl)st^*Kv~CIE@;oXSPQYBR-;}pqt(EB% z0psB*0@ZmM_NhgF*^m{ zR8eO(*y>nddFf@=2nTid>`?%#C|7alKNA+u*>*H)sXe8!@{*ey^c%C`zEMA-*wS#X{4phm# zd(-_E?Xk3oTAw*g-dI`g^&ox>yIR{fjo;bHK3J)E2&Rsrrzl;Y8m|iziAm|SU4Xp! z9Kp}4?aKT>Ph9orB>DkK0g-#sUPb!Pa9~1L;hK0Q#?tP|>WY~P2C|4OUOQF}@2kcpRP%J} zIP?(0PTda-kHaa%(OCOb#Z!-@)y~j!7hDxz&oZgO)aMSxjLYk&clzUT6Xx-m_V`g( zwVuErzwX#K-!n;%65VZJ{=wd>I-mDO(bn}VaGxr5_L$jX83e}jI-9mw-%DuB+v03mmoG9APbP8OU z4G)HnIm?bIciDEu2Q~lm575w-+%Cr~#f|I@`%`T#5}`(! zOj`&x#TPn*>~)MVZq5(1W5P`MzgI;;B~oErt6@r52BEyQ*W}H_-ASZt%tXHBbt?B* zmF34ypvbUiU)r|!efHiEf_Y^hb93@f(4_04K3j<7N(H-iz-9d#|FkW)*;?mVQ^U-U z&$=J>8pZ+D9d^8v?oHQErNbXXrwW z)@C1Zvm2Wp|JvB&GMcwSzUGEb#u`cXu<&R9B;cu^+R>z-MbEWBiX9W_p%Bng4 zBUc?8SwpuPdXpD2SvHa@EFePXjBc(LELs#VMx8)vA;Ld>9_`=*eMgyhW+5>p%UC4`55PYa?r{E0XrIJ=%5(Dt6&BZoXj^1~XoydxfL6tY=0c7fH14 zw7qNWm#571)Casu#`8mX?tZ4Qs;X7Dy-P2DzTAMZz+{kXp0L{x zqRqxs#lLC8gVD=WC2}=m8inHp=P>PNN5;|UxuZEy9DA#BAK0$)XcV~9S1D#`!n0_I zqlBTJt2&E2TTn>bccbmUt}?1-e}zzoA;+uv;VNJzr0x z*GGP7%L$A1dFyFx%A?jdUQva7CcelJGs8D))TwB!GCwN*CQi*>{7qc_uKW>9kr_Z1Y>mQ!P;xm-A_99S@$j1X2(ZAoJpdaxLDB3AJ$}pS}Y4x zwy!k`hfZ(VuvR!w`6ozHbP?tIAUQyj7tPg?M>qYVt-b*O>HgVx^6RhZj|8!7q<`lZ zTyqRz78>qPB;T4z4_+IDcdAyN><#p6A6MnFGk4tUgt2(VQFGN?;^q$xrq~&hE=_L_ zq9kXDgsQ|cU#ahDHI;k6x*qLEh7bu|o)^nL3G0NQ=dMhIhgl8pRZfc(7hYp-ygxC+ zYh4nBp<**1ORb$iCGL_n4_UF~AHKOT>?BLs&#{YV!;nK&Ko*C%skmCH(Ugr~Zn^AU zH;O2-=;3}69<)Nnp6)C6{hhX=o|`Ao$Wc?r;wBd_BK|jBa41j21rk)TzQ{RyCYGLi zLN)ie4FKg;G@Ay-+Z9+5vGe3ynWAUA^>lME1!i?xS1vNGgZdGg>76C_thHuC0SuwS zBROPDSrFp}{xyra;>mn0Mx`t~+S|_xnclQ#`_0N8b#RG6sXrUeW>DK@s}*J!xIW$| zsb&r>mgCHL&4$4gwUp!Kvf7$^*I?C7WS*aErW4!7eRtTIzEE8jiC!c_ea&kgPi(#_ zk>mXBg;C#$KU69vqTRo3F0BXlFri-j-Mdr8cTL29`w2UL+SN>6^T>!m-PKiJgda^yR z#dF&%^wh>DrCbR_LMDm9g2|zcq%hQWzx38W%rl<`LpdYcY~Kg<8^*&@X*J< ze<(cs8f71=c0#mP)$0>#?iqW!=ufHxE9LFix@}e>+2zr#Vbr*B78TXz{$U5F79Bl* z4g0)XA)C=`zcQI32MYcb>56B%+fl4E++;hAJYfwjssFaqb=dEc(i#Y=JX zijuutzhHL9F5knDkp1`FbV@RLVtoc{WF@5W9l_JYplfMI3J!8(?f^?UxaLuPzu-)FCvzz8|9!1u+D5K zKgM3_*@jq?E-Q=SU}HQ2beB*jeJ1!hAeeTPQoEa0*ccWgFXb{~ar}>50DErtuDwc- zhEb}2QaKiD>oJv>n1oUO!v)0b#AE7=6W1(SXQ# z{aYCqBQa~L(+E!<8Byd6v+eWK5(?Gkqjg4j1Xc-1O&YHF&Bp=g)0TINMsv zKLT<`9d}M`C=uS&yg5Uu8x60P+OHl(9+e78#bqPCI!K?3sVbffJ41pEdpJ76EV9OQ zhL_?6GCuof(4PGf^;iX(gJmU2JDsW*`~k#hU<{W0L>7}K9p>u!>XxD_HRjAlJC~(d z4zU`hp&|*zkoQp0Nig0*8toKSLelDTF6}ovGjB=S>c9=jn(B;SfNTJN9u`Fv77Jni z^1R~*Q)5iq@ln;*0dye?4K=gQsre0_*8;(kgti*kch9Z|*k$n><{TZI7JUz$zBvOO zp=f8?YOvA;-^ONEk;i*=`stOyDWs};)jX}ZAmtNq7}HDaT@tpFApT0Fk0QwyU6|2N zVagx$7{SVW)f%jBrXzDy`lr0|l`$*SMrNpzl6~kIETX z@CcJVtin?DKx_6Lp~i4SJGzd*r7c~PVL`}ZZEH2yOh=K4py3@YTM?9NTK z!RkmfUdSN_MJ>igyy&LP{MNK%x#Puw2k-Xvz*Tvj6zABoUZ|K8o|W?5-r$zOmYemT zviyGLwu*T@wPi>BA!fFAq~gBCri&mIjYs1C+;M~b6zNIhY8DAb6SeS3<7@H6sE-me z+fXv@7mQ7Qz${ge$}=~3Al8m;vf9>My!%H6Wy1ACX+L7+oLmalz59E7(pn`MjX|R# zjQ=v*W}ibXi#w;9aH*t63YEGtN{NWc%v5aJTL17DpWa7>Fg)*)?6&EFFa~yBowIU% z8M89pYC_lBDLXEbvv=hJ6Fmfvxh*ppeRt7yB6?~oQsb-G^Z6)aV`}encQu*=al7BT zZ7>@{Nz0q!y^H}T5s6QNx!sI0+DX{S`nWp<6<~OQnTWV-W|5qxLm%?j;A}Ifn;f7T z@P@u~=zc!Y_c!9Lp)9nA6(@1@WP-cu1!@^uHu-=YRD#EozNXFXd!dKVYVO{!UE=Pkud*RfUM zxW&+e^FsHl)CS@-n6n#nsT_PDm-REx=E+Hzs^Oof*tVrSoK@346PU+UWIc_-dO7K2 zegW-*YTC5IhAKw~SgYG67joNkemNC6!Jz!QsS~ZmoytAaYO=ljsD0RKF#6@zZaQI7 z=bC2+pZv=XX>c_a0Q;8NNh~1M`EA71RA5FX*sL8lM!Byc~F() zYC7^`yx1m2g|TQw6?s5DhvZkdTJWamMoI&bWXo7Sb|)WLV*kt_pN`B%t07?3C|&?s zxKyuhg$Z*7$+Y*bEiYaJU^G&h5QbrL#f!}Mz;>Nm+e>*y3=l*LlishT`}>a zN;EJlpC6!MhaJS3wcIcP(twM@Hb=5gHCt>!~#+`bi_dsckz_dcuycFBkYiP!=kmpRk371CT}2D zB(s9CB2y>f86h#l?dhbRm#=2`!Xav(r`@$APEs1v_lB6GfLZt=d7NwuC&?A`ogws( z7%Ugkox5CEKD9hJYW#<-E@sGY;D9QF?(FKpXVi+!oN}_t9-0fuW-2E>kxt-x@?TNv z8?AJ%3vuMy{VDtDTFDvsbql$pkRhd5*=aHL_wp~RV21O#5U>JE0$J=S^{~yDph~>F zue3@*o*vEbg>CKaaf7n}_-%m2-YBoByKg zHWpEiPb99selW8p?W5a~Pg>c;n&MB@kOAWT?IHF;BV3T)vWCKnN;~&gDGs`hze3Gk zrD<25y9PV7@S)#CyQ4@hFB`LO@4KV#pNx{!IpkQRgAs|_p1Z?F$e%C>nH>@{!NurS zNlsV$H&Pu5%Dt!!c_@c2UP+>L!@_^j?uIprWst_a+DeNIkFJy8`J|y6yCA0XgrVt% zxK8~+i2uBcEcp?RT1&e~@2;+O@b}PfES&{C7DB)QAC3%3076#3H}@0In$}TU5H{m%gp zZ@4{d-Z)TYR8TFao$MealU*-3*iw2(K1#?d4_+6X?H^)rAF?PBi|1TYvsB9X50xBB zogd7oGaYPNRd8z&+>SK#kS!n@(+cNQA`wraj>{}k(n^#4^P?tdIvET!BHw05=S>)F zELa&)w(9P0G~2VyE6vbfgn_zJ)l_S)NiOIvDdR3y=&~Vq*tAau2kq--l=qyiPG2`} zJ3~j*0=t89C^EY%NHDgSDqeub$gT`}WxQ3VJhZ{CsY;XGfPzF5D2JG-{_ao8#*VU0 z8Ls|fwKcuVN^iapb(GQJ%;-q7FhK8uHIeR~P*KnBzpQ=cI_{GWI@7EwY77+OR)@Wm zt(mdeROHE_jSSeJRP@NM4~C!W{1=$}Gh z6zP2vI6$Jej>p^txLfckH<$Qkfem@Z^>K)+1!1|CbQwtC#uba$jqv6A&giwTfQfZM zN20$NHctRKa93#z!7ZZsFGvM3Up#b#zqGo49`yGoB^!LM)Ownf?2mpvR|)doxt-V0 zbwR+)+pVpicU#SuUn^tPo;|(}u`rZa|7w472CwKu`cFk2%N}NN+snUH; z^^7T0+3wA1c)Sth$6Codlsz^Ti~D=7v}0o|d!8=BoO9(_J|)_&zP~g4hjiVI#Y~EV zZo~CnJj5{Q>oUKhJ}p6CogB4pm{7A-@@;>9{Vt@N7}|@MT$)%s&xvQ257+5OIcW(f zR`zn^0uy*aiHAK@v*oYGM-gXQ*L}%-d_U?-+%R4`2;ciygB1y0b~wIbxBU5I+?0v! zrw|+1BeTkkN|=cJEZOfGiwZwgCGYtiVtWdHs)m}xw9a@WjRBKs&q-I3rh;_EM%-sd z3MJ$xxYS;05cH>mGVZSj>i7bx8yd1VshgFr1rD;eh~!8jber#1gZ&1X!s6nDe(|=F z@n91I1bTy54QL^$=SAw6C{N^T!nLN}JG}u-Hk^%O`aM=u^!vmwOR&bev0-h(e5oCL zdnQdtmtQPD61mcit+!=EpR2A#GT;UCUWo0@spXgd)hisZZ!|!f>Zgnn{f7}W!^&^7 zd2Gfn)4G57{(RxZkp19KHGN+le1c+zZw76rrGk38RLD1i!SjgVLuvY55Tmoz*&vtR zx}$CX${Ef_s_ve z5_zpNOg=?ZbC&btd3Q6aTHm;(Xy$N6xj*xa+>KZQ}!>=le@fnrRHk$fOfL zZP<-xbE$N<>*tXLTs~SqoV?+oMAkj~^8fVISye~r>7Ra{bZ|MHJTl0GL!#MyR z9>Z&8vZVD^+x-H>rPHq8%NW$stY;VYmMY4t`{NUt!hm}nu#YzVqHXL+AF+aE2s)y;oYCijl?`zy*XjE4E^Sj6dcIxKD|CUO|n z-yfWb+n+Q(K*8sX>*k73jiYaj4Z;GsY_35z1UMn+1r5(kc#5Ep|T#3XD<2BEWYp#pVQv&wR#H7*?x z*pW~-gsnP3RFVjp`ITgy%ks507wxZzi8wFbbk#St#UDv0FdswUr}ms;LxLt@g%%76 zh{Df^I~0ETa>IzK=DS~Y8vuFiY{NDRz?FNSF1ud@WRW7f0s372 zHmF;#vhzVaO|7I#$^L0IxW!J6TDBQQkuSP*=&i%{`=qg8i76x}mCZkS{=m%`u3cvg z)E>?)HFchR_*8L_;lL&HTZ4(=V2OMb=qDX7wM>+2vzVPi@9QVBDaSRVtYUJ>cw&R> z{Y3P$h+Fk)nJ*zg@H^E4C6bLX2Bs`EW2;&Dtc)9v6zr$$#Wx#Aj*PNNd9n5GDG&f% zEx{hoLDUaX$vS$t)mS{t42BF{$!#;SaG-wVd@FwCLi8;v|D5SPai4n67V~|seKxZz zR$O|lWv{d{ub)Su2R=uH>Fghzci;m1=E>vi`|()bF#^H!y%zd86Zmz6ZtEy%{1gtr z$sgTW56%7?yn3K^4T>3&K zMScHl?29|Ua%CmxuGrj2QCmB!dc0Sw4W6|Nc3|B zH0m1w`<|A&Kwh+(ee;I$sq?~)^QknA1EkD0B){R4@l-w}j#$QCdsQX5taiHmn$sfc z0~s&k4Z<<;PXG1k#Kr-S(>%k<^SjDTsb3?n7x3!e&+@U2*4r#fd1idC z8UvL>E>fLX9vP%M4uzH-sOd zysm~WtykyGEs6ZPlJELMZ>wyhv|M28@ab(Z+Y<>lGpH`l<>J9kc3P?O@MJW#Yf#Kd zh2MWLNKI$@>vN~RlZ6aSWi9r;h%|y5H{PJC+jcUGz1UIPRIt-e-~xyDMLo8YY~Nk3 z?{O0wvx$)j)!0d6U^utvE_;>_e%`AF2IHm6Vv#!#YBnqRwNq+O-mWzoD?>&udX^NM zIU$r>ohJ)+e0H%`CNAgVBYN`CZalI1O{!kED$lO(#QLi@d`XovMNgT-x9+UJW;;bB zsHDT(3pGDPDkQZl!k*O3*30A`q~EM^9i321q-fdO0XdVb38_}C(7QlcPCA!3!Y^i6 zFBFH|At@3;0!xC(KW?1LzdPS)M5^(U&4eC|NV9C!Zk%vEuy1>T2I|mP@k~AN+5Qvm z3lYA0+=|CbBbU+XP=D)2yhnv7&~i_?wX0<|PWaN#>-~#N*Mc3kqw#Pl9s2k5{eIv7 z20@6)izaFZjuVq%_m%eP66aT^IEEUS6t*c#`?N84VB)EE@-V4WkHhCS$YSKW|6cA> zfwiR6d3}$K0!Rf{2o%u?c+bJ-r=+^CX-mPu{0Mj=^ua2=)FDRrPcANZncS{|Ht^*e zWH21=NvB?Nm-EqxIl?>%>WQ(9N3#P|0xY@$XmRB8x2GayJC|O2&#bz3t&yXgmw02L zr8-l-H@*Gp>s6TF@>#7TcC3-UVQL%gBcD<1V%t*)!%16pPbzPYtmBr*Ee*&)zyGpR z_H1Plna2q`%wLkCC?8DxyQnRjmtWew6{6^u3x4S5M#{j>W^;jKuI=c*!4IAao@veG zhgrU~KxJ~$WVsrR++C&b zZ(iees&-?AA#bQPshz4B(?XBtGiBF1=cM`_HMy?dY#vRp3ug21redl|--wZ5DtBpl z;tCRZWXRMcdma_@#9dQQVpA+v$VyHwwN3yn&_PU{=Mi1- z_jv7IO^Uhj9>A4HbjXlk!;8fGqgTM^#ohAZdx<>17x=cJ6R?@-ytrpBXne`A(l@!c zF56yMSv&+6WEaZsQ$Yt&*jB+FPE4wOD7?2(n)Ye%QGdu8E!*}s86yOwXGW>4Y#c%>XZjJ8rsZCkQn z(bYg!J?gsKcmw`|9o{HCQx5*tclrm&8AULUj$b6n@n`v5Ru$r=+V-IRJbXK86e@os z1D-Wc^N4d569cp!S%(NK_u2?>h4XgU*jXHk;X`)20Xm~Q_pxMf?+$Bt;}-jP-mRz; zC+xRfdCl0e&u5-P9Wj`5pN7`ADtWrw8AQICtuS-d5LMO1W!EofWB%Y)8eno|+UjNx*VWh!@W@E|N7_L9 zYVb4X*`s?{MpJ(wKyfF5CL#M+4{1Lsd&j>-?_8Xo-LbxP+E+@Rb%!vcy>3jgB%Q)* zGY}iopPGBKwyiCwWyC+8whOQXfw#ky_vYmL#q& zFH2zLAs$~fMEn}101a8NJN?vG^KlW*;ogy(fKMa%0Md%IUn0J&_24^vFOXcaGY#c= zAF)@S!mrb-ZpqUd+zQff*A|tpRf8WDrXfpf2{M-AaP|Gg{UB76*t-B|bEAK|QSQG1 zdL2``G)JFpz%C$>DcHP;$0ob?ffvUv2jejfS;tJEdiBmKaDm1?)uaJKZTN#jRN zH=b(NE8h5UIh8dlo?U$ZVCIUe)tS1ArbL0Q`=dp3rEaY|&$cED=}h1pZ7!BGo9XtZ zlY=(fu zZAoKmY>lL`EnBiBTe77@c4S*goY=9Q*oi~zkT_0oJ|HFu`3`ZuNN4~P0u7MTv;;_- zv;jh!wkf4OIj3+g^iawzw9ry6?dd(GcsRfHj-cGH=Q+%Z3eum2M9 zS{IoU_F6jiQ)qZ0p%ezSs4~7XV@B6hA;8dE$)~ot_VK%8$;4SJ_`2|gDd*QAt4tHp z8M#%H{;YjmO(O)EG}?nB^yJq-D~)dLEB9~PH5RoF)YP-dLRlo}JZJNeN;F@j4h4WJ zpYFDA9qtJK?3P&KuIpWseHMK^$t0uhESs2BrPI(shTIdGSmNeaj#gr!YN@>H(9%BJ z4o@x|1R+^dJzJ9Y$=pCR8v7|UK5P1$#dCRd%TB63ta}_JAUhb37~NR6B;~?Jk_tIiC!tgWR2}dI3&@w4 z%@m^{jjtIvOUPDwXitoQulG{e?1m?@OI4OZdU(q%@7%KQR%+p87^?zbLrn!By{TVk~7z9gt}d1S9G?e;kMb&^3zCtwY$=&e$Bx0pm|ZHlom+}K3~NiO zlw{3cC77OPLJG6GEN<`pW}$n&wKtTl+{*OqV24jDbC#XmNqa&F^~rKJa)a%UobF21 zQXO?wzB-*us3ZGQ@ug(6yZc0kk-lm$)44sbHh1`HtS4=_!~RmqX2M>u!K^O|hC2nQ z>J28#Rg3x0Sm>pB!8ASB*moR=TH!@X7pZvlXl=uqgzE(NOYt=xZtrr~zSb3vuHKvo z+F^D_Lc4+vNFBv4FjeTfp&oi=KVoVv`E(M&0O$K7-TVsdvjx`DT5~&UbYYD^h4S2d z;VT(Bz5W~QVKvcI`@sDEAl3?;cffAA!O635& zfUCuj?zY*_E#qNF01r`!;ngP~D(!{BmGRz?h~y^Ff5&ptlt9d2hxEt6lSR5#+xw%L8jLG~#7PcTO0-Ve&i zg_}RE&0(9iJvAC}q4u3Ey^hPyns=T5DYY50rjF-&v*%(tOR)>JFBGdJAy?bQ0(nR} zW>-uo&nTq1BbvUV_QzE7`>L}GElFz?oiWRng>p3CEbOOX{5y=+2jMlg`xga8fY}T3~2AJ!MB}y?`5-@fP+=FPXSROwRj<5xw z8w-vexO}Z;+SmHiDC8s#UFo5@q=LEhRytluWkd>A@}4wJXCx+r6>@U>rM#WKzUD;9 zXMD4K=iIcNI53pzs@&n)6~}%2=H=>u^|YvD7Bf3ibk$K0B<##`gJtE}1!K|Al68LM za9!7#pEG}l`N=T){M~I;vc(TEi}I?mmRkZo8T0G?f_KRCWIkVP`Nk)U8{`;`kLkeFM*oDturU52oFjz96G9&N;e;Rzpt zQLsI+JlN(Kx5NS6pVL=T0+&t&q+T=H2jbx#0N%k@pdP_Y+%wuQQUGemMI|P2i-G$R zY~*9Mt`hhP*w0()2|TLTRHs{gYa#6b-<6?KpI zB2V8s=02>{LVrH-H1=Ek%|i1-Npv7-`jhB${Hr6=4g55^vt8k2RJs4E!b5W&Tg0vC z6TWI97MQr!vDO>^xhEAhw+$BT7s2RA#D@_2p?c^~b>)SFHDO6MS~81s_7xFTYyNy7 zL8H3SK$|&@ex*vqt%mo}QYPuM$9eW1;==T_)tzyUpi##SJPcEV7{R}qHhXJIzO%r< zml}Z|lg@Nk^A=velga5j&7lMGb=Nkz1!be zvFDD-t0wbiac>=xQYpF%BJo<_%TN4Lp5E4~#A~JRC>sIn2!z&g1w;4c#(8=$7px+6PdtNrO zHURTH+2;QIY4cfAwOz&(-vh*sPoePrt+szRT;t2e9~ysR{F(8ejK4Jgo9sIfD{gJ< z8$%ID*ssF@f!H1vl9P3?*ci)O>my>r!hQXfMF*@O(2ERY0l_aAxbR>zqEm#jknzg} z1tu5amn>Yowth|h%9bC@_F2FDrU(F07X6?q|^#0UT7vpl&k|DVsxcwHvnm$&w1vx2;atPqq}ke`siSJt%T@r?n!^mc^< zjM$NUi^r2;tb9vyYE)zTe)T5}$}i7Y`RRd`Eh_WMZ=0ceNXX_G%XvMj2KxE>cfm0T zwO9@U+HnTDvCkvqf8NTUy)63<(XaVp(ZC9kNHYw|>yb#J_DGa9z=oSmL^ra{5E&uM zDY?50Fj*oJUc2MM35MsrNRnX-6$`{|hVlJ9w?_~EnGudU>!(H;dn(?EN)Uv8^!ZM3U^94TnmGIvUBQEF_g!(L%7XqM?9_dWq&6zxw4> z3~2<+P%-(wNGilK-T*A;FhWb_sD3<8jV7C4`qgI)x1Lc*yh|CmVm1^BBTCdbw!5xk zS?JU@bKU58O;?P$XvF5HM}WzURHI?HbA2_~5jE4OAlt|e1+7p%ZR`k^R3Ms)gqlBz zv#%i$3KxRGvZ@6RiSOC5co-`P?IpJ{F;*VCyh5T*@*Q9Mec#H&4 z1(We)$PPYrW4=GiTKHlZIVg-LGqY#8KjkHZ!Gr^&o3b|$!VFUc4X$!y7~1aOIy&hM zb1f78uLjqsA!?JoBM1wJ5-t7l8uh%E#!Hp#QIr{fz12k&>Y>i?2)tC_0JqYEmX0p# zjpaX5{bT(ZS+ARKHD=#B3tU_&8{@BU`>AB1&VbID3@4+|#YOMPz4_a#?R~d(mMMauCLgwpJupv~AY?7u@${Om!L41DyMJd#k(O@|eS=TH z`x_u1D!?T;)AmK87KjdLF9+mnGzN3CL5c&28IxL zP?Z#CF$1t1M=oD&_&R~-+q|zo(yY=}M8tU)=XHWhg>65>A65R0sU9$1owdyDUgbb1 zQlE{gGmt#3&9Ish?)MtAvBVHFx2k3ak4xr|scqm_KVyUz5O{B1TkH?^tHMHojti8v zCzRkG>*a%;>XGYI&sCFEWaX0fypNx`WuMx>*5T7Mg}_(x<``Eld?UjD{B=sb5E+a& z|2vXQ8>sL&_3Zn5Q~8e^NH$QpUXj6(XD5m`eyd0Z9f z_WRT|e}`VGS(YXC0fvG3zp1q;r6uvg^+ULKc)b4RL z6POukK3Xm^RDG;@BgnXRV6{;iJV>GV(dJvztb20(9+pzn`f1v|0|t=&;0JdPmFCT% zts90JfQYY?ZliG$D)5-SXfLh)PW_IB{pmq-)&KU%cqE!nW^*tseJL6`3B$RqGh}bD zCT#mit{7)={3J7Cp+Sr84^kN~hTrLyABZ|bxuPh5842Aj5LZ~FUgp6jwONDeHEN!TDoAGTp<@#&N?eJAemZ7wp!J4*0T0Rn#iyn zDX4KLIltDOTxq7s>2V=N5<>3dFAt9!-s;A2a=iT4lugQVrSUjoMu}L-BgQnZO4?7F za~I0Jc$({RD}u38_9TEK#=Zo8b!G+Jv1*@s!x?wxtI=zrO2>~?)Sf|xN=paYdzANV zGSRHG_xNgfUBNB#n;W=BMQ`TyYPAQqs-@{m4VJl%+d~Nz-w2n&xV}mSZ6>P5;}6u& zdZih|nhddSF>jw{8imALLC&(zxn9p_?mzjdQ}yy0W3*CdigEXs7Z_8tR(&Dr7Q&H2 zvgsr@tOFDEhm~P})rN$T_-2uv5DRwYisoM(ez|&>W34Iw3vQia;x+@n{m3r4aJ+@7!GI>RA=w$WeAMoc?Zy4fn4o)i+j~(WLi;S2HD#_+TYxCU0+lce zeZ-cl(5_1yl9o(`j--lJ;qmufH`t#*<2`5=w_b4ZXqU1~= z5R9mioSvwo7Kck}mF{{aWDtP}_!4jA{rCV9xin9nUcVHxdt0mxUS3BTDfX@4zR?xr z*YHHGvjq{%jc&C1*xHhxS;(TTQC+k}ojJpaT%#XBp=ib<|@vZ>d$WGspj2>MIMWQ=L)L3pB zd{(+PV>eH!&faR|vMP#Jsr%^)Ds}pJV6D1sSH4)PPS|wj{H`h?nl zGlTF5ZA%Jsm6f?9tMSE$(Hj~735u5MCb}WFwY`E}wAeLrUNq|DtORZ|03=;FK^}EB zJm?HT@;1I8$BvO?bxBDuiBw}a@sI#So=Ue(x_sGyK6HQ!xSg*GT@Bl*(98gS@Kyp8 zIZwb3Kq1KioWso5jRB1q7VU(ZRr_ZiDu9k|>Ah-XrP@OA1@^j1`&{{DZrm*;2IeU-(1 z4|I&HvT^u32RBEu_Rh&<^EIc~Tdh^gdDk-2`G=Kttq2ozth@1$dSsIY!O>e--9F3! z#j1n}8Pm8J&5h1`m*$hDUAt~VqepdI`_ylKY7RiMXh{(yA1mvF!7(5LAc=$2{+SpQbT0r=e1~+Hcnh2*ZJJBKabrrzVRTB(F>JC}}Bhq+<;aLJ*I@BINnJH5Q@UvyutQcX$i% ztOUB2`;ZSTazr4}(O;wyxCXwgO^itPwe7u8x%eH%#cC_7&UatR5{28t$qdcLXQ0@I zJJZRZmI@Q^Kmg-(W|de9H=QR$?^y8Fx-A@apfDIh7>D)9jr&ye5$gAxhf)_$7Bi~& zvVDKun<@Mdmf-MMc7uEq7eCauhgmQFVmqH5uc+nKy9(YwqliX}TC5tkUdsE@P%2ek zFk{if!{#h0d$Q(lSuNuWDiALhzqc{%k}}d6JLZmdG@rcSUaxAw{9z4URa92UCoOA+ zZdxK4eL8>J&!#K$XKYk6NJ|R8c65VdGqR$VKah&WBiDDPRdBr@zku16zKwgQ+j|pk zdv82@)%MxOVb_H9RbF*)-P-3W^a~cou1DUt%em;^tCq@4`i!)YI|_&E&*`69nk}%G zIC6E{$J@TXVl`uu2y!ZxzQNLO4B^Q~nAdBe2Eu3Bi{VboSH?!M4Ji+` z{7Nfj(d0dj7(Eo-`h*_S@)WuiBQZki1Gppvs&yE`LlBmP3@hG6zb<21Nu5UW)?#_& zMLN`pyU}T;JX%{TzDvI#pP^-`(&%|}3ps{oe50PUgKU2SVCLN)B=Q||yOA2*v^hh& zqWDd_33RWF^ zb-iS1#zsfQK9X@S%G(bSI+pEBa#)Bk{%2uub&QJbdcljjX>FG0n|D+%9Kfe69e#o$ z*nKD9;|@~g*fC-yZg5m$3UgtngsV=*QQoh53>KPu3zKlU3YvI>%3WGzu%C#|XN7kn zno+&<6t{v#E+nP|k%vgM!+LaSNw|eF zB#IZq7+U6|waldh$%n)Zfrqrqlkg=&1;n;7DN)KJNTK`rSfj?zO0IOmmAPjPiDu7s zM&`#tDwHlX+i*flHw6yD&?Fv2_Mk#~K^kF8`}~8)D(ZBAcld0+m?+OR572RbFa!a^ zttNf5I0>Igi5#W1>Rkpe4fz8T+WSh_Qcqr#zH+>5yB({<@f+_gmXhBX@ldVVe!TTFEOE(Lc5p)ZUGcdNARZo(F(j6-&<0-c3Ma#1msOt0>MOJ#}jO>L+ke7025`nez^?YxT?H8~lT%?#{ z)>iSa|=?(ubbgS*!tN) z6aDi`9Iee1Wv27P=5OG?B52tRfta^Yk=xU@AItk>8`H$p&M_WO9F2}(8UX`QI7H1; z*ktG;9WgG{z%C^Iahf4cvJOi?XH-}!#w1ZtycT9f^N$zh@OeqipwRfI%2cs&8)(iY%|Vc74J?2yTX?i)Shi>(JeIJJ;fv~rN4p;qjJ;V zDs{xHx#vH{A4M)KFIJA=sGe6PvG04?t~8+Jh0c zd-7uMUA9>3ihVj(aX5D0L^^h+{Oj%Zp)1y`Po4>cCT!0)=aKegS`JIvz_`hPXXat9 zTDDtTL~)Gl&7R;j=50iUq2Q$cq-rP5|2-rj*L=ZPeLtEL*~!>Ql8{_mYVO;5@VH7M ziQW|+8O#~otCD$z%bWQa%mv#k=ee#b-QJv&N7t9)ETLsaq<6l)kAafgS1}xRc0Wxg z=82d0y+9VYOj0a9tvF0&4BZ-SW3Z03iD4D&%k=tiVR$3aP11p}N4j%kvL;PbkH*OAF-shrjCsgd zl@Q#B()zp7&f|>%=9(=m5FJYfSMl1!&sN?h=EMt$72>@x`J||M@?WAk4F>&D!H$GZ zZJE4Uu)mzu0H3R(OU>B4E>YtEc{}+cKU!ef?2eY+$Xn`$sW;U-7xIbUF!OI?7`M}+ zY5uce{UoD)(0l>b;-uPQI4lC@%P%?gq|uX3MPqLBjZ7YXWAjHg3ejiP!!VMK?SlT3 z5zS$e5b<58~u%l``GC?F= zZVwX(SYftu#LSFxhdP;|h^YD6$P9W1c5q8&vbJD# zUzaLfi#!}$WyY|i=TS@^@L|?;BrBLa$*qqTac1aSopJPG-+aCSt!?V~B=R{v)6C*J zL8zIU2$df8Rc@ng=igcEP3?N{@tqS3y;KHv-uF14`fnn^8JHRMtT1YI8QD1tF`Md%Cl*coUg6nqCQ@S3T ze1z@ie=d6=H6jyYB3391rDaE?a3p(uI2J9nYJw{6M{EbZE&`?|7ZNJ; zNjV%Uc8P5f`*DBz27v-vg)pgmVX)flSv|^a(j%~be46D5D{Nu8LVm;gzBPt?C}L-Uv5$85Q%AbE z9FNuRu=jIX9MKuHWNx4xSsH8~wms$5({w(K+cu8Tl-U+(Z+`O&HM_Yl%OVNrfHK_| zyQ^wJH(;J@3D*;$6g2@8D)K^CEUNO|)}~J!=R^=>P<3DJgGA4YPy`-A4Ol5bi2W*% z9?V)Pex0dkbt3aBi)CJ_yKeq%B5csQf2tIU^>oadZv6{YYr!(3?l+o)*^Y}UjAbMX zC&n8O^s3Ow_mVJIZEGK!1rvubIAn5|U#6r+Mx7|x_AF_K)G^BJnUNW02fHh`%~Bm- zZo8`OC4-?+;F+WfSPORJ1Nd}nO#D<<5&!|}@Ob6qJxfvv&JLF(Gw56%tOksg!Sz6J z4)>`Bw%>zJZwtn1WS3LW|lTJ2YvYHbzausfaLhzFNH3wp%-xep9o%lE`+iS`u9 zT2(fFRNfbB-ljAu?o2uYEi z`3g#$)&V!o8bzhPp79sA{JvV@`vvb4<35~-3tqVUNQ^Spl=R*6oJA2E9pEYmJh3}z2 zf(ed?bEhDGtLwV6=JQ~JGd+<6c2{cOAAQ(~g!VpDrNw@%CsJZ~%~Es0u}J5V89c+5 z(w{Eyjy2ahti}Vh{P^T~uq4@XHw@ZbviUF9mj(w_KHUSteHB16Xw0{FEzK}ndg^E- zd|5n{=4vvOv8Z0e3)i8*+a~lG^7hdY(HiC;gA(}8&2|Y zbXL9fiq9{JkKx`FbnTw4d;-8nXq)48mrvEBC_%`+r9JeCBlL+oVRGzgdlfWFEk+!q zkZ@%g1gMUgBi9p2W~S)Gh&>FUL4eMAE0b|juG1zX7eM|(9ZpJagj#Wbq$`AK?iKzL zlM{O)wQd!G$FV9gb}>Z34ZNLpo%mD?Qk*S@E)&o#-_Eid%?09Fyb07>9p^#1;=RXT zBLhv^{-bq)N`cT?)r1dB^Ki#-H3j?{@36DCHx^9a-QAj2hPcU_(ERMVYh)l#Yc z+P-{&iOXmBZK+co-LEsiP=lH7yT_)`a7kQni5TL57d6A3RQwVMIm{8|K&N#CSq~z^ zVH+oh(Q34(%-K8>94FYGn~Ru5nvMO2>4Z1#rOex1@MzD~UgR@$tdbADs?wCBVb%HIFCkRkN zAC8hbKj+J+4Sx|s5SPlU@;Z|F5^r&=_&iBgWO`-=#ff>zn`#d%0aj8KjBIT5i$3Fn zO0_>%1M_m~YOncy8AG>cYK4s2Z451{gJa>2LUxDRu{m)?f?_3#4E*lw%(mZ1o-L$0 zCJOXJz&=xGRyFTuSPRvxL#t99r?O-0aa%-T@L8-Vbmed%Z>4To&lD_~MJ~K{JH470 zh&%1M62xI>bd_9Y?$9cBvZ>;(?Z(L&CK4319@83+3ALZ0x%o@ni^1(7mf|0g}Sw;`%YE2+( z$}mV`-H6sTx0zA;9rHoX*5YiX%CG@cv%l?ga=-bd0b684p1y$M&{wbsswU*@o)iq^ z{^O-7kg_pBcUAhSw&S*G;D_{S(bA<7!I!*1N}6k={i7MOKDpB^i+~RV7=fiF?LD1C zt~7l_gG?|`>o)Uv?bJDAsduu=x~ykIu{a(8!pDGKGA!RMF7i{CyVcBc88Phv?vOc- znr33kxMc=9u00&&#@&~wtJ5{d+>?S}XRKz1hzfbcW9az&r$-#8F)|$FdLBrpY$tiY zS{PP6q32abw4Y~k0}~*ZjmPBHF#XcaQ~;xzg&FJ4O84FDBfkZn+&tlxSW?WK2(WCC zS{ut07+K2>p9^fSx?yWqO~NUXQo{Fml$ zHhja&oceoX@=5=I)1G!^*PinFJ`>L6{ZcZYK*D5EMt=&$#f)zz000VcYLDF=Nza94 z8>Ilxnh9rdovCY>t$i;=Ji6PqzHKve@q4I-zaceDatmnj{QmW=kts{v4&lmyJ#}Q} zq)C}MFeyEQ@_PL=#~}m9+&Xz4U3sn_w$M^^5GG%VmOoyZCr}2!sdI%}voI@=6>umG zoGf3}AW~vK;8jA0K5_{IV`7HHXugP(Z(VyysSp?gDbvrFXu&5jXjveY@JbmZaIB1D)+Xv2;G#?WwMB^oQ>aHA`wP(>Q@XT(>)GWG61X!nQZ}r>}N$**p8z zqfm)q#mA1+V_UvkttC3xW%otz?hzCrf{~<$bPfekx93v@oITSt zc>R2;=Y;RXyo<+*${CVR1=HH^zOOs7@j(rKPMvT~=W$tPZ zo!Jv@f6DDAO>RYViY_5I(i|??1cA7b<-T#w9GGMWM09Syz&(*)!d2+pMhr+88sf5X z3UW-8u!B-<)DTe$J7P+b`beF!9Wz-N zzD(w5#}`*!s-7l=Mb-4k`TxqDjgzaQG4n6I;qT6pq?pCOIRDSHN9?ZbxkHIm67P_i zEq1iq$Q4g^QFBDwh_T1p>T}jb-V%N`|UY` z+63;+sw@YiPSJ{oHdx-4$BHb9ve49&0Ae3pm-NV1dT!Cewo=let`Z7)2CH3(_hB

nq(-YT@F_?_APNZYSVdpO4hr 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

J4>-QjdJB3Hnh|su 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

}G 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

d1Bp<^V)dLOY{ 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⪇4?oA+WoRV4qOs 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

5nlA}8!{s?~YSq#LNo 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

$DLI;ZJT8~OEyKzosNQ3yV4LWtHIcn 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∾1I&9lib3^<%OpD20=Iv7NdDg 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 z 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

9*|{g!cdgnM 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%0A1@_CYhgkwjW+KQ(;}oYmyV+%%x}Ai-6M@k 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%0D1_TmFu8|32b7?jXCW0&rs?d5&irlh`sU**|Fe$ z7#g+^nE@cGd#L7zx?vxx13~X9ujGxg?_k6XQJN8?`Hm-OT!xi(aT-g{vRI96;L{=C zYuf19Lz^`R?m0VecB{XxuIP_?9LbkQlUyCQCy!so04t`O^L?()t0Gh}l-`0At48=a zkkRLWrWMSP8-cI@)@b4GcdJksKzlS~M%h)g-J}nQoKZ!d@GkzZ`Q8)*c|23tOqs$& zYCLeH6v}7g_CA^tX9piU zN4nsNb1#xFz4HB6o44M2bfElrFi;Iqrh(YB96!()dvm{@xM!Yw_+w^Lxo2;_T6GGZ zdqGu+#Lnn7xn<0WiV)V6wfyLnr#gO_7rXL#UMP4q@B}J_iHi)|>SZ^0*ho+&{6vY* zPz&A@PxwWo64pb*BckRMe=0f<+m@tH=d$gXud}?an2ch+L`iOobp?cn3Pcm+WkU-H8o=ExtJ zOu`F|Hq<4Kq_*;6qrME_P5db1B?7^uKp2$p@NU$qptc6(BGd`3j|0*hqSA%GqVC2l z6oUqNvp5NPE~>ZROdh=**U{-{Kc)%$@pGorB&ZTkG~wLm@{jafaO-mk0`Z#8pNP~r z$iGs8kp7+w-%-CA-Fz{IrrcgqmQ___DB|?{| zfcvyAG8pLs`5yuL(BBr@Iv;4+odpwQj=Q2Atr*CWjnZ0Cwf&bw0#6fq4O^gvk}2_s zShhU?-BhluA;@n6Z4s-0wCHVa;>vs}*(x*er%6~&%jCZ!u%HY8D+cc$1c!}?d5B}s z(gN#N@!n6Da97x8?QK{R(5$_jp{upXVa#(8z`|L7`d0oqzGL2>o{FuZvpTMY;%Pdw zY{y$phVjB5QYnVZpCW-~gELX6;u*Rbu%QlvZTI*Fju%CeC0R8gt~L}eAKZg^ChK(xy=80#2&8VxnpttUC7 zvZ{Xg0bh+vZ{Ky*b%o5}WoIp1jM8Cv`KGSlyU^CU=nP};oo7lXKYmc!*Lpd72l65o zfIhpm9|EHg6G1L#XW4;Y3O}-5=n#I_`X+QP!t5ARv>=ZoBS2YyiuhjRT8E0p&d|~p za~V*v;mjbFr2j*3%7!wswz#B7Y=i2J0S;suD1int}lUFCy-3tTyuwW{x;rO4@V3QX6&<|{ozcf0tYq7Aj z5Eg5Sd@>FKQR~nH_Iw7)&Jcu4YhQb;`xNLLKzU3b9UUH0gjbCn zXb6CzJ+?oyO%UoLpBEkz6tjM7&Cmdte~!uPvF?2!4^RR|jw`{~YMjC@8qo}6CD%1M z)7UKNHKJK+mX0cY$`e+PSv|05m(ax57Y0|Bnpw9m$bl@hkw>TQC`4pT9W3C1(D`I9 z24IkIM;%cC9_Cr}=cZ6cyaHI|PmuCgs-1%jCdL1y6+-(KoZAe-r8=I5BkHBCd`iF5 zRJ=Zhn;y6H2{;JyEOSV~v{ypO`o!!FjS2uGq)94LY>ujiHuc3^zXg0KKsX>U(d8y% z#Km4putfw;@>29x>bRu4rE_x2>=2Ou*T}#%nIePMMhskV6l=EwZ&m8&&#!=>x)@wx zCo6A>V;&weY-K}muYbL7dkXpj09}Xd_HJ&|xbLn1zyN9kJ%5@q1_H#KuW_qJHR1i* zidejfbCVG^rs-voWg^eo%nS1E1Fn=X)2&{-tq}4nYOlG}$maGQ&F8C7=-JfzK5(XD z3-=}UKuBl z0~e}5-|EcEnl*V*pKWyVzYth9qvTeV3nsTYvbl%dF4YEoi>&kafVGwJw;)<&if48; z!l~s(TnY#I*YM|GQ3e65(YWv=KZ zN1BeqB5|cjkqdd03##RMDp5x#M!owYrj!N}B8n*lIHnV){X$CO;67k6=_7*0QzHnB zU1N>3LZ+;aPPM3p%djG}oMA2u1CSnuXK+DZ!@1I4 z^p!Or6*?#9c{N*qYfvdCf@5~+g$4CV9+;vqG_0Z#jV+JYHk@RpYao(e2W5o44}gN@P^OEo7}^q3`wOge49*n~Q6Cth+RPYiyYaBOq`_Ra`wB`o9opdAD*kwfeB3p#S$Vw+Q}*y7%A*{u%*}W z-pk|zi>kt*svY+)R&~E{Y+I+3SqSxtSOKqA(HF#<^I(6~qJA`L^QO!R>v%knv4t~W z8`jyvicZ4|NBhAyTBEomcPxD2S!femz{cDP1nWJi+T54mkxtlUN>dS-5ORWc zAA|J0W;+?Uq<9vuVK;)(l=MLgeo849;c?ooVsbV|0izNb!Q4EVOr!0qCMY$=b|WPp z7(}EgHH>3+M`sd>#FI32hAps5cc15`YE?z zqRlDbGRykf__(6=4=a0#cbs>92;|t-jn$k9p1r`8#iw%6 zXOb799ROtvjZ@cR=3tI39MmM1-z0F5`DGP(vDlOriJjt%FeT22$DZON2x;htWwQus z&cPTUXxG+O%J?!MqSwyVWX#9GEG%H};Z4Bil`+Gf!Is`{U^R#wlvU6eh~`2PWi?n? z2RaJFCvb&RDnmdejEZR5L-`EQD)3l%wh`h|w)2!!%$kx8!f*n-GCE1aDC9(AFboVE zXQ{&}OKCuW2@_Ae4>=RwpC&a#mI7=`j4TMCE|@3bYVgsN1l44G#Q=X(ETi#bnhSyO zJk5$Q&K$3a0L2iJ2h|zAIHne}^U`<%^U_It1f^5>y$ZyxP#@=xlx0quT;GQQom1K= zuEs!*AK02}DChc}*lk~2+=507dW9unvb6^Sy2`lgCr1?Z{5NfFvO^9MpdL@jr~C%1t29lJmU#>r`h+@t?FZ%5?2; z4!TWBbEWL_<%I9nK`F3I9fEFe!|dHJ?1DZWy27&CSRj?ao}ROflF+qOSf(bza9wdW zo8yLX1PK?ATYIa*;%TM1u+TVw@q0bR5fcZ)o1h`r36EuA*K2FX(T;ae-bB?ohMLel z#wBUPgK3ya3GOU+6D$#2Yy`QQBqJnN23S!8W2v;UG@cS907^B9Q4%m)G|+_owo~K) z2tJ&NS`1@Nyk{0&h1X$t#Igeh2Fy#5ih}}bbQ~6&z7Yn5HYTyp2KTk~< zv>6e^(lfynr~^kL+z$EpFM@3 zHcH24yv0$Esy@m6&pua&G$*AmOxyxgr> ziMO*27hiPY1)p0f&R76`_;opxR4b9Bx{LHaZjpLsWl|M~i_6TB0IESl>HyE&8wb3i z-TI}FIekd=yC&ByUr^!QSQCdZx>g~KY)TkdK!rE(d$c3Wv3m@@7H1r1hjMisU|w1i z;v8p$-!t_Cfn@o^J-fv%;vM}EzpfQA099CEivP}W0d;MjM&`q-;KoLu#>T?IZPGNB z8>U5exN`SCf#t-YwRnY*Dff3f*0Az4tMI>n@8W59+q(OI08$A<OHIn0Hs zJ4<{d(seTqLff_JK zKIrc>32FZf@1N#9$Ys(5x7}l=beFOrDQpuo(OEEtLFz*Bt`2cr7#f8^%5(tQob6dt z_MDnG$imu>(U?p#oAf)+z67IhkeV-9E}$}WYD&1%&!E7Y)`;o2=uJJ6Xvj6{Z0okj zFY%G66wyZs4YL@X#U+Cc=i=*)T$PK)R#+3DK(?9dyR zyi5VcSJGy)6qRi;{pB6HllbbZ!KRHmBF zCG3*CS-^xY6RFp31jBd6;ndJOpVyz}qsCyjt~8=SGno7U=p_=n!CzC#nHfK1&zzvG zmDPw$mbiU1Xs$wA%go2Jvu{r-uBBHNqszToUM$}yPsq!aarr;Q3Gr@eRJyt1$~kZo zqWV)ZAty~_!TE!JbR1ej>jxz-J2>bFCG!mF(nvHjyi_X$;;X#GmvYtLWwUyKPB$|% zXj4f5V(A%l748E}IJb+B0{Yw>aLl>piSCQmpZS{|%$D1pI~ObE_3#3L~Jj z;Nt|$$Ulo(e-ab&Pj-Bsp&tV_LrqOA=mI0aaz(XbNm5KE`bor zo&brY=@`SCqY-~M&>1~N3kht^45k(Z$%4*$53<&#s`e>C$>a>bb=M)o$B2e43M?-$ z;w#u;*Hw8hj?T3_F<)EZSy488aMBMTkPuxC^6ILzG7O-|z`MDvr^RJMRFmwgV#e7} z0}4W^zlLoyH1KIimo16Q?D291+2H>`1d+Rgi9?%nAS*QW9aT`^Cyv~DA$0Wd^A9%{ zVzXwE3i0IKi%rO)mko?chBl8#W^v`dLdi;DIuiD{hfg4XkM*q^Y0bsy7qBHWZKWD_~Q>aeQOv=7+ zjj6q-ZFHD$+P`k%Mxle~vv3UT8DC}YgVQ3j8IKMj+S3u}Yqd{9KcvPEmp+>oV6@LY z>W0IQc3JTqeJ*F5zwcXFrT3!Sh>6wy#k;rgJ~(WT{{8Ne66-L^^2HDKs-?`w&e{2N zEqTMKn$kMu&l)uTZkuD47R9S~6}3=t&dXSlbMu8>){spXB}S0f{QiG6 zeEdRt`1rYVJ3-gv+1loefE&~bFN(r~S5#OhwaUN4ZA@Dz(AE%-XIAsQryd3v4V_3Q@IA;s=za4T`>0_TrRSO*#-DBLrzFbL&)G{WRV}>1gUvMhRi5vZq?$wR`51?yJ%3;D_ zt7BRy_754?BQxJ?UC|t?Ol_W$`DOVg@Lx*?1%o|w%9?oDjFHq@??VTDJOg57WzpGX z8>NHY^a@Zr3&OWyQB%$vd}lTPBrW|3vfLZqeyY%UmU7Sga@!B0527SrckBf(%iGV! zpAw|PV{y$eKdF6NXv_%8dVCdjmQ(ta?G|HU@A=itV&ihHmh?KM8#XqTY=ugJ$@+mV zZBco3!)OXF_=3Roxt*4b>_D)~Gon1U!WxJh2BgmvA-B1JS+N@MLH?Z8dZX5Q_9o$( zt1c+NZX6_Kx(i@yeSxnL-fRG|^t1fZl&@(&x>WE$>0FFb{WjNj8#w^CNV_Ot z!8@cW5pHwX1wEwh6Lq#l>P+JQ?PsYTB+%B}&rpOv9^d8~n&i)39NuV!0?8x}?ZWm= z$^n{c3BdlylBo1ZJtRG-Lk^Gp5)3o-U&tJRU8B6P9ZRX+K;WJY+SEFw)PQIAeT98N zfBx&MTK{B2IuyIkJQhKDtCZ6l1?Ws*;~66H-hDEJwjnr@m`bblq~mJ>@M7S#5dNKg z#_?4!en_F?v|0pv`H&kry?0b1N6#leWNZsiFDBG&z6#WfY4Sn}dpXR#!6_Hdz09%1XubqRl^p! zZPuuEhdFGvK4OhJXnMBac24Ka7Gu zjyw}}@!|g92x*?kCqI(9DZ2xPB;dp>!V9#iU!E3^0q6~HG3DS2+_$|a=hpc8T?OmN8fCm9)Xy2zom|$#jea(qlkl?pp?KwYzCRE$8koQ3 zf>vn#4A6U}(`)8;3d(j{=zQgvH90gb2#aa~cJkyYL*bnD%FbrN>t1+}*C*?1(fByv zAKolXq8Em#n_j}D!T;jc$O4(aq;K=zE}W!=<$KvSkzJ`LN^%NtsfsaJ-MfF6kssRL z*XKgEYbff$7gmIK@?Dqjw=(va7qw_TXF*yW`=i` zoQBjXls}_1iLUycgQjQFyBwi5ao#JP$BV)PUunHp5vE=3$ax>8^|5z( zp%wN;-3@0#$l0M(a?C2eyC5!A2-pIfX+kgSt>}*qKso%hj>8>qp*=zP9@-v-kx+O5 z1v!kS0tSdw`MqIlM?6`7>Lbz%T!r`Y^8Y}J1IJJ%m z>~*A;(4a>lNBykys$)x(R2*mbHSh^A9F)LSEs`jJ5S0Arlpcw4}vhF1wTMl2h9>Vd?6{;R)OS}lZdtS}$r`1+06yeuK-Kt7ov)k@=^MnkLBzRl^L3~j zJlFB9j_;+DzyI8~K*VUqFeF5)b|h)T?`&|oXkCgzB&#?5l0_M0kQqAHcK`hr=e`N_(R2+0_{kb@5CX`ymUAK zcLCXVOV!}rtZ~0^*Wp{P`S30Bb?h%s=v4flRlvV4RaK~tV-cXB$?|GJ z%X^Z5CI;|(fh;dU-3J*Z$x*iyin1dwL(s-PNfO!(XaHjn4e>s40w7Zxe11zs-$7W% zc3Q-5K@^nT0;f)s^n$QdjZ9VClmR46UIMx_2g7W2Au!X~EC;E;%as-K{n9#dg|r48 zbqScyxAIS=yfxc9u7QTeSE=_(clQa#2DgLeQktv$v8A=s8I&jO##K85Y7c*rV$B~d zx79VsNO0~6Iu#PHc3Za1nwnZr)K(s^)_v;o;v*>>p;|oN+-jea47!G_DebV$&JgEq z7apt)AEl<)ym`~6Yk%{===}DEcQV#IoIWmWuX!Bc>f4;@sap!8>|U%B`+1uO@H_I% zXMpIc5#-W!p$9}GGW;pt}dKP;T^@a9rrcs3Sb5S=J9JV(s<64(9*Jymr@U?6n z8y{$%ml(o(AEJtAsY6nU~!RMU+0dk zS?;hKi#1LTw85rYSK{Kd31(dIKP;=1bRo0Wk*YTPO3=!n>)KS^o}y+u{R7FPV$q4v z#^RM=gl9_DR5)dt#{#0TJ%WA7I{5n_XD$SyxIM~>Ac2_{n4~AFc#ph|hBU;n`)2-~ z=oJ{)j30#7&NZ0Pya%rUsEHmpM|j7WP@IH!m+CQa>gZCD=b0EwrL&NKM^t&kTtKnW zZmrL1!>1`ckDeI73`p$T!Ypug6He-|X7__&F)5i5Tq7Qm4+5JW4rsz5zPPv0&cJ6m z_hykYmP79hX1E zIZ%l=*_(XiZ5DP}GmYV)VzhN!Xx+%Wstt}8)gFJTT<#3Rwb8QGa6^ra4_aCI*tK^S zim}q!oSKeP0XOed-v{u0D73qRv$*UY_e+<6AN>}jR;o3;PBe|RbqO%ZfPaZ?6}BEe zGjf5vjpHc4KlXVA7R0U#krhkYE@>P1^ACl?%1ihqL(XWEs%75C2UnnUwtze7(Hy`uz_s-ZfjA%pNx z4KqyNL%~POb#LCT-?SKnS-IV$}=> zv#J5qvR*IvsOU6%ou1EQ<&lSGy~nM9^Kh_v56tnPAO}#L6#R)`$enOnm&n>CIO8G~ zjHE{1r)}#+qU;7DfIgcwECRv060?j7H}xsRz5fNweMNL2a~-FoY#Y#c>q?tBjS$@% z^mJe{R9HpGRL3WQcudn)f%!Fc0b7y4T?Kv$O63&n|jfjr}?gSP@MI2?- z*p0>dz-+pm4wH$Vp9(&-eI~|Tlx!jOiKqYrl7RNE(ego$3Ar@l7Y@YgDw#Yso6}Nl z91`o)EkbfV#(uVcRJchH?uGdHRXop0*E>S@*?oOGei<~LJ-w2Dgy#*l}Ld7p;7`u6=8W+P~LfR^Jsu%c$82RgR;(E z2Q1^jaxC@9qq6;z2WvtO%qwg7)4LSmx_xr6{!*yfXTZR72}o9;4>u${2t0>rtc^;Uld^sztNNeAZP-CQAPKMf_43Zp0~sG zqCDdm68XJZ63R2^q0Zcleg?$+Y+DGi6I5wt3Vl@1__Q(WGT zEvuS2hb!*$Uizq6s&knoYyJ*8U2H1xcuj6ZA7YE+j2-*oo$Lwhs2ioYb))iQA54su zr1fcq?NbmZSh7Ud;#YZl%6y33)z;g1@KfY9-xSw&8ID@` zYUsqP)`nW?@}6KO67rk-CBSr@%Biq7(xZJGe2z`jU%S9nw;%rIAh=rI+wp<_t<_Ow zJr~N_HkBr>bBO%$d4wDC+7#hNlMFIjn)=b3VWqR-vmHIr>Or)cwO;C39xt`vZf0@0 z?XXs&p636cy!sxr063SGM%k&FUsACX;o7Z7An%Z2SKh{6{fQfdYd>}!JNkj6;^jzP zJ+pp8pg3>bu7rh9FrIj;B}Jw6H=Wn$1>^rZEMdc8G%PJAqs{La|H>Mm)j>Md`<1T> zpJiW~FQ49(Z#h^De5B0ehnMdnKKKz69sZtKoc%Ls}OHPeMFVK-zPzVdl^WhK(k+mtNZO2qMs|PQd(D`=7YD z;MhIR&VuVJ{vQe|nv%g=EC!|Li;A!pL2f6~5~p2FwG%5E@urUCMOMYztv`tW=8|rJ z-ue`fC{_X+RHKCUTui<7CC>5lx4llj??>-oZ~pe%q$hs$LH3?w**OBAI24UK7<@)o z`~GF;^nw>2DzQ^T3$SxxQ~)Xn*w1ohrys?IUAvj*Jl;wMKXRTS@P=94>{)M2KPVH~ zYHJTeSM0F9NBxemMSDKc{-stjTr=k?;x~!_M=LqrkGQJ35j-oK)W6VHsoS;HTA%d( zJUCuc)*fK4Luh#@&j+2TyiT5oK=6Y;_s>8HM8ZCQkCpqC5F2(2t=)-jeV~>F#O>^1 zPg0Z4H7>F7DB{z3c)iHxeIHpw?S&^XM=GS(^dzVP{!=|f` zaropZ;?SX9ep}+1R%NImOVQUTEW!4n+*_xe1xWVZ4}H)Ue>bW}u*!C4?DN#Bo;oNww(ay@V6 zALAb8@BJtrKt#>9^g9j51+C5Vr5Zdo&);TPS&Q3qa`#zF0^7n0YwnzxjfL8RvCJXR z>ngG&+E%oz#vR*g6^DuhRszVMV;K{GW(fH9c_y&%6SD_q8Qc zMQ7VRfTGw8WRyM|g}_n7cV}%3VopX6Ca{6m{8ToMx*BKj0Iy5JE~qejv8<&SYr?v{ z0hpvg)WrqtkMHcb7#`$qu+-BG9XCDE$ro8yYEh}vr}71;Mfi?ckWciV@*1)^T=(CPgZ6_P3?01Daf_!l{^DO&vn7A0{9Q%ChWau~afF zrD$!g=&HGY)B>S}goVJ_Jr6y^s-KDy82em5)GM;t*FBS~V(&ZmyEc-)^|ZKyD~IYf zAN-fdk_}1R8eyXV!n*c9TCHEoe8ER+r_5WD7D(V@U}2`F`(V04!T7MOF7?V9Xu>u_ z_5-z!#v9Lq_@nIOOiV3e;HiC5!457ODoRH$bWpy_2Z%e9*cZDNS{DyNo$5QV8*&@S zNGTzOCBWCDGsbrFEG?#+1Y?9n$x;_Q195XXa($qD*iG@bfmE2btfj0u$y*)WX% z1Ow*FJxd+0tIn~kF&^*Tt@-+uja`Zw>P!(m=Wi2-XZWw$`hyxpIoFNFes(AV$*2J2 zKqG1W`1+eX$@QiB*{_DqJs9TCIJa-1mwDCIcL&9)1O{g93wF>gKUwtL$hKQIxL#$k zrg;m-S3}8O=@~N(cN%dGy5-rf72Rggw>T@e-d!~XVX5!?UEm7~wBBGpx1`s;bjf;P z#^mOKe&T`#I{=$r@7p=zdC;rs-LpoeEF}CZt+rP$qi?ZM{q~>+E>b4S14<4ccLk_vPc0mCC)B%8VJfrCnzm%55l6m_?wzCr=Mg637UpLi{l48DR)2l8~JEJr$ZD2WLo zj|v#$Ns_g5QaC<>oZFx|695$Hun1;_!wGkd?LZn9IDwW&CIL?bIxk=O0 z)cv9CPIF!WwZ|}*OmY4+nWC=%j5ftbbcj$Hkg6TXPivrQCX5t5k5~IQ*MR zczl&fd|S*+=@(dvFF?d*_?bHbrrV6L-Fj`O@1HBo@E;dS2ZWN*Sr_865>Ljby;d17 z$f}-QAYR@%)ff^&zO-Kv9zeh3sn+k;$bLDO@GHx*yhaul+!1PSq)cP{qYMUh*C=f_ z*yrG`c<3fAIkd7q3|1&3=Bys|i%+g$QsOIFbzf}so!`milZxP76+nqM-RuHF?4(TnPblACKDC=I6!8o$i*ZMiXUST0V1B_$)dvEvHd)-khlVCvh)r{6qz!kJo z(Caf$_|ugdlr265b-nSEF!7SYQ2HR4F+gFa7?0V_nieitQNzg9zPI)JolQZ%jJXGY zD+{{b7=2Q)eNdw7&n|lSitc!1#6bNoZ@;WI`AyJV;g=VNuF(72*v8KR+c*v!&e_;x z{5EC>b8rbtT272t^P4#B4jTLx*qGai3OXTe=E`&`L=FRdDzF-Op81NKz#J46fs?G( z(|}1&z|~M01m^lK zPH%1C6nnUn2bMPJHI!oQ0wrHzpDsp8A2zm)+(Rf+MCmE-6xEEp=J-nrWf%GLceA;s ze?|lvd&iZA55)YDTP!84+}h+B=Vt8uh0e*H{HyLPM&-M2;`Ep9uR7g(5Ayk$y;1el z)mNYg+;7W-lZfJ+Zpv;yU=k?{h_Y#p&=7)DvZ8GgGg+x0;%^T%dyS9=1=6x!0V?i8 zEAqW-0xaTVoqAEH9_h77@z^)rEVsO@6&BR6PZYfj)rEQLfEQb14Fxj_AJQpS=nRdH z<;}3u)VhXMb?Z_|Q!{(uI*MDu(l%deoSq4H^(eAoE<0%E*N1rIc5W2%-RMcX{Y`}p zqWBU!N-w+@dK-PPufrV=x6!ly@jQAyB|bj)OtT?a#EWH7$up#>3mFFTdM-_^Fcm$X z#f;OeC}k0$lm?kPRtGRVm`knab0F^cTiS^aS#tW7gfTM3bm%nSO{Np2gz$4IKMV1Q zmI>-~qs-kDc6-})8MrCh`)_ce6w%v37^NV-9G&yP_R#d2T+~)uJ)0}<4z9i>Qgfk< z47+DC!g{2o)=-)y9KU2yY&!Ma%db>C&D3*A>vks=GfLp;x2|__j4XJrv7H&WEM%?~ z>v|#C#+G5BP0z*OcL^d3Ozjd@GZQ65DS!xd2kl>Fl4Sj~4PO zug70UT!L~dn9F`?)FpH0+N9z!RH+@aLohbgIRh>SVh~rBpo@r3F$Q6Qn8B{TvP8sw zfX0gx8P9C}Yk8Dm-32RZ!YUpS!bR`MU}f~PM1B>gBq(7kz!d((;GAhlX2$e<7EhG> zp`;*TGXumWs%=)5g` z)VOG%-;`RQPgDfyqgfH!00iQ+$X!p135#{XJEn1&K-7E}#C7E89nY(AoDU&z)1|eJ z`h63Vy^b-n!pANxOBP&j@VSP1q2e98*bfUPQ(VIp#pRKu=Sy4;fQO79oy(oBLF{#^ zxnXrDd_eyWU>_EYuENGfey65EXXrXtuZHhn9AA$%KrTqlwa&JiQdWNq!?w`u1UN_l-&dn5Uo93hP_HwdA(@u4Xu;jLUVQ4x zuF%>0mZ$bDoGxO8wxGPK0z9LlS;nXW;nA-$56Lr#FK~?rjh);t?}!H*au?WPmxO3-Crwk1@|;`6G@18#+4IV+8$ov)d0|#rM=Cc-`C3vfr@wQt%yHkblA)e2_;kkLf8B(`h+i@oN zCE$Dh7Ytr0Z5MX$WJFwiVSiCY30 zN$}CnX2H@Lsd4KEs2>~hS9?t{?*2>e<$opx*9U~%a_bpso&5iC_9g&!mgU*-y!*bd zXW!<`nSEc9naPk$_Jo9xkU+v72wMm{vWXaE6)Y%%wgQ5p6<2UW1QfLvi*<*#irPXphMMmdm4kIPAZ^dTHYG7U+6(VKi1lJ`Ks>c^B=8s8!iSPYVg@2)`!y#m6GJt{m6JkJ!kWlqG@;&1<#^9qYgni6g6(`jDP z3p>(VMq?guC6B?#N|2);J!)gQ*pWdTO*t}) z%Caq(1(kXjVj#yN%C0O$%QoWJ*HeGMRD(aM{apxRwm&7J@9*>REH|cx&^Y!h5Y&B| zFW0suCA+%^d=IPh;ca)teMP8O7tHf{AwP}nY1u#9)i<52SiPt2XeL1}@PEXe2BHY7 z;lKBOXwvul{}=lJ8WpqP^*`qxC{#R!FUQJ9&nh?o{`~&X zu`BeG)m3HF-CZidYOwpHN@vzREVdN{2f5_6MGw)&3ZrMR<^o<&56~sCH7==Ud`7{L zg!8fJpA2tD?$3tt<}^C)s3)>qN~MXLaTCALt?2+V+TKcC;BZ2Ue2k+~2o?1vkYt&< zq9M;RJhrz>JO59DFG2(AV;COhU#L3(c4jyPoaH5gaE#IkW=c!XIMB%i;|ZrTj%Y>D zd!~|t;vB;kEMO?;4$P1%z#P6a^P>Ekmi$q#vkeR&QSp0%LgR&GdL8!MdJ6~*az+T7 z$eW3*8YJTM46>92FONF`))pgubPwp&vVlJJ8kn=eFUCnbie3zJc{J^@(#*W?7(>^^|C@N~;S7`TOAfUD>GLUR?m1~ol21bG7}$ZwkwFSqwX zC2E^Ea3HOWy{jOa!e&ugRMp3dVsxGnZZ#!)@A;Bw&W^asT(KMhh>DGWllz&uFmFfs zstIsca@x*bv7m_MtE?W`%h?ClS5W)oJt*Sj`Qp$@AQ_Gg8dJ+!dkW&tUY|GIT?K$~ z)5^@cqVU>owf@G2DT1l{ii^*?4hR!%Zln$XL4H)Qy$_AK<~gERHPEOSshyW3`DFv* z#bcWi(cOABSZx7D62tY#smZ6^kN#&jeA*&Z7x#4i{D0}w*a)qYoc)_^)ot zw`lhgiSSWw3KZ#ykqIr)A+Z{bc`E8t993Gf4NG#Q%yS{; zWp3^8Pnd&)XUsbNEQbS#B=dn(u$H+$z|C=QNSnpl99-*aPPP%m1_Mw z;ESybHf>H4FS3)z`Bk~lYuftVg7)W5(KO1*|2^xmP%pURis1vt5AI#JHnj@dud2MQ z7zLi1PxPmBey3N0GYk^#eE#axDPc3C9O@rjRQdL=71<1-FTqJ=PrDR8)*m%NM(OB! zzHDAoLvQ zgIIfY*UjM00c%bGOM;^!&QW+3>R7`0{BD6Bq(E>*aFPv($FbHWo<)wcMNQG_%oLa{ zl&E_GEljy3V=1Iby;C?qdKGM<5pZdW@c!cr2o+->NV_I8wCe_<1A01op=f~M+A`cH z{u3n-bO6Q)2r1}+qDM+yQ{s%n9stn%%mvd=fuTjkG5*;9k75s>thvXtuh}I&q?ElY zZ|oO?%l1M?j*~+1FE*gLtcpJ#gX7~$KSQSovv$dEKj4Z3s%v_ANF)G9nM@`WX_9-#2=j(QGVc#Tu%woWKo*{lfuifPebxo|t0BB9c4?onF~WTlKf zDv(dkfU)4d6`D|B9#b{{rNsf`*NlPyR2Cp+DHH;bd5PBtDYnSL7AGd7IygF1YNML2 zT@qa|{43b8HC0SZL7xJ}hx2OK83>I5_%a3je}5r-27DJPbfa=$LcO}{)~>%t1xUa$ zd}^=~d}1p&MrDGihffilBB0#EhlLM4Mqok^!{-Y3LrXzqTUY{u9WFZ;iu)B})CO_lfVln3&KY{6 zcn|KCEx8}XaP!x#bd2i`e+TU;gbfI-Vc0NFNQmIyW zxj1wdp1B9IhkR72@)z2J&S^DS>^c2hUQ}}zHueSGzHo2To@lR*{Gp9#fP&-n-6v1% z&x`1=*CoVNmzUQgK9BiD;km7%J8P;l$?e-Ytc*3VkTK&#fgtP_hlL4%<_b_s6Cu4Z zgMA_;%-@%)h6rjMSU6#-sOnZj-SHtL#G!T-Ikj;YEcZ-zgAQAv?Ny&i#OFbFqK3=Wx_||NVW2=GuK)T z>S%vuwk@?NF`Oo$9D>U0}lQvuy<|mdIspYFM=)ZEC0(ViG5ave;|Eg(BTTcKFr7_kRMIh@yGvvE1WHEMSK@|H7rOuV`x6n0a7; z=dpMwnnhWt-iEJIZinFk%D?<>?v0?kK$ft;)Mspi5y=wEqU$<$;)*(Pt0w;`CEBJPxS z3-C*euRJI`2|h}65xTYjn{aB^9|2?8)x;;lVn_oqgggjKp(ilUBgHtX%_uxj?CGAM z@WOl&V6(JwJptb^Qf6B0rOzMTXe3# zn*>LdEvd2jAHpMkO;}!XHS}tf^;3cmZ-vqTcQi#$V(}Bc2kCuh!>xfEp6ccoZC>4n z6!z*NtZdDs5bhBtHl;Fz5TVPH0EcTXaBr)qB}fEHJ#XdQ@@9DUe&8p8kxBnd_+xQj zwV{Z^HwK6P6RS4Lx+p#%h%02#U7Rb`@^Z~jiU<_gB2oi$kI8QmL*t>ud}+XFePDa- zq%i3}*>s$jusSKK7na1+R=aVn8jHcEXt*FAfIrdh7l-_?=8KDtpoGQQN(i~dqIl)v z1!;&PR55QpSlNCe>f&T}$G(=c;_SB<{9NJsZ#~qi$QE>GE||fi>h}(8KMfrs$J$w6 znHWj6I6PyR?ua>X&q@{5f%$RE-j0>uMtM;5oN}bw3R3olHNMhMO;0W)7bK^tJ5R)* z5Ds#^E4rKDXSA*ruY5*$0^GG*7#{K>^p$P}Q{cn2F%(d%fXeh&afoKC)vTn)wse+C z!>QxMBgZ{F{Ug_a(sI@*-MOCcfm$E%IjpUK&IPRs;3knE;TBngf@6UHGWR0KyMi5$ za&6V9$v~R`GxzfSVW{hc^d*iPFPLVJtsm-h65Uk>6>f}sSclA+sPGs&y#I5aWPokY z`wrJ!t_uL+^OGa@wEyd`?B^!5yj|=w5_6LKRjFWaSz(24_sExuu5Uw>Fl;;)RRSfg z8+ottqh3FDM*CDZSLXjVeEIKe=xL}z6r0@1A8>uJUx%e9%$(J@HMQ(CEPR<8&zFH} z;N8fuZ^)M*B@|j91NkEm?{L->7T?%J#F(}bM8Q5;<1nd_?P)av&yqH!NwJ`q>Nm|d zVIZ@1+%Eazx>&uzyhEQ=f}~l~%&@0@VlgZn+pF7gRL$3IPoG>c;__I-imj2B4RXVC z>*YW%%^RWD_iHQGsjH=JmCbGpF-!RnicM++88g{NRO6xV3IrO=3qGj!$|J(}fXN%e z&d}?yL-I)1`?~%Kxe*%9_ymuthX8#=2?oz_1R=|&wW#4Fe7G40*WuLv9$fz@TqPh_4)uH6|Vf*b@c43!!Mro-!GM*x;ULS;}Y+{yGz4fd(P)R^2&2ffGG zvnLoP6DAA*0@~k740WY!>|Dl#5-d&_Cem0B<-st=e_x2kdz=dk9(qT~WMOjke!Zsq z7q^C9m#WSwt7{FYt&9xD?QIY<^s%mE8J^sw$m67PVi=7XHA)VXtQ$h3IvU%y4X z`KnaaRlnDJ7YL1f0NsQzd_fG$PU%Ejd9$5fCqnSMbh$G<2S+ImoL*7_$69r@UEqJ( z)C#$qAd@M}K6VdRkj=f_#%nkeUS^hAU=I!!r_m zgFzi@Jdn+>Q4MY)yB3Y`=n>Q7H1Hwq8T=R5;yVvbggQGh%-&{c6%THZ@;2(#GPj5~ zMSl3mgo(W&yfC{I@m-KhX5T18`-AtI`F1!Jb|C3IW|YC5?MCIeKrZ5y&@ zi^N@UnVyZ^ST>R|Jl1jp?w<&KeX94I!iI$dW->W8`~Hzc3i&3yKV` zDWUxWCn-`9y0?M|IT%qf@{)*I8nk0V@P%mDxfrA02_8@yG6w+}#J3nT3{E&MjL*fE zh8CfjEL^v1&%6|t4vY`3f~#78?8>bJW6r9-SZRJ;+djXR_9|51Io>V2fL@-7`nLz3 zo^bRp68gIPNaQYa0k-q#ykHpAA!(!%1EqmXn}M_xO4j~cIC>nWB}^f8<;bjYI8fL! zpG*M&i={|6>2V#QJ3M5(0t0O=We1cT-~^E3LWi<=YALlLNE*cv93vXavXg)&51}gY zx(vHSCz_5mxdWox#-c=_TyL^+FGMK#@k4)gf+gGDIA4fkdjNCLm2rU6{{q(p*xKf;c=SXrHC*HBy6W%U2EP~{o8Bge zhF8HP^D>QUnk2UM>zn3-BetWR@pdG~KHs=tzgbz~=e@zQVuwkcgOrB#8c0BsYrxqnUW-OPN2zM~Zk&aKda*0Yn-()+rf=wUpGtpLTF?+2tt3$P!vI z(IbwO5J<4gscwQ@U~ZPl%;AyrH5hq6FI-vYe z7m`(>shV=)Nl)2UDG5r;@}S`W1yb(&aVb~RH%?=RT@9@DC)mRL6`P%gr5{`*C#mNH zd-b%YS~YY7HDTeFbBo;D1zrAf;nSlz?oY-siB|~!hUG?Kp+_|3Xzf_BzcFZnUkPk# z`m*JQsT-sH0Kwr}`bW?K+9F{Q9?edG>;U>#+z2Q;S!fur^(}CfeoDH*L6sNUmQ-3j zsut>f(D*W?o|R%!t^!@!Kd9I-Trg)<1cL{gp9CT{Y0eHL%R(1Xw>YHyz10z{-}9C) z+MMY*Q7f(YBKuE8y{B7JTFXn2uq&(`M!whVGm6~<2JnaOOeyvn(M1qDU zBpT0ITaiW@8Lu}QeB;e%Vv5??=rB^R09{Ge_wMZ0;u+`(;5WEd4{PpM-!BuLd|TP4!}B5;}QQio&?I$N&3)Snll?fG z3ALKRuzsC-=);TP1Tu$<$$?#>dT-DeI7es&>daiw0_WO?!SB&4 zggUbQIDf#j1Q&5rJ^5I5&02L&WuX$i1HHnnEwNRMi@npBA};f7OB|WLUfhIX-UmbR z{_(*@QP@3*a-Wye+k}S|@ex(rTw5v5rO@vDGMa|K6PsH_h1@a^t}MZJsq+P?!0jC$kCUIeSYY=QhYHI-QT4;w?}mHEyYMS|N!l!i}U zN@g>UjNSpY4vwDafdPkdZWa{i!AO8HI*FemQ1EvGH|pmeufaJ!D^2YN|I zKk!`c^KQAU@w?i;0_RKnKR9hUl(Pm#8-8P+!_S~jC}Fxkgf&k)lwawpcut@1d)l4c zaQkdW@0NLN=T6+IVqG_C_(HK2=uWY3$als<#9%WjnU``mJV2sErTBF2>KwHFiU56T zPEAdwocu|><4zjb2AXsyMMxH%fq;KD?|HQtu!vB<9UGBiP%&iFEy;+q5MUCtts2M| zk^V)TQ^w`CN@-sHH@%@>V%GGJ6ob$&V9(ZErEQEOOOV6dy%j9O*bqm^zc2C|t^gci zdq*Bb0`-3GlG&1>2YSxXTfoE#r5&4!ZmSh2TFt6%DcMnDMNVmeK)U4c@(AU{9&iZ| z0SOvjvTdaR5l&N%-P9%~Kwa4B9=UP)CHE2^-6%^`ZSQt$8&nXTY4otR4? z1q#+|9XEiL;_^iBj7Jc!a~A&-n0r>HMM;>2-i(PHk(dlU!5{8-)Mvj@opr9*q5C~j zkKxrW?wMYOWg+ZDkgP3th-^o0fUC{! z*<2V$jh1T2$s{3!4WW4hU_epR`Db+xMBo+~l4K*WAqI+n1ZE4lV~M~yq3DxBKmC_s z4;;=Nv%V8Huc~GRC5j-BrY$UAQUusO&!?t}gJEFNcPQlPsL%m14jss9H6b}%t91_5 zQl<7C1yPWTfRlQh6>ET{Vf&OF$fauSP&d@1bghyX4JDuQ%BO(g(a;|>#5{n5X5{>k zcWt2YB1mTF5jwIOib)H*!Fo>0C)j*#Y#BfxR&$8gl5ArQ6^oIo=_5A{1ne3Ld;u(~ zq3(g85%-M=RY`#-A3=#HU_Twlh(8v5E$n!-f2wWNu;3=h0wh!KHHsG6ChPzyw)9oS z26m2)7^KI=@8HPw$L6& zw>*PC0x|^nXY%Ce$KZ-+$Wcmg_$fzWo(X3L_y(PCx-cI1dqN9ar|YNOzFXe<(Anb2 zZ`>!H)_$&!d#y0wRio>KD3NXMB!d>=uBxlvfxO`bN99X6*_}{`){Dh z^Fif%ISiFJ>|Oqn+YhK8$2<6dCb$VD0RSzRxb~f2MFTGllJ-9sX7BF_GrU{cA)YLr zAf76pAU-9pl+FVIObM7GbtUK*72$*!sA&NjR0UmISwiX{t;8Lbz4oXvrQK&vnC)8z zTit*)RCrTX=S0osq@3uR?i=EYj!j=k&)yS*GhLVmhBc(?z7c%Wz~cFga(GZxJ<}t3F{UhiP|LJ9o;^e0%3=IV3>1R!I z&UBXhEu|8sJ+JHct3udby{9)h2Xue(kUJo1f!O{p00YEmY@FK{imP%P-WC)KyMU28 z#vDhxYa(o+^Zs$?fcROj{N`eR!Bx*2Rj+q1sO}bznb|aA>FPW;>R55OyT==8sAXx- zsmGYDWw%|KDlKs{x2#1hT4>Gpxv<{oie?dvl^h3EzY21Y-# zW}+qlFFfXOXP6`W*oI|JUaP!R5Vu00&Wdx&w~)>Sy|}837ymYqRZxYL!S>T$$M>)EuDhi+tjwIW z)YswO3w>SLX)WU#QD9=M*nUlz&((z$oLy^hD|ow-x5QPD^ypP;P}p9iu6%x(k<1*$^rwM0PBbcH2dSP!Z1c#h#9TZLn1rQ9_5MImEeouf_ zk#s+LVA+=`!M6B6;gvK1gYlZMG>Wm@O0q>2w)m*JY6AXeh&V#m8p!(UfHkL&3T|GobQ3%E zDY}44G1hVwahu9j3{BLA!S9OMRxB){NiM0rt@6e;QHO7v0~X>L)}u~fO@#1U#6ZI5w+K2difnMQ*{o$jd0w(M71iQX}Xz;w1oL69|DQ zNGB)^5}ZtqphK&UJ*O``_DLnjn?N+6}j`LBuFZWSxWGqQH? z#MzIE+Iw>zXpXm{>@U;Ld@(6K*?U&xfDHjf=PWu&HQxw;($$RK4h&Nxm8?B8I z-7_W&0EvlDLHVq;E~HSm+RHckM^#XgEz^NW5Uy85b+>&vGV+5E!i%k@c*>A!U6T7~ ztAaF44C*-%Z1=m)$3C@#uH>AkLToK;iT{qGz?Snq{ zv0-j)9e@%R?o0iM^x6?zMO6oYg;3SVuZox_0rI5^mE2!+qk95}FKcxuO<|d*5ExQj zL`yzti31QoJGF|xMxoB38hcHL`KV{WTQ6Xq_+Dt9zlb>b3MLR2B6DH|Bl!YWFm~o0 z6fRM4vQmuP2h!UkWh)!rp`JvfTPpaFIxiKN%8^nRtrf&7%8-{DyRgJggZnq@M+&a& z^=Rn~mP>a;vlH+};Oi`YPq0@ehlZXpmdHB4$jV6clOs9Fe%N_C$mS>TIw4rCmxh>h z1^k5FSoRW2vs8LFSy4?D0BTZv-+2~QshxH;e!>b^`~%Hl%7Qmd|4vKqlWv)<)#7Kk zD@qzb-S#$4f()LI5gY%A9Rh}~lw@pW^P;X6IPQM#G*p#=uK18P#Ka9?7?eDY0>$sd z%YB&pHw-TQyv4T$BgiC}CH2rT&{~klO+p1-_jROZ>p<(wAyD$RZdsfFxU!|~;M37q zwMDf)vvOg~kad8|;b8eIA?lZm!y^$0O-Luwh!fQIEBI+V7($9j7H5VHPW)H=pg5M; z2H0y*J_>VFcKgSQ5sk&ZWa0beW9D`h3gkKXEwEr=BLuu6is^#xANUH_RR5wnnVUMe z&di&3>}!opqa|c|3ec(#J)aBJf-Y+oIMS@rNOBL}zg9#&C<$&(<^DpPohF(d{)GiJtfA+z$p&+Wt_$2zxBJf~|3c8~Z*kikRI51S7`S2Qm&*8Cj^rjDp9(v7W$qfl_d# z=i~TXez-qy)^nGtX)Sab71zJ7DG3L;A2*9Z04UK4fqFa zWvN(#fKtW9F_-sR<8srLrCB1SMiXFK%F~Mcx+*-I>;{4uhGiUsf(FFJG{?1X>TY1V z7n%YD?ZcI_TH{ol*uE7SU-O9EOV24d&%>kCQp#7VeSLuaXgIX+r~K!F*+qzrs(NYG zH?Vz}&6fTIS_h=h=|i8VA8NwSbp0Ap4tWqO*|5EUd;~KkmJp$AV;E1C{vhvVgC}%U zfN%%wJ!5BOz;=8%)Ivb2f-``VL0?FeGLcy|5sl zQJzc7mjV@ol+9nR)Fr}V}c9EobpZ$`+> z%mf*Z>2Nn5x*M2O+RrLm2XM_r&G=OROlwaV)v5`a2*|3C$?)o|A?&S$Z<4`hB+{0G za$})7Rx|Q&r;RhkL06sx#d98d(M1tnVW&mcP!!G8l3{ugE@ zFu4L25E>5^bX5xK15Px&@Yn1?q8fcVH5Q$4lcbM(0w_jZjK$QJIzFIcMnDv_J@e>w z_7?+iWyq23^hhG?PzDG2!{}GeGJd}RDzP8!`USH0tn&y^JG@a3^STjZI?LQMeAeOm zj`7PA9Y>bzuqFY%WQ$;jo0JG!umy6FNUdo;)H6kO9!-v5AQT1?BwS>olM;DhybN3J zIuZrYazML+TqWI6mT_gBPP)M+Mq@Pmq8>z+ED^Ug4Zq-Lsadgvzv`rX*sWq!E44#L zIR2ME625EwO!&w9B5&;!s`-i2+l``S9MFS?#Wf-EqZd74KQ}f!Tn4jj1-mTy(~Lsk zSKVK&+xa!kN8t!g7rp%p&e6;~Bm~x;8W!IDj-PsQXvl-w3;Xq6vwd&b^fqM6`YF^o zb3y)Ge(kt_!&?Ef4g3D~O2flH*YaH{^3%cIe_1F>Hq|O0qGITCF zP$IIhB?B8qsv9vlH0j&IoYBDeZDuvrame};ok)#r1k|a3zLHf@vjeX{2-?m|bT;&X zR3!LIX3AuZcpM!71riP9M(HPU5-^;05C8PF!WHUG!t1`6jNYOoMMl)8KLYq(g$K3$ zz~z^6Gq>_Lx4(APmE6P~{OvFQkQ!cCnMK1ZielmbJewa}c)YL&3usaM5`_fVAjJ1na@@F9>DJacWYX?=_z-0FL$G-H~wDUAw92dR&G!t z9bGW-i#hbIg~%#yI*aRw6X1Si6cmO6E`| zXQjgOi9NtT)(<&#K6=18ZN`5bbI5^!z!i|mU!qNoSq_h{KsYc;0|_P~ap8FmSqoJO}IwQtq(jrPZSY{b*Z^G2Xn7Eua$K0h_@Uww0} zYVf(y_2{HpDsg~r`+6ZU`l9x)_I=UQi=nt?^y;Nt2<26L5)%AXhu->sKaE{ zGt&V<$e6j372<=ZR4$8_7`;%{0WgljI{Y*oCytn@|ISyv%HFb=`6|F>^C%b{E?-(Q zzz!8DGeWdEGT*e+t4D<7KX!?{*EO)K#hD-weG>@32|eYfLxksR85 z=Fs98vfKdKPb~K@xg~^noYFtr-M8e@))y^PzTcKSXZUIWvRb+4xxbDR5N*YWepJcJ zSiDcv?{as#n7VuzJOTS5IU`H+#3m~9o^4g^5wD^-tK640pX16EJpkpTDgAo|3=mos zq|3Gre4|~nbw3>}4fntrw^j^Ag65w*!wYf`y8fn9P5gX+)n0Tq?T8P;;-2m}zZYO} z*wU~C^@q8t?Cjo|tGJQ(p8$%&_k9VR5ywEymUL$G+fZW^3|BCJiesUHDU3(P2+D8O z_^Aep2$$@w=anx2Q@H}s?MyHN-j`{C5Vn#-27{DY2?d#*AKZjUwB>!D8_eY4N@Li5B!uP+#e z_PNmahPc}^&cPoDAiF|mig6B(p&^pVG+xR*YFEqJrd|9AT>KJ5LIlk$uH1Z8P`3HX zNvBPm;wMwX&7RQjYmQbPP*uy5H9%?1n|2=mhCTdAcb&%Vo~Q#H`5Ep4PCZ5zAXAod zKLw*GsE&Xcw_5xuw}9w0m7B&NZ|5K|tN_#7+;b6l=W?m++V?mscl(J~jqkrevq7{C zem3puYYcvl=jvr)w>dIB*iiJr<->ZbASN}Fi?whkSl3ZY1|T!l+xt-as?D4!nLxxW zu@qjo#8TJ`xe@(B86<_=%~GEPeFE?!8NuN4fu;oDbah4dK;Sld3RS1bwF>>{ph3hb z<^#D;+OIUkQKczAAQwW;SZjn17Hnev6i)($9FGks3@lvq$ZSJ09Io5gpeh*bD+B2J zf%HJDT~zTEOYT#+swb&-A;Ek1tqG4jeq-96?D)ZG4P@P(xx4O&lYTl%>WyD-{8*q}ez)MkuZ-cmz$ed%z!n?`WXi z!fyU#!(V|-5^D70k*wbj#|>+bSl5O|;23(N%g?A<2|@|S+-PCN1g`MofCm;c6VckE&jA%qpbHHOBVhMB&O4g}V(?s4l30OBQGIgV?cG zLAPuMiZi%HRDw}Ox@$!>9$pVfXCU@tI$OkK-ZY^ZDNYZ#W)YpyVJjJ+RS9*HS9)n} zTPfz>)%>)$6-|GX$X?yh-j#$mMMi*?e&@04#pbsg-a7`iU5KvE6@R?x8oVNBP6rE4_)MVQy2`-d1k2i@It}!&3EYt`g+ehg zMxJ#nQN1cRtqu%*gVnl9c-Nq6_ns3(z~a~_;~#QVXLKl~9dGJ2l3yKSyiT`v-2v9{ zzX7`P_i&W(d#}b;0w^;hO|sTHRlt0L1TV`T@fFMe2rynk2OgOmGX65b-zH%~@P}+D zLFpc&JRzHeYeXsXs!W0)7RY6(_0GI7IpH37T59eyvS&C;x~J)xjPw;uEU@s-74g*X zcU%91$19ff4xX~(@%ZrZ_a8cV1*mV7$d4i$KtanX6`h7YE(Vc#dTpl04F|QIe$N1o zudx3n2u*r>FC3DzQ_iTn{+VZq-nLUi{>@K+(n4M+bGg(uZfn%fGlZaWpm6N?DM{HkL%)kRNxZ)^b!FIasffOsq#;mvb;#p(eik&Vff@j8y zC-kYlI<&Rlj#d@XwbzEXYH=<3k+oo4!a*5Dej-PS!bJC~VW(zAo1KMGL2t@yP8was zeZnZ(R&G^4DrMAD_YTzbWU2?d#jtHdpPVoQuvW^LcfL#bhTtK3`_NYE#f%D*PPEIZ z@0;7f$OPI6 zaqHB1`0qL#SaItiXqn`0jgJ{Qxk3_ z>BZ8xD$f6@II_N47Nz?97TMng&Iz@pm+a+Fach83-_2dPTXMr-Kv70o2KXzK)_}9g zEVzZrfvE^Q1fITb_^gH~oUlTKer-ct*Ei%4+tdJ|-PJGHeOO>G>JVK{hlX$l}?Ae~m% zsfA7FfIS0lsxRX?o5eVA&^60qn2|QMZGJ z*XXeJv|u}kfsC&kI4*qB@OLoD>1H-or-+8n7XF4!(#f2N@jJ=lAAq)WS=7ALm!(;T z(tvlNZ7!zD6;T%%HiwXx$$8N$;fkHtL$K+)Z8zsVq*gAis@$^I`}RdiS{a$$p*SFkPgGbJ#(qi>U#h!aU)18M z{98qIP(|??3OgTh2E|p~)q-H%!I|r9&CtEp{l4X@id36d&GeMBs=lSV&cYXZPE^mi zwYK-}75o2fJUwk-OA5-J=adwU=q;OJzsf)>e4k1duD-9>v;7yXgu6$b5g?GN&8uK3WWYq2{Ty->y1n?p%qHZ zsXM|-g;!9D>Eo0$PpdBKEvH~x)U@arUDYPW=hamPxVz(G`tp%Y@L;*h-g%E zGL$!UU|3{4(l4f^u4!Bioe@mkSOWyRVD!?cMf?3&^>p10jmtSVu$$b8dgwY$2((Yy zU_B-+Q2qUAveuy4inaXdr0Ez}Vn2TL{f=9}@DW@7-vMu0^VSD=`9aQ**Og4g)0Ltt zpTu2XbgY0b{o!fQj-n#~@yWemUQ5F0D{B)Pghg-m^cwgBAkvwSwfOsY$k*Lb_Vhos zrDPD?f%&{`R#O0nmgm4#UrJy8&lqt8)Pg1M1n%x&7&NUn@RCyix_CN_&~z~T4H7R% zr9k2y;}Qt#oSuV(j}{4m@5-(jo)Niw)|ab+{mz$5RVbl)zM-5{z;ZU|zj)&%ePmc~ ze_&M;gJ83)snS{~V3Yy4URU*i5L1()1uVE7V3B(rHvX8qeaGkXTf{cU$ z9zZS+MwmSmhl%B7_z%=yZGrY4vInp$;mPdxA!-ET!=uN_4HW9EZvXc`$ZGyhbzG>b zXo`7E6)KTWi~7Z9>=-UcRy??7tY3xL_e80;`c4su3Yd>&`7f9aMdrEQWhY<7-aeKX{R3_}JwBFKu|)5cg5lTf|nkJ$R-bd4S5f<)WwQ zPSlJ_Lj&Bd+Gzi~Wh@GGjBWIPHnwwg2p6fM1th|wPIICJhrKZU4dEE+62Bm$Mim5Tc_87mfR^wg zv||*|(YUdrh?0eMT?4w&8ZRG%UbPcI#ti?OE9`Q6BY-9ThkiK(@EepweLK}fW4^2c z1mT{2`>qx@gx@>Mca}iN2G+h9C;Qltlgn$59ZvC}aCnQdY)y|t=f}o8?MuQp_(Q-t z9LI3~ukCt$*G*jyp$_>=Fy6g@Y6SDOfI=p9MPSI8{Ska1oiR75m|Ee zBEXa(w-@UieuNK(04Qc`+T4C8aIj)w)cy8Nq*9Kb8yx5^tPyS#e^GtuwNM1rekhxR!2Vq6F zk6&M?8dC1(I2jEfEq};)F)R|R|DcD(>Is)RCfEKpG}awoKybj=>_${P60io52KCxI zBFx5~RECvq{HZBZ@}N8^zl8(pLcj(K7Rvyr(Y=&!f6L$<(ZNy|yT)xexA+2D5-A7> zqU0uoV z*sAIbR5PoiMqOqzaHC|pOKwy?dRbe>-VIxvM%vLD7FrScTpOh_k`l5GhLV|~1x0EW z!pqwNiw3wH%3EZrhs}+aW)J&sT=VFcR3uzz>VJGHJ7#2!9UAyf^gzi)scHu+N3TYM zJ=|HgjMfBm0vtMi!ZS;b6Wr${{!C%abQEe3QHh?lbcU$MkbOrcj~>B)_hu;hqF{jw z;q7Fp*imo)mfzajI)nR&R>E5J(&E;qMFBLIN9(&8DgV-eRXU%^=w32*N{T&GodF3x9K^?i5%9ql|sUC!v@BK zfE75wq1{lqT()9Nm5s3-^$9#BIQl6&P03ITalvrF4Hku_dOEOam0F6eM9Hd}oFj%l zz4&sjtm=*KoT9nq5^hAa<-yf`kjiT<7V9b?^&xJya~r$tibs(rN`ta-@o&E3+qE!q zd9Rmh!DT=T8iE)AGbqbaB+MPX^o4cu0`|Yq+0R=cFg`L?#zt`>4-<48S^DB{+W1Fv zI>x8qs$AH#5p#&TUD$RU))qjYiLEiaV7uPLObj%m?GM`Z&8vFB9<=x{;JRpb09lB= zG5NxnKq3&vd;=Ef1>V8G6aOx&YMHH*jvZADd-W~H{`SKa2b|LMaLd;uw066NTz5^8 zQZ*j1+W(Y*=@D=)y!?;&%-+gBiQ!8E~@vEZ67JmZu>12tl5K{Ox@B}^$x&z5ik%=GB7OxlrE5pdv3$s1GvkWsNvz;-MNP?~+w`5AgB zF#+;e;zvaYI|=MEor)Ow6dTZDKMUmyD17LSC%IdcuvT5EqkZBo3W)|-Z{CXSZXawH zdOhB8T$R_|@`&IMLwsH@CnuGZv{8Ymny3fdT|#k}T>yLY#ummxW^a6GX9CgTve2@C zi_~Mnn_;83r*}+qv_xLLAdVX)AFJY*i4zc^*URZ8GWR3-%+yfju&_rwGat!0>pbzo zvWb3ObR`1Op$|69XHX#z+1xd`yq!kr*LiMPE>9fRFaL8KqgRZTf+}|gcYiHF$*~=* z9i`T=0x|>ny_nu)D@#MZ93ElIso?uumiO#w#Qa3paux=&SJ+7ICoo zcVc0OlDZl?y{iFtkkpN8&Ou_Ni~6B2D@tz}{xPuiMXYRn~b8onp=cLkVV71(YcX<%OiloA+054ShKlaw`0FkI@aQb~vPjgH#A zWed0U9-7Z8N2rv@g4q}pvtP7XrXZsXO;SoBXg`;&UZGJ(CBsr`4yR9M_L~Sg)EA_~ z3qMKL&s;2ez}XBIKWHNDLNSoJ-j1(7Qk*E(5PUt$dcgmzCB^_5BbR~#m{d0GKh8DPz($^ZRmzai5pRRDTYf zii%+Jo|XW{f;qkx-Gu!XQE;k7NVJ13IGQmygNPmZTKjMP=YvCEo`7<@G`~-6TCzDo zn=hK$>_KC(CK&28#91xXvw7o`CU%r7{pW=ql8st?E5rU zq6q?%1);AnzQPO$WJff12RI$0CFFpZ9^jcEJ;vG|StfAm;4Y?}xoNnN5+#HfD=;wn zkNELRDTkr%WJCosR>71?NnAkDO(W-*7mC5&A7_l6XLhui;v25 zKK{YVOXh&vy&&eb>eh*wOU07C76Y-FEtB4Pn4v$rTWsCGT{J#Cq^b|$z1><<#Z@P4 z4Z&f)u!mUA4e^IleV82W<^F!I6&?Ui9O)TA;m^TFT3E`v*=f0b+QIc$%bCL~Ki6Di z_R|uN2|PIV!$eoCYzLK%>H5-0L3L6STkl|QRGk9Wo?WA7FCpAF)X&6rUj>)VYv#0@ zZV548m`&W#Sp`d_`)2MeF9V8rVdPS=c>I)JQ1^=V=}zx4;&UVF+fE<*sCaM|L>`J6 zj`t4An0vNf`H1jM^a?aslh+k>9ozLISQI+8P!mO!3u;n$Df~g&EgcRh4A_V;ba+g$ zlzp=>#1bynS^zT}=zg;Z)^QXpW%_TP4=Z^U_XYgWVb{ZchfI&{J8x8qIdHN2cHMhc>4_oK zSB(Yl-CfT?Ox-Mx^E>vms^xcGxfkrj*_9vj)ax;ge0hANyRI9aq@kGKEuAUt7SEDS zMuNjx-PpL1o=l50oQQ~L&`)F_C_iNuQ`tGSwbD?kpg}QuA1#~aVyrV z0LcQpSiBWtY#2WnyuBWYs4DM{9Bi!FiZ!YUw7CvhNU={17J}Wq3T~h-a8tLRr33kE zAZJN8_Z3U%?K!v3*B#k#c3g>Fu{=H)tr+E0K@ozm3uvI`D-U6>xOo(Z%HFPXx(0-c_c6k<|X+F!sULh4Z=4ok2|GHt3YXeTrJJ-Jn`eVugm({JFe|LASfjbFTz zyQO^>kD%uAr7`X#%`dB5ocHYZyErGzbB(8z5Bw=#DTB7yD-3=;Lc-7b_!vbVUi$(4`c2Klx0|~E|toCl|{1?EF1K^OW{eCPf=ZeP* zyQS^IbMi)MySz#Iyl+5D=Gkfra#hPseM5e(jM&U=Af!- z6C_GYH zvepOPCi7{-d<%Y$C0XRd@JhsOnYBNbycyZzZ%z^Tt8+6mP~QB^$Qj&e!dcvzUy)sP z-!@P8&pj@B`RR;hgO(103?06gpBzS&fkl%WxZ$#zVnH`_AO*XnQnW#{o0?}@;yK+` z^bT8+Ziu9Oz-v)pe9}(rZtl^*$I@cH{e+R*Z1rgSykgVf*2D$7+l1!eP{p@mC%m4& zCN8LmL#L|I*yi2Wg|g>~C%)#4(KHo~+ZwfsPcDyCzdFeue|8+-e$h3hf~&}TZ>lt` z;^5XLwEBzXnes>=Y8Okh;zRP3{9th^2V$*4qpq2=0PiwAcWf?vU0}MosOP>_D4Ez3 zUJkHk4r^qbFeQTL0IIlZ4)j}mAIL7(bnBeT^@HrvTxF-&(oljOXon~m(DG3em8K05 zV9C#=Ccfx}MqCl3-0Bo&y*5pilsRk~$}R?BqFi$Zde=846`rtr{hrw{ygNo~CA4-` zO+yg$E2kmCX>HvraV1NVcWtfZ4W;%^S+PL#BprA*;tL!@pZ>L7{|9mR$LN3jtm|bE zW&tbwyL$H72H0i>2_5Zr|aO7Iyp_FalfzFyhQj!nJlrRSBFn zQGfTaiB)!HJ9RAeeUcc34mnoV2f__s`&(V~b2Cc8r6&6C!G8A6#W}w_&^7?P+nSv< zgJSa>?7-iJ(S1GH-Au%G{l;aMibl4|n{i`3ce}ET$8NlKwXpK4b=>O97sT1Oht6V2H^PM;t;Y_h zN0HM7V=ZgG9@w?g{8leWduEPqasL6lW_(?__`*g%^36LxwlJGh{bX6v9~4K8WGDw} z>PpJ1?Qf?wFuLU$4Q)zPBHu-N8jI59^&tAffS%PKDbwG@zwoEgrF}WZRge;IHsTsf>>tab~RqRM%OU{=KXo1Qn>&!CL>BITz zWRn)hSpZ-T9G)6Bi}RBuq`ztN3<1M(Sv-Ww?hd;#WufTZ&;g+1#7+=S+0vHNkt~@_ zt2#L=KRa5|ZKsy_x1kQDo^@Fc+uuyxqBNh%K@Jl1a(6K@!Zp~(uC~A66ab0|B`~-D znlCPM^p@kqPrS{&<$LeB=TMM^0V2}QTV!}a@}6O)wJdFLpRtkVz~C( zN;yl}IA)t}W4Hh|aKC%E2}(@v%+zwNo=DN{2!ql_(=b6>uRA5>dg+bo4e|{v6qJcx z8|_If{eaa^{l@>}?LFY_Dyy{d`|jTRK6{^j&pG$rb9*o8o%DnNp@dM9Kqw)EPC$s# zq)Sn-fr5&Pg%Jy)C|JNUFp71oqo0nBGdiO)`dQ}FoP5uE_qjI-;(x~Z{eR#5CUWoT z_w4sw?^@4#*7H=#esSf=N?gJ8c$oZDVv~aQ42+E}r3q206yRv&E{6OChJSTOaK`i? zGNfx_nr$1C$ruz~3-ckepa4A*LKm%Cs0Pjhha(>->XHh&5JytH^N%JJi3RSw=|&tJH83QyfUQxeb7ENRU$r-J!u zjL2Rv4$QK1+^%13g%eFlg3j@a$MA!$~|Wb%8f|zL6_-NlqD_rrsi84nemjxzXxVwE?~7lbRW6 zfH}P4E__4Dz5(g^i%iTL5ikCW;Ki3PN4vG-VqnxaFbEOM^5T!E(X&W*U9i7&l@NqyN?-e+W$gdCwrIgw!bY{+TZqu|NQ0$` zG8E>kh9M6_97DPl4csaQ$mL?M-V+4TMpbt zJ`d%O*4uU444g*wmuXyb>&N9Jq0fUn3SeMC>mAkd2jwdMKa1)|!0d%E28 z%6FThUu42nf@wQFY+js?3*n871s^t}iY9cf+H{Ts1H4N{`?f82E2qpK9?-x{YW76^ z=3OcbU=qj_ms;;BsRbQ6DzR~~14^k;2p&67f#L09S+MHA7gsE^W$%F7f^QOlae4UI zU}4`WgTCMy1-4@=T)Px$-KFtp3`WIcnw1CDHFnG=nQc_R@St+{M@S<4DL-|4*e%&s z#*E zeQ%x@kS<*lsR0e4!EB(Rz<~gG`UK8${DiX=0hZ5yK>3JI0`Mbt3W+7obe!igWe0%J zu?hj?*P9JI4D%;Q3@9w(nFQ#^5dr-qn@24Gqzu$H;Le74GC30P!~+k2YAwAC-kyAW zvnBu@!lw{uf{x|{aKax}hC#0ZOB?YVuZTCo-TbAesKI5d*hKYT$t~f67aBqtnz35arM8~! z^TkM8z1?4_ZC%u(KWScdN=;}zs$m#K`lZx32QolW_0+I+$!bwFH{_gMfr60(Wn^IA z=0Rm)=Wa<|+L=gUnxDew5C%ppX;&^iP%T!WJ5+vEHIa%N?E1{ALJ9fH9-Q90lfiL~ z5(TZaQ@R>Ch@f1dNjHhp)%03SqIgA2wJnsaTahM<;fqB{T_svDIftb1g_vS7i;WRg z8d=yf(+R-%krc?*IF^hFDcT9yfvqZf{F4X##Y<~zAt71NrDe61Ma`2|PN%uP@mFJt zZv=^rw6$+_!&+@St1(?sG=%OV}7nSqFH`DDH6ue#2n^_M7OP2 zy`$r7cnw|A@mhGJ0_P_#LMGjQ7}KJ{2*bK4V{oVveE|u=k!1|pNk9VdkJ^@5ZH`JP z0W_Aitm%xVct#;_cC7fQDovgfr#>nwW^Kvt!*E0vuz5vBI7K#hhw_O4h&GD|4N49{ z#Bzq?X3co4JlO-6k3Zx3v`F=WAMq=KVf@r{{vp?Rr23vtTQc$GE~CjgEiAD z0ermdJ&%g-eb2NgmDBN2zfup`qcDYQ$dZxvJg)lj^4E*^=x-AqWV@og4WR`)b_xf7 zBcg|BCS8xKK@4tkU zhjntdC%Uy6LDaBvsxF_NpwKr_K)Saroq~G|nxOg{w}QZ)t8BP__2pfb>rY<3QL;;- zEXtGTCiu%OTkjf~sM`5Xh?T^$C3LsGbIa1rU0&lfjAzo^LH*Dhp!+exJ4}P9g)YGQ zdvC{gnRArm6{g<_2+FP`UmEQRal|aDk|)8ipfoUZ!oWhn$vcO{W~+9B7wCjUf%1TJ zlTPtDLRhoSW1|(t@4+&jdlX;qRJAt=&u}!hfW$mUgSi<;?g^XX{(;6ryU+HM(9< zqMT>$7b^wEJ`HEU!QooCroi^Xr2_Wuk*8kgxNbkB%SF)SC$~*vA3)Yo?e>@Z{cUuE>_T31xo zJu}ZT!)(>3L%9rzo04lBVvg2eOctDY;3q#)zS4R}57_=cvCz!aqRp|=8@Ku!@Tw?T zRS19%^;&<(C2p~Db7b^~OU5A+k-jq7dh?_wr=D0lZI803vLIjcypgFuI_0N*)va$n zRx~a@zrQqi(-nwZXyJqlJ%IDLLmzRf9U7)1utpWP6oeq^WrY4mg)vN04xnwEA~IO2}M zrVx{q7?at75p1j4DK1-N6wdHrf^K*V$3rMJUD5iLykNqxf}wHkKXg~j#}gpDYlU^8 zaIIU!5P-e1Zs!(ESl`Y^ij$OHYZf+^Y+X_@gZWPypBW2*|C4$76XHJ ztnwrmu+=>4D^yc4-&8OVxoE3x;7b=hb2Y?pAaoZoXd#-Sb+EfUvQPR!E&^+~Ubwl@ z0B>LJEh@6ArU~R}RWFoG!BTUh7E6ruiAo(`RygRXCF^tY3u&yNitW2h{z4wcZ*Qrf zBT?$KopPyAu`!{L>-CovY(p+cTHcV#)fBB_sQhYvK|zXP0U!HY@kin`*PJuqw@LbA z$7A}I)Op_A@dwmiv#nzQ`(1}=1GD9P0}y_D^-_9*6-5aECCCn&=8zbUdXkZ#d^r1& z=6PwS;6Wk*6GTlw#l$m(+0Z|mtYj*66t*dw$Tk`^+VJ#lYyrNQYnF${b(-n z6(D>)3p@ZXpsmBtp4sp`M^>J_o&Fl22>C7ArdJ&&uDj=CbxZ5q@&MbTh6RB3{iO;B zb>6{Sf(j%llahHn#*!jT+V$7eAS`r$xF0xGsS=y=GjY)~;-1JZelPD^wPBo-=e_kB zcBOX{yYAprCQ3$a=QL|OpNqHjFL^Z(!BiQZ>YwfNvOQ7G+7JB7C>PjkwKAl`nH<+) zAd&j7fPA6(9(I2@!3pBEwxkV9k^wC}$j$+9QM1sm@NZ+^szdQP9~;lJJ#MNcw~Mz5 zcZ+ujua#aSKI6H#OzHu~V9*NMf|cep;Z@~2bgk{K5hHMV22YxHQfnM@+VWV%&UuYN z$IWjkdV@Whk(|m_D2tVc)oFDPFprP6&wp#?uSkza*`zl0uL6cXgPqKKJV!t}j*yKNI$L}ljQQiUq-N?{ zgb;nk^Za4K{Rg8L+)cn(M%SeBAs~iwi8*jO+4|g(}hzH{;$); z?XNj=JM(smr{8xr+i`HZ+We#J0qJ+4tk-I}ik=s+lo`gZL^s7ou436RjMkd1--BEw zta(I!==<+`N|bC<~NLs^dWS zQ+xpV3bV`rvgJ@@dPgAA2x)yH%H zMdU+Om1Px2hd%CNL#H-g_;3!~US3C-Vj(S|SKuL;ms*G2Zt%(4wl=b`&)0YD>M8~2 zt${v0+u_Ce4I6dS_xG*~S}&Y0pZ&he*@f@8Oy2k8byuH#vAE~E)1sx$6ND?FsqPF) z1+V1vg%>NSr@g5GPw6hthT@DFw@x`su9=H+<#ply6>GzWCn%%yn{#3A7KuGnM#rrl z{)ye2U|A-SrMJH5gYYca$bS2-_&aY?=Y$;{7r9|>!MmAT8)Ua=dEccG z?jfZwfbtaJM=8V+*)SV|0n=1v3!>YT?dbL*$b)TZZl_;KwvKj{%CKZj{D02k+nGDh z$O(Fq2f(HvdfXN#5U zusJo*?5w83j7+=nX{g-7AAmF$i3E<}wT%8o;sSBPE{?VJvi%DNNAD@&cUut&H+xLDjPTq0fwV7`umJ_0X6W@o_}5mq+7 zA@5;{G$c((L*gr>0eMUskhWNX#Q+=JNE}nlKZ#mSHXQAe+J3CWr!!ZjZ_X{m7ZS(> z^vs{E7s{ZYB>s=Hm57-gWmNt82X8ucI2gO)*Uv61qPSHrDup^)^1~P<$%DdTG@Tp< zsfUMTZLHsd1)-5df^Av={1kXT$O$|S1o#Wmdel2s9C|a(+dR(OQ#;OrkLH!oseOOP za}+I6HbG0L-IG_jGccO!F#w~OVC^Y1;b$f$(ZWaIqeeLuL1=oSuBInt_&iK;2*iit z6Dd7S5T*`^AHw{B-J^LwLuya_r*8hD#1lw}2#?Nu2F+(6enX{hKQcSw)3~I$sJVRoyb5o|-rydD9a> zxpzy}3(fTApoyWbPob|5GBS)Rg{T4-9&`GfIp^pnHdg}vtpw`gzArJk1=~UJumCOauO;@WYEr{|CtS^CxKCx;3 zMHgCdGak^2=-o@3m16JS^hHU{QzL`4fn`OtfmMyZ20ykGKjz;e^b0xK@vOhrNryRCEo*#=J*wpR#TK+BwIbmAx6VGXK z!BEa(!!{1_u=RnU3v-0d^^&XhpQt;cu@5GtqAxcVHO%O$YU0vb6Se z>vIwtcUvD3*{Xw^kB{wO0{A``Xx=S!o)TSmM@}!sDuxdFW^%)AiFhq#TLfkaQF)kc zmjX;S#c(VJ2iy({7KYCY!#TAWDIjhFWJTIA6j>%lLE;O{*l>H}k|*YYJpI5#!ymbB zzbq^$p$`QKB_jBh4P8;xqH9eQEsfcE(~?StCR$2pn&xRk|!f{Ewj$f<;dGK;|bij67f!Ts8kUs`Ed= z!MK+VvQf7DuXmO~|1;BVAdm-(o`xiwe3=3TrFBSqg_b)4C`CkU8cKzcN8)+(Gu_H} zT#l-Do{l18=Fy+vCAzXb)3JH?KV9q6&bM`?bsx&-oHUxvBVKcc|rx~CYx5z zj{w}}G1g@GdgE*CMd1bZMd5Ser(0eBGcTHtXh}0b_ssL_Bd~V%gKcV@$Cz{jHcJ>d z3I2M!7>GZxq?Y)y;m2$rgOe6Kg#~#`lwF7yL7@o{fEL3dS@z`$)SFZhf!&p6WY@&^ zH45V!Weh4W7)!)U2v5k9$fPEv)}-oT91>+Qtgaq|1gr^FcsnHjl+PF*q}18JS~auZ z?9}^ogvd`GZB=7+ln*^F{#Ya@<1HP#IxdBl(S03n?0680Mo)MAf`NvU6>d}^;w<4M zBGI%syo=RIwIknB+0H@f*q&WLHHN%AM1Ib99W@=WiO{y4=;mz*+J>$`Kvj-zASOE3 z`@*k;cTqJ-_tP_)a0dXWi=6_z0R?APjG~2tZP9&H^54$zv)U6phL>x~#Q^Q%2njsi z8h_8>7G5`p%{b|Y&qucpD*B)*+vlnNQnust?xgtn)4CiN^{GdTuebDv`eqB|XNeFSjvWC906SJJ!pYdOm-^u3J)}^#gM$eA4WC z{@M_Z#jFUA?p3kxmeWh^916^>3*aJAgQVQzY(Q=O-55}7T`c&eL3XJc#|kt>EVFfe zt}d|F^Zpt?7&pqiAGiDTIBflEHBLmW=h6l3jvd?JG&~JGLHnCf3|hoK8-ScsKq72i zH&Zqh)84+N0_9l87$_XmWkbF~8E!h)AL3eTxJW=4HBCr@D@gylFi#k)D&$@HpN6AXvIqe7ihLU?j9{^!t;Mcr` zcRySRphvUZD`PG9%RV6y%MbFDH)p`-q%4J(zG*NRhm!NVL_>vp5!Y9`o|J5|#T z0=sjmp8NWBXo|VR>;%t~gCuQz50)1Ncs2_khnd$8*n<%1El1yFZ}D?(Df<4Sg%C2Y zKkK?>y>G_03_!nf-W!*6ib_220sUjph)W<&5Oul2qkig?;(rmJvs~=KS4u?ou9B#` zCk=v(g+3(E*7Z6KLqdSK=s<7USg)kEg^5YMSHH&UHJ+&G^>i>$Qgw~CFJJJB#b@fN z05x|{PrnxBp>nPk1EE-eyP#$R>GRFyqBT(I)_Z~o9H3+jQxR2Er4eMnMCI-ngK%!I z!Hj}0xOMnxh`>u8@0SdgY;~kh}vKT19q!x??K+6&FlSeRQ1N@@% zBtoW}`BFhZ%XE6O+j*uxne{M`j_{?ahIkB5dazEj#f?Bqwm4|u@x=|n_M;Pe9?8@S3LmLLZC(Kg#(DGpRs=kv{vZ5F`Da zFJ0%n35h-n>73569q@V!pp1`*dYs_pdrUSKhY|AD)~6wBee;AMnr01`9C8vNNT)xv zfhM!J`7!*J5;gIXCuJe;6wZy8=kLI4ra475^F#NlTIazZR;(ql2bYx?%1%}-*(fA^ zjUXSFHytxv6IH>fj8@84H=Oa#T`63gi)|xGJ}kV>){)7;D9n7H?glQo& z_Xw(()GJ1h>y6IvqOx?a1Y>o@_1xYffX?)XY3pxF`Qq`e zQk8B}9!s?#0%d1@S6;^i#6yboPJk{jUwImKEgHP6u(JOJ_JWEYd+EH98AW3e>^%h` zDj7`Z+M*IG!h;9PS_AcoE*~3*phv8LE=36+Xj&4p_PM%H)GEkc2EdsVUGt3oQ~DT-=E1OkVfEwN>O98AZ4}L{it$a{hq;MSOz2bYIaiCx-y^M~>slcocbbO7W zD*}9k6u7}dB(Q{LCYmIgjhIQ={$N26gAw=#Ngn{7ZnGINMu9}Ui6;=)LTcE+IFQyL zRZt*+jN7qh2@s)aAC3}cF9CM|q%S11Os;kl1fYOInX95`!K+N+&KP#XDnync&W(Mj zPT}PuQf{7{5t|C02?ql3E|DsMb~(+f+G*6{^c#PL8PF?9e}O&&|Ii8wdQ4HctcTuq zdaBN;9C(?@85|XOR19n@MY5I%RDk-}>Rz zby<5Ag}1r6+3B7hQM=a}hUU%`wa|C2USAzNP4h0tNP;G9WxuXk zJ2#+BoFBiTx#9rKjFFp5;cZPcr@SJ#kJjnY58X~zIup~Qn>QL~NT|3AtKhtM$0|Jt ztq;!}{5LrIjjr~M+u&lgZ(Woe55>i@qBUB-?CI5o0TVMG8xmLoOhBCbBybxcW3bus z!ASzDb9EyMo2FX|rK!A@pR$0vVCrT(y1obuQHty-=t_e>YBd@MRu_Z;U@^v8IVh=J zDT{x=uT(i6J)Qo@in7Bq?-!dr5pp6ZB3XL1n=TEifUph1%!K4L@gT zjb>?lO(MAmp!>aj6-@l_=Xoofnu{c-SOk8gRr{CWnU2wKx%*1{{Q+p*7fV{{^aj`0 zm7%-%toVZHfD3eH$GgFAgZq$7eTjfr5`;z1b!j~UN)FEzBh-Nat7UMZMchb9En+XiEfyF#o+7R0nLFn~ATGxlYJ@dQ zNeH4Z-hv9t9^iy@ZqNx5Q~!kLHuUC4hh*FWG>JaWp73@9_o`1!BD=q7B>v6)< zXE={3oSmPJqJH#f29G}}HcC0w?wYQ4wH1pNC&?wu`~Hp;AWE*b2qbe|lf0XQ#S(KB_KQ#tlzp)O$1xb<&7 z9B5o?+A5pdJ$K7#lHj;7f?NXU7S%9Dy3`!BYRz!9UNB(kT9T^?oC1e(?~CkTtJ3TJ z2nx5(qvDsq&=;Ty7;i*X0iq`OQ>V!)!aKeo7&K_?KY_d-X^6yoK>Ck#BgV9T@8o2@ z(+ln2W}ZmEYKY7JUh(7T-54EX;8UE68uI>*zhyJXM@TycaTnKpW+De6B~r1UB!w50 zag+QPOphUNMKq(ZhSiM#sM62mWRO5MCTdU|RS2f$u@}>A1dBI7QUlmNg^4!8&{YbV zRA0hUl^ZZ1vf_oXVStI^Fv!U7TtuI!?fju6vyY-OI~wp1L}Z}sY8`KdhvU;Bx{_A| z4w+<{!{ss3C*&X`NdrziIQ9hYwz&@q?e4DacittBSCj6sYy-8bb4Oe-zw3bH0$$Gyal~k)*J9G5{{4jn_rwY6``^`W5eAiXpt~we?G#MY#>AFf7k~wRUr7$iB5dnxstlufi#)>eB_B!^p_~&(m*#wJ!dz-uRlRV-=3?}y2Md9Qb=G-Rt>{%CWVb}a(Q@&q6o#5P zc=ssZCtZg=2#)9!H2=4B{0H=jeb7Ggtc-nf6y4g+K^h`W^M1BF3=ZkGc1nFB9COtSg_6lJ{76xV;AojFG7*@oA#k@(0* zb`l{C=dnC1KnbQnCxMek=fI!#Saw^s0;gE}@S&i8Sh+%leEMY2t{&#ziXS}K^5AiU zYrs)XE`-U5f!8`yMG(-b`jj8($%X#gBX?(}vEc31s!Q-}JE;O9nf&>}cp zTApp(Db(5OH1ay5dxe3bYKTrPc!hX{4ttPJzx7>VV03KEuuvC{4xh2#s@rVo#P#P? zO2Wx2<6}P6NL^h7*kVp8!YP;JDsQ{so>IZ?%@4i8v~Xx?zNNp$QECGCdyiZs?>czC zyyKQj00myi_6T2+%`vE#!_ieYmZ4(Al#wZvSdv#-zmixZg0Rnrs3WQ;a7KijT?qK_ zMZZ@`-__%La3AQAlt}^AC%oaIZ50|&e`Qh%s3o%DRS?4oTT?-1><^2EtEZ|ek0s=m zqltzmp=ws4@1c$ff+#Z7stPEif>Mk@Nto!Zj3ypT!FrQbh&6qM%KB^4+^dt+?Ohk= z4TyE`ndw(dZ`X1bjy;ls5>d9$1m8;?#@ zE;fXNWtx4|3hYRaI{$93pT}SbK^=L0zX~L|PzW7$#tbVbL5s)iOdn#8fi#;B^Q}C< zXS-3>Ef@yWSRsI0PSA}E>GK6$>l?##{odBc+?*99a-@Wh_Bzm?p!w3b>^Jv*@RjV+ z)~9sAkB6|QKwSyZl6uSLk{8tCH{DSQ6{{=PIxQ^N(96r|Z@ehVVblsC@ci>EuPC&R z>%y?t$~X;rgaj-qAWY{7C-}i-Hx-M4K7KO99x?6DAbjJ+lkB##k12q()y>lJ`NP=0 zy8;bDX0St;mSD|-30OseC^Q^0gt4mO$}@qc1x6nj`55^Vlo?s~V$6zxBjy_UF_>!D z`ut|e_WgRdW2Nh3ZKSTa{$|JucwTQsvyE`DrWZ%@vXcxd2b5dX18Q(-p{8S)1q|8! zG?}0^B~ilO&<3zcAp~Q=eI6`fMeSE|rU`NCCO9oIBxRaNp&ln14LldVh)$Rrsg|hy za8OsAla9;5@=X|oh4P4?b>1=9V*waew6F+}tuXJoPu;O|BJ|$>^_|oQGdh+Y`Ws+9 zlv{;hZtcWu!If}4dn+nn;PTxu#sCR}LHa@!A&LW~iJ3v-m)2+CRfQadau4J=yryg* z*TCAs5<&^ko*=A|gdb%{AqDToY`zj>2@>v(EQwwiNn3XVg)Mi#fBC~2=GQ~TW|n2R{TS>CkVk&;1xTu6;R2dWY6+HDy#9DAT`iBN9};y z$=V8ML{HmXp6c}(;wF%{{4^5Do++RYsy`~v_(5hV& zi;n=kTeZAcxJ%L=SuxQ1)eDhSHjo-DDul>HB8cQ(HoQ=}4|Z2YZ~}%fqIbH~+T{;# z9BLp}zrPBFTMvzz{;gm^Th<#VqSn$CIF_dh`7PswF?EiHF2QSz<8ox zNW-dS62dFizKHutk`hqNx`$O9C`?v zxi0G88uVQHIz~D!Lmf;?<6rLh6&pvsGg}2`=%dF=M65g?5jdpg5w%O(|ES9}+YUlJ z&Y*Dmfr(%iOrjPbkQ=Lw&jQkXLP*Wk>{sd!QCD|zLZZq8e&YNcd0Io|LRMn)%&AFe zCC|R`^lWy9n2wZ@K${g?1MBjoA}u^&LP&1kjrth{0ncQ3lEpz%;GleFjQC=7>qN$G zJZges0sdgZ25>Jq7SO3cl|t5vU|V1S$6jy{k!|nbZx;7^vgFRk1j>JIXYp9fwr^*r zo%VJ)pHIWq-q2F_hB;fy1;?}DAhGmZ(8plizW7|Q9#m4VGIFLOc%gw7rsL`>&~VaI z1A2W;FjgCSeJ5(=vfowcn>bgix^gss(SnV(g@##>*xHyS58bv81e@R*dGB{TUAc?J zP-)dfzua@Jr?5CPA5ps#*<0L2O+S7Q$wwr?cCvL%|<;7doy6UK4_z z5CtxQihYb?pxg>UemG9WT^QOCFF*O%tr(E-$IjTX0J9wV)jK;^b$jc)?d&OigSK7Y zfN6OE{2G*zO!#zQG+8<(UxYX(PGNaIzsm-X7k4SpPFok%p{8WfeXS+dg z*nIIGejI7;BNxJc;6!e8q>ujn|LL2x=@ooCK~;g<&`@iRb2aeUxz`QVH|;Hz^+2*M zMSl{;ndg2{h0WZ)0?U{|7tR@C9RW;7IUW4qrLL^!v0!uvJd2iAjDIV8IrMcx+hmvS zWT)SLrntTJ$J4jnvR&LJB&P}|-*mEgLhJV@pK#qt(g|#tE5%4>=!Ff#0nZ#VhwT^M zMI?0G1zQ1n;Zp7}L7iO#&iY;8uD5=B=JwmcWf!`@YQGii_SS!FJMl)a-R}h#!1gf~ zg!Eo8bGFLPWkiKP>>%B)t$`0I&QHF%tIfIW->X0oD&z{e9LH}0 z_s`Mu+ocH#_jWM_yqec0I7hS73vdLzHsK8I!UUTtWBx%q4Rk5$h2!g)ohYPzh;oIu zWM~YE0~$29&Az{0I_An_*b1iZ6NSrKd&N^IKu=z9!HkeMlZ(9Nb#_gDGOmh>vB5w> zA_e1Ds#;Y%_!&1b;9+C;BQHA}ec`I$D?vG$kps(^zeRVTH)BANZ%?0_N>Lo2hjgJT zVJNvs7>Z$ zSEKRBdeBHS6I1;t`#*+OHp5kpTK9e;_&o~ylo_h3QE6CQz~y7?64V!PC9uCJ4oef_ zka)JTB9uQmESss7sK=|Vk5t0o1XrpmZ&xl@T1W!t>W9{z7Du|~YnJD$Kxdtt?rJqv zgJGqpqTvm(ofPi7;)Dx-ZgD{NO<5WapmiXRkctXWi;Gnl2X~oXP3}YAZUm<3k!-2j zup5IOI4D@?N6R@~t0RzkcL<w*QVCp0tq21wHHV2u8#cYx zPMo`+$++C#q0w124#42}o;9qPr)`_`g}eus0jG=jA+pa@ErR!)IfOQ2Y?ikA3wzCH zd@ZMh-52Z2#(db;DJN_J2@^t$#*hl{lZ-RybPOGO5L!V!*v}C%&tc5>t%b_K9;j;G zk1kA=ZD+5;sfg8=P=^G0+BEG7Y+srVrHUVDEN)0ZX>9`pkwWC6cGsj1DiUAF)H1>I zuq`u!Uq(~{w+RIksQr{(@&|CG0ZKr5a*8{q#U^$twif~bKY@{gV)7L)ilBlG$=i)! zf7314@m%`~8wZc)CpX>{x-V^6E`03iS+8mWHt^X174R(!7=bn$+quGjRhucjj4lgpH4Un4hz=VxS ztjMsfF#I4)G+|7mtXg|OGFeUNd7ir8 z^cELj5vMo~Xjp-oA2!Ek5~eu5vJtWnRnu(_Ize}Lzv&K++KSCCK%TFg0dyT*`wkUz z7`jwR-$&UlifATQ9xCxwzLxHV0LJcH7DVnYxslBpp*l_zr^Q!NK_c6S)Bi)7nT z!peG0_X3gCs&&i`Yre8>%vO9)I%Ygs=p|hi+c||z9JpFJ?Du6y(-wn`ES=`brjB{% zdwn~QReP6;Nk}FwZN!GGfXN?}H*SiyG~f{u!TK0*0jWEGU) zRQaMO|99z%h3 zW;q835+`nb(-T2Jg`!Q^dIE!^7}9mXHX003#%gRG;YAESO8+Q64R6XN{Ie38!#@y3 zxg+|{;arU3gTmO_{8P#ldHBz;Ci<*dto`~ALBTBKteY-mY^2<2Iz2(_T=sNl-h%bY z!7n8RGsZAQBL9QX`cm2rR(!c?vem-%LNCj!le^S>REp*C($8EpKqFB4qSimb&=%z! zC)5U|ooKb)Z!AQw*9)4phz;6=YWpJ#Z$^k*jqX8#Mi*A zx%DDq0NOqn8h>Uzrq1)ns@m^*?2(h6l>Vs^wO(xfA6WW|@niF;S=MSbPyf3R)A+8X zMPEDh3T>qSEqJE3Z@)$Rd9TF5RROfZFM`N)JR*` z`_;fz3yFNY_)fo?>h$ANf+L48mlg79@7Kw z)_LpMo%vMMK=J(3^SNSBFD#ME7SvTCq6{)88`R5H)i7DHkT>9}-TF4=AUfu-1*i25 zKnQ<(ex%sZwv!FV$JjpBMCH)?F}vzwCgx&fVxfm;aR+%@P zLHiag2+oG2WV!u)swB&TuNK>tf3@J$Q@%=+3@$B+lN;|K&Z{YfIBh*V`=#8#-Z1;6 zyq6mARAg{+1(Z2 z3y+okRJR42z7@DLtMsiHtblI?Y}?kiX+kne6-zK%UzXUfBFKlD zFLKbI0srey-$`-8(TjQUvWp!aUJi#jC^#_zlsl_-W+wM}5C6 z#Xn3$nQKJQj1bRqvcLtW16nf_#=`a}Bla2pjx$jt;_mU`1YRM%nJowkK`(q=(Q7j+ zxz>l+>p{XzgQ&HjV8F2zUKz3V+19_LkbQ^2aqgP@_U^@D{;gIzpM6Jj>ne+?wZX3l zpFPy^2|;;Qcy~BI#!0N$@g`4zu?hHS(H3AlFkaQg0<#;)+hubLjv&9JD6Pir+q%0H zGylN9t?l$m`83evt1Geae)CG>d}G-ZNWZk#mSa7=3w-xsU@5m^Me*VW7oD&Ytd8uv zp{<7u2}mPW1ScR8P>tIl15I0ViI}A|ncR*eQq|L)eN#9aGBl4K!N1vTzaIJHQC~7v znz$ykhQ`kQ#@=Dk_x)kJ9u5LZX~r@pQnSB%2eU) zYOY}QE$Fi6xd%^2gAiY@%}fWA6r=3dpqyCMr1f{~mIS$bXVf~sU?4v8Yw++N()dGX z3@zzVJ#S`_^tlSiQfA@ip=CzN1|w2gG~n0Nq>_}A^9CPTwakzG`Mc*sn*pU!sW%O+ z<;(N_&hFnr$W->wELBl3nfX&4SdEjtUvQ?1mPQI*V9e}px<=X=E^2P6WA&l8;k$1j zL!W|5?cG_V!xxG?l|WBqne?5LkWrh1Q>0e=tN(7xvz&26vRGAepvX#^hY)>9ZVRPBQsZB;m+ z=L%=cV8|2_g%6Z4tzlM+T^5#bb{+nR%ixc(Ux4swHiemM++OV$!}I`ewQwV#gwQNIi|~ zF#_GHTd)^srRTDRPSQUapB#?oR3)|5&)fT-%6$?J;Bg=W%uz1<|8NzAyCkJKd{^W~ z3KIO@Z3i}W%U^Vh_pKlH%5NU&w9=`syH2sSx_t#k&yMt&zjy(#501D1m|pwy3lRM& zcvei|2zW`)Z7b@SS+59R(^o7!Yaw`N*5YI59XD7&T?{LiK-m>18rheA z^UHkcq#%SZ9gtZVz2r-mI~E>#9-2&zj?L)hy`tmo9Zz5vvRlu#47mZJ}}E;QMd2Y;L7 z8zz)hBM=L2@9n{92%=qaxco~8gNqm)h@w_k0jwU`?hED6((2ts_V?Ku2Z)13E{J;G z1TzrwUEVsNf{0X_?_y`bu^|UL8ivaRgpk>>sj95?f)aTU;Sv-b5Qwxt@qGxi#4$!= zEU%$ecQ3t3D%<3@gRIHTbwEWUPJRZ8JdJaedxVqtbV9g)A&kJKq5 zJ|@rImjV){%mw7Dxf6#tLa=zkQ3ilAnaBZ-KgQOBfD{E4mwks|=?q9ZxdSn5F z*)HsJ6VEjvLi0*9?K0V2dH}k4Ir*w^@ck9n21e>CVK6TIq$}~9Mxpg_aeo6^?mp&J zw@kuGI-i>43*YuNfK1R dM79)S$+kdxVX$b5bRM)tZ@J1eruVMG5D=3S>B7 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%0DB&QucUkv0qIh1y8Di^Bp^Fo? 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%0D16LkRJ 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 z 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

5UJzbx4qBDQtpXOB%oGH~OO(aoLhFGbm5Gz;$ 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%0D6r3aun2QlgRRp7;O5tIAA<9`an$BfyYSxA3Vsl? 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%0D-5~9YcY2YbK4V#+2g2mgJexU2E5`1uboa(h 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%0Dn(+ohbv`IrLgQ1)p3U5@FksLfzIec9Tx0+ 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%0A)qbiu;$arrLmeX<$&VA{Nr* zs2WZ2B$2fAdjC$1tYB{7yhe?;_zXlSk_a79^sJz%4B&+=%3tL)0-H?-xj}!T1klpp3KOKLEoV{-!ke=pX}|@Vby9x8`A1LcXkD z7siM2;i53muv|_CzE2zYkN-W2W8w8IbQyDmK0)UJtbWSc9(pBB3%zO|_?*8kSjVv@$ zCh&ABL)Q4`th8R}&!fOza{T)3stlxEg0*nO%N_dQB*xzGg|<_#J$2c;x*rBC#`XnaIY_BaUh8x@2N`~a2;2waFTyi! z%B&R~gy&#M2L$y>u8UY*#?3;v=HyIp)<(mzP%A93g+NXJ5ge$rq5|xYRUwdK@_HIG z@-U{t@oC7F;#9;9r7Ww0ke^SHTY0011r2qG((c~@6_)SdH^h^`vU$3b&gN2u9O`XZ z%%vDon8*`HODo_EV4#7YPOFOKbj5`@^tE~k!se?9#`f{F*UF#`%Rz~R_u!uoFAiO` zDJcVNSJe|P&03fkBF2+=7u@~mv?9vC>fLSnhe#%i@*E9#v8ivcQ=?51K&NI5y?17K zOG7{|gR!E_oF%7>++k6*P>RP!sYyLJg3OK`>q&WdLTcI}(c zGS-5_D_A{#%6$v}iuk6%;{nkHuVbaCDl_Xb^cn4jB<4l=&kYW=joPC-!KOxYR`JZ{ zC0l#Qp8n0tp>hGB~|sJitCkwKx zDe-e#G{|)A2^!rkM*qlg!N$*oB7;4ProraW(Nh-4o&IL$7X- zi35KQ_^&}|3h$~H%z_KCr|RDbUS35#=)GYs?!Dl^1_xf{`_HI1aSdh9j9fkc&~nT# zY}n9j6iwbR8Nf(QcFEK-P_u%w4s@CwY(m`jtjMDM#i4?vyPn?K*@AsyFBqheUBIaK z(Q6KG)KzQim$u=wXgT*#mx_6Si|(Ide$%wDD=EMncw~OTbhJbkmeKyV8rmF82C|xB z$qowxzcfKAj-nGDYo^0sI4_n@b8THQ1HzjKzhXhx!Rt46FOC&s#c~E1Ro)`?gUQTr za%9U`R+Oa9V`rlrXjwIkJ0mI$2bR&i=wf)}oykmNZbmz}$^Fna0%fN2GY~fDflkE< zGxc>XCjGl^aOD62a8%cWA=c z=^sJ$%=pJxaxvrI%8~>B7~Ffx5SM6=@q;h#ig&xaZmZ_EGcUu0<`FrwGbG-}rb! zi-CFktel%S5|d@dvGaYSNmGf$p;n_<&J=p+9|mvd61bl|LhC-9cGXpcB)dp2Sv9Mn zOB4G)Wa^ys=nN!BOFN2`g;)i+H*+kx-kD;~R!dU#F1jOh94$>*+5XC^$D3k0&m*qu z%wIgD2=*Wh1b>UqFX;%eEKPijs7BVj|Kx|B#mEP$y(bSjDD(w77wu=5z*PIj{}CEe zmfGCel7cYO8_be=MJ{YA>CoZBt*gwbR&-hg-l-YAef5luK}MZtjFFG=3ooA>**Xc2 zRJ-Sjk6r|dFDoUW^gvz{aSkF1&3r4L11~mAa274&3JJ)F=~0fbdLFovm9I^I0VGpy z!7G+?7=@7y~^d%@|xTKaP=`Xd+DPF46l(;Q94Ycz$(I z22NQ($Em7{YbXVo3#|MrSF&nvNC`7R(01lJThF?)G&8Cl?6|Q30!pEgxyrIxtZtfW z&82(`;7{zm;7pJdF+hfA_x84rW&Gd4>p?D1?-Mkm zZHctLm$Ihxhz2JrC|@Ddc?{}w4Q9>cIqfX$8Xz~+94JgPW!XjdE> zv~S-~h~gJPUkieA|C*k$UG)G(MW=i38S7I>L?b#Zi#TRzAc~e%6Y9GnDtaJX#xZ)* zstP)p$WN6^;}%{8>AHF=;1dX~1A4axMmPNi_Yod#SA!1}a=j@y%G8y?=l(A94^*%F z=p8}g7`ADUNzV4I86yZFl^sH#9;gr4*WoIpSqCNh;e+>eu)(2-Jd%Q`bu688{Ph zf~o*>IHKh!viB$#1&qL4yWvCk9a+IS4euk(M~APzeD6ZuNgEcZXXzRqD9VzuX)*CXPv+qtnebof zde@=$H^#S3HrlWQ8oB>;J?U9;I!CqGsKM};b{$*a0ree*%qcqK^z>K(ysBs}{2a3b zan&}c7wqr3rokcCa5IL`Ie%f&|CJ2DffA#}4bT7n_mFM!9yO>;Yw}+R zS)7i6y#g+~p}hKyS5DM@N_gS_Ft6q9XT%Sm`GTmr1{Z7eRwx8dL!O62eFL9hOwp!U zMnpoN#iH$AfPeCGc=SgTDwYV**LeWVcwUqDAqfBpB?|hyxf~Kem<>wYcQYEcg0z8} zpHjzc-1obc)zSiamDDJnBCL`&Q6KQp6Cc3r-*(Kfxq)h`Ds@&^37w`QV^Z(Bd_Z)zUDEU!<&9nFaTxdGPdJ_ zf@n7}TM2macn^Rvv@OW=BO++-;z5Ox7?=eES*U-@;Vp|xcBc2sBwEBy#_g1@hYdyy zmcmq0WIGd6=_rO2N_?rK&47g=2g0GpIM-8vl*N28j4{7b5F`m)w$?%f{U*CKpT_#O z6VI@}V*Uq;o%iA>4rXi4%rdD$=5(+88xliUQ&9~k%)|iLAuSC;S|#nLfuD9Ot49Qg zoFK=}#C~B`XQYyr&H5pG$bTp~m`zT!BQq4EX4h0<;`nKZ(KesWo^|C-^M@iG+d3hi zNpHAu3;0|BNfo(ygx#zRFB+22Bc2?AXc*_DtdEj6k_eg9Fu+izzlr)MhRq1}Y|8cg z6dn3!z?~Z>Hzrt;G-b5XAH~TLr@SBsVh>K*q#!qSxP#jYg1}3*swn;2ZrRQHFRE#D zQWT?kD8n&&ndMD#3b*6IcZ~zu(9Zq(r?!lYXJD;_6DSK}0*M0O7r~q9J$p1w-eC^^&@^s+WKiRhN{JFW4k~Eq(wqx+gin|uEsr2&4 zK78*OeuHkpG;HlBc8c(k{^(3X#GGaGdbur*=7pD$RX){{WRJjFM@|+RJ;@TGrP29h zQ_3LzFK)~>DV5ZzZ%yCPW&bX7N)IHx2J5$R_=Pf>1~g#` zkP1RTm>t1ol-?Jv?jt9@0RNQ}fk)HXU{#%mo%8>b`phEr1V@KJ@lW8+VLni)$mqVn zr4HdJdM|3iQ~L4%WU9QFc8@8^Wj$l^Y3gAJI1V7GA~)p825Mke5}9~1-Ri%jk{173 z0!xv>O%E^DqZVufs81o&*0JiDEhFdmwe_O61|X@FV;H!1^CT6|lz+f})t`o;Bg4j6 z3jiF9qhOhNrdE$Zb(nSFkQfFGiRU0WVxF`Q8~(XhP*MyS&r){5XnR+2Z9X8y;IZBo zhOD--IP3*k^I)KgJnF7AFpn?FHw#zGH$vdXVP5KE?4y`|7Up-s<#E0E~$z?;7jBblCGYYCJM39hH9agaP4$ztn^r6~uQDQ6H6A+pM z4$AFzPGMCU`GHx~$>JvvK3z$I(G?hTYD(i?ev&a+X!G%(C^!R_M5ZEMhmplH zh$}!Y(VZ195p%4gxM;LAV+NGs6?aVhJX{!bv3FY97X8J3csLnP9*nf2oWLff?6%QQS zgz-Dtm2a$6=tv_evG?Z}7%pt)xP5Q>%i!tZ@-VBzfGmt2Sw&u@U@`(kkb2tcFUfj5 zAZO{od*q_F`robtTsaQa?M>v1QHPqkd$?h5Go%L{H1L0t&`SDW7qzwqWSNL}sg};h zZ)7z|Y8nP9h|e|D$C&r;xM6dX0ORpfUp-=brkfBMH^1Y7Q9%-byG}eheJyzMX-rW< zF8U7{&4U1ch{)#w8KOiFcQ3)H@gd*(Qk_ytd4keoM z3u0^aXhJcMe~0YT3*|t zE>X8bkwx?F4yu2JKP!#S)EQr>P+sV%Sh1Q2HZQJ7U?Vt-$AM=+yHE)mb56$E^}~{* z9i1w#a|TkFQ72JuW~>~;lYl^CG&k)$6aP70<2=5CCb$WS>>~G6_%T2n&=DP4*ig@+ zSW38&T<%v=4D3kCGx86RTR?Mj(inf6u+;AmvO-9rqX&;ZM1lYFF<|br5|YJto&AY8 ze1m9*(A4a5&cSPM}NJogs3 zFbRh*_SOJ2lh(obFYs*w0upyN*-zP9EnH&`vyQ08O%GTK57fOMircT#6&f_^0@Rj9 z-2RH>mhR>X86gyhRW2_AH8PPI&_1kGs>?LYOv2i8Mmr0b5_mxs6F?3Vf}2?Nv>}2V zk)8=NG&%>1n#yTuzobf}K+Zy+GyvBj3PpsplpqHp-~FTtIEIdtcjNM>{nsTkj!S{c z9x5hTCBx*!NH&Xv0Zdz{ZK1CHgn&8NYCNiaONzyyWEwNlR8hP)@=hF4iW&t#}%~EGqb0UFBIbdJ% z$?>v`^L!M2;YV3F*ojx+Ob;h1;IfAWLXE0dEf;25?zp)?Bj#!*XD^DA(SCZLtEp@V zQ&+(Y>{mojP}9PHr0l}G0XZRGn(Bl2c-88?sEFC5*BE~Vr!Dd$ z7^8wh{pSGrhh_o*wfwys09I3rRM=$xfc#%9iV_oUUU=FiKYR1dAOHA=+#-yvH?VE3 zj84AZvZMv}_}gCE!>hn_akq#H?4Ijg(WH;e&mik%6*O`qjl;+RkXZrc7m+rZhmbZQ z&;^jca*(F6_=Xyc%I~J5mf&NHvV*D9&Qk}}bCsc>EW_Ts7#iGz%rDS!JW}_bx(^2a zBd`dqg?k6z)Rz8i(jSu7O1GVE;+o|I%|-(}0Rb)Co+rCaU>CC0Efzex(vqz0yEzZe zFeieY96Xn{+rqLUc#L(~CfEbyX*ZMj|KvSdRY0IXT`NRmfXIJ9#x&cq0-bMo*hVnB z4vm;^l4j~x!BM}8w*GDy^+vNx^DB8Vx1dPrL_$+2rhMh{lkuc&~;yWL@+2Ir2HVS{) zuu?f>d_u|<`kHtQ48{Z5gdT7h??Y_vM}_im-D5O|b|?~Ci@LyE=TzY{*>UyY3rdzM zP4*oK678}L1kAk(v=LYsAe4tW8dNp2R@WSTRAuqZbVGu(f-WAEG?ZF@6m2*cWHGn_ zIFdrnAvfXygc)1l?5KJl`3$H*uy}T{h42I%g$ko{8ps%*!9)a{h#tr`{~~wWHl?n3 zrYioJoCb0{!Xc1%_aFYXtOec~_7Nr~(*`*p`YRPsTCy?#pA#smz z2tpJ(lj?rV`_5r*#y-SQuqZ1ICZS~swNL`TDM*^p8mAh--~x|!ES$FmsaMj+#&=b5bd06guBYLrpm zqR&R&y!pf@*kjC3;qDr$`Y5(nmjiM{7eDNzsnVl{M`11EV>6+_M~H+61v~{uX4it1 z-sz0kp@L2YDhEoFc7UG8kY&UM-(qwo8AmgMNFuUkshm3+YtQ=Au)Bl`8cZ%09mcnn$Q`JAs_xmk7wbNW?0GI>Jm}L^LJ5`_ zPK>~{3Yj0Y52(-4HAYhuSO~5Rz3?>o8ypy8I!VwDntA15813n8$U^E%tr6*Sd#QXU zO)Vb+WU89~VVRb2l`Ao(nsb8@IqSs7As-uz;a?;E;Z~ReoQ^<<^IsNJK3Kv`IxUb* zkKwG>n*j|nlBt>_pCWxAyHIX=fI>86t{xIot{S{4!uLj3ozdk z6)3*KL_Jy;Q4xHgF-9!GbNV1%LV*?7E71iKMe2d=hXq181&|!mETSTuuST4prm7#n z7QqH;a|qlxWlDI*M3+gTn?uF zFCnCJjB%*ND#oYdIgQ%_J1}_WB~vmPdX(g|l!|O5ddM-(K^l!|1L6yx%15KkhU&jX ztc2DS{o?g-)5&me*mfy_s=YgQ#cuL(d$~z^q&0Gs*H>dGX3E7T?nNn3g#C78Qo=BK zZUryoWQ~EMMfb1KF@%m+Mg%Tm*{Y<93d=4`XTW@onhLi(Zh`eY3j6#l+-{?WMQGk0uFm8NHVguHP)9Y+D!;-5yt3me z&#IPNCqGKDv02hkYeu4aQs?O81pB0X8{eV=4s%6XBGcg;dV>xKf%iPf2>(t#CZ*$8 zkwC!`s_+uI3C3?GCa_@D!n_FhZsd*+qw?Tm9txWXBVkz8GjMrgb*_}ZSOmm=zCh4x`#h(>|1BW?*(m0&1fPUl(XD{#aKPo($JJ}d_VV~f!tBLe^i zU6cfR&$X~yzIfk^6&;|N52eS6x~lvn0a*}qA7ONAjJl-&S%ZO zsca1@#}D@gP8alY>Gq<7WN2y~oakk0wHt{3t4Dm0ve6TscFrqjI9>=qt%j5_kNO`J z$STXfT_C&r5=s9k`qH3_5D#E`KRG72ISf1?{?CQ&G49Hhitm;zKB~wVolQKUp)1NP z7cqJxEk~2u|AXczk2$B`1|X@G5|2A6zO|k32~XAP6IW(h?ZWhF|ouj{jS!-VA^~mC31L`Hh7Gu1&G@eC*ugN>9pIIE9 z)RIzfiG!^g{nnc2X(3 z-LrfN!%JlN)-$}}>CDj-xRHDc36y`>P<0S2{yam|Y^O)^HLQOuLM~$b+ahG&J)(N> z0+UMNyOyo1_f*5*-?tZ*`-Zpl+%_ulg%QiOF&3#MwM+p^St2X-98HuJoiDe-$_4~< z4P_=1X+-41j0%)S*he_FU_*sBL>p31n{JRC+!S5q^+i`Wz3v*r|FN`Y9OI5%Cx-rq zp+cE)QK73~N~&C(U$PQvvZw|8{779M9M0FkL+GqK2G+>CDH;Tr1z0c0-UBca4XOHQ zjdErH_6GJ9p@UYq=E35ok|A8kuzM}wOsSqN^sr!RPST+pbrmtDn#{q9q&LVy2Kp&F5qJER}KQ(0hLV| z-^^T~xE>Nt@Y8N%h6b&&6U*((d05Q%JYnYOgjEo0SFC1MW6)6ov4k;W-8&!JLr#0| zc4-6iy5oO6e&p+e>?4H@yLT*~|L#wEq^IaVa`VSm8fL>CpSk|6OcC6IWPR*4opt!; zHo@e*l*>k)-6E_(xQo)5xx(Kg%Bc=DTDlXyYb_}Z`AVihV?h(#=zbVr9%MF1>=Be z9fy+^m)Aue7LYlEmMfM&dW}qA35#tIfN-AEGlRxa84LZT7!FjvgRC5nQXr1{7gWkfz(05sIGSn%J5(&;TZ`hYVQJN%zW{l_n+uri_$^ET zSFo!^5OdKZkHCnx92=iZ_U4HHwCae+4j`~o(Tr3DvJcxrfe?d`t;=;t5aES3v_$P+ zT#Dpcn`Pe1!~n_i-cST*4QQAEPbHiR`qZX|mU?Nw$-vJCrtepCU6Ye-5VI?7^P9LO zSqvuv;g(Sx(-bxVpG_)7dwjARenp^{+lto?x9ck~xl-bD$%~{6oG=I(-qLJg^r@gW zO*QFL8|$0xwg!yiYC^C?x<~F6lH-YTgex_#96@5!ebZJ-^uxej&ohD}A(YRaaN8uBxuiIrrqzlg6HPSPak0H%Y~u(xffEKBlk8ou?R8w%nYDLq?7oO{l9zH@v?Or(g35fL&4ASf$o)B!6{OP$?d6ZBF#6nI)kTt$B( zU%^4L>RLb2b7+1hhl`Gt3n#kcsul#MHdA*OuP%gGZ=xq_>0=`v_=w5x6k#XdD}Eci zo@6IsBN@U>qf)eo5n_PIII#ues@ZoL<+2=$b2R}O0*8oC`9~QgE(f65MZ%S+iy@5^ zcpex9+us7poDpL8gk|-`hv%K@A>q#EQ*0>Oe3(r)-&9twk3{Tl@r_0jxzbEJB0e`V zcy-kh5b;%$`#J`u?>-0hJke114bPmjj_ukW3|Wn{-_TifRI6iJkZ1Omqp44xvusk2 zW}-sk?9bj}sp;GYZ@l0nw!>a$JQbO?UujCpR@AMDq2!5b*3gwtAtI@bRhIO+Kq@5B zx-(Ykjf;k!*GuPKEq=kxxzYZiWHu<9>|^!B(gwI7?TgP{cATAyB{dS3?wpVL+823Zpy2 z!_Qp*qB20N@W|2nh+Sso z(F7b^0-|9cih(_PJ}kg_WU=z9B`~xJQ?B%Of*&87YjoZ~5n~ctLM0d}T44`}dtm*9 z=GA%Z$(V4pCZ|T?GCDs*3l;Rv$bJE}vcv99dn9}!TH8St3=>jD0&C=>xagDZ=d5g=ygARG3R~|CI)cVwl~tM>ZIrg zhaC*#H2xMG$RuBx`iNLwOGr~B3~YboG{;4o_SNTHGY$G=a%DE(ab6MW&J*6b<(p8P zV6{SSSQNq$>V##EUpR^4>q`HXKR-Q!!PTOcyY%9TKO`e+NR5RvD1LK89QAF2Py{sy zg$%nx^{}uKX;*JFUn8+|g5Zc|CkWA@y%pK^tnA3fzIP=-z=!ZTTDl*=)FiKcDH-D8bd2Z3i&0&9-EU%Ui93{^1`tH_PuKm2guDXbiBJC7l< z^tV+Q#|5k67;rr8nC9mqR7R8ImT|>2;eT3>zNVq~osnU3P{J zr!a(MwWB|BPAsY*I^`m#@fbQ#%kd<(Pf0IiMI6T(8Ly-8FgI9i{^>FH82b$W`3y7B zGY?#n_JA8o%b|23=e->`kWjeO6@zFnrn?=v=EdMZP7`^w5OwABKqU0boMDCZf+bA!r{W2 z6xMXewc?8hZbURiQ&;cr4J%;?bK}$j=Tq>|i#sz^=@ZJXoFa!e6xfft_cx1LU=^6z zb_ebN`u0xZ5TPk0#YeLNRmmwQ$eH2=j(7vpKTi{e87FUxB8&hK!hOQ$Mu3q<1Z#n2 zrpO{P-`L#L+DWtacD=oZTRoy7O}reByG97Zqr5<_K4B(8hF*k9L;e^I_<*oND)@NR zRe-ibsg>AHxHRbKE2Qtj+6L>G7VMeGQB&Zo_ul3w^$SrV3~Z}z4c!sCO?-8>`F{Ak zfVAO6aM_J9wnqhDPmlpr*xBEc(IiXDPo%`=hZx(@e1NfC?1{T+-JYX zivyG`pvgR&v{M?uMUe=C30kmu3sM8oSR_{fU$`il;K;qynRZ6(wLVY5si^Zf2l?s- z_^^*hEhNP^{k(|p{nC!Go$;WRwGilxKtbDdQWP1Rb*SrBB=aaOO6X8f)h>iqylATC zsb09@g%zFKxgJ$^-=;=r#X4mQYCeQ^LXj9che!^Rz=(8-sP>E#YzRvO-bvg6N+`!* z**Nbz0uz#9qqBkvh9yK*snGaO@^3#e`vgk3>(~<_7--o2!Zt0Aa!er;4%P=36#?VQ zFHJWuWfltbuwBGnNXaFigPUx_Ham!2HJ%B5PB@Px4XjdodBTGhQP2YLc;43$*=6i5 zn}1Xw95T?SA+*sIvh^~+>wreODNt`o+fhM?Qbc>b@y=xPfm9MUmxbQ)HV5EL5T~Tm z5;ASgFCl*9WWvdG3KimDS6yO)!o~@M(*MLPkepPE#**;IrOo_o*XSE%*w{tD&b(Jl z0CRm0Pzhg#H`xX+(k8v-%Me%^3v^Dw@&<`yWbulR3TP?!!5BeKap)X^604O*+eA!k zO9Xrs&+`O+$pZOESkts{v7p=ZwfGAs!g?ofN}h`y%r#?-`@1A;UvM1B1|27!(S6$f zWq_=&enl@x;t?~L{c5Z$(|^i@nXzR=osht+m18*&$AU5{zJBL%OD!W}AkmNbPFPp+ zTCO*CD?pbZbVkh6LVRrl%4LzAu=~FafFAa_(Nyzi?89hz0>8(~C4R*o45JEGz&0iE zJesQ6Xs4jW+!Wh8aOU1AbkFVCwXO&ImUKMVbN)rAOrtr(3D=(50i3X$P6*$a*u5kN zsPWLbi|u$AWr9*OISJO*sAZQytRN>8rP9+?3IV@wz`63Gzz%|G3;{ZLSOW*fRComt z2D<(kD@AptGO`-&GBTxVmM`ZX`DH~mel51!(XD{jVln4IsHNvV)#pX1U+ybeOa8xHuUfln{|g~9ol zsSF1J5;N~&Q!>6SW%H-vX5^?-b4`K+d`Tp^p>Rn{)O>K`+)2eDP?UtzfDsB+-56fJ zxS^s^M{R${?35&UU|QZfZ9 z542WwjoYXg4~)g2T2;#zcTV+Rp6n;65h|cm`m+@vJe1bgooN zVy6sun$Bq8j7a~AAfvkxe6|wOw$34zvf!Jw?%)h#f2ci6($VlZP6Tthni?F^6_*xANw4!a$`s0Rk3U=Vw92L2KRnuRU6d^86S z;(z!U;RqsgOxc34H48fu2*i&kq$@*o43rLNLzzYXGZ;J$QV8f|P~0q{(JQ1ih$BUa zAUc6;M(E3Qp|n_NJ5>c?wxDjC)7ebD9Ihd7=0MwHverG+>;3Hw9+oVU4u#ztCWNx z-A2*}GZmoYu_r)^1s~?#3#IEu{a12g98$!$%T)5u4`rQZtns>^c4Qs&iL`o$&-~$` zr5SVuM{JeQ}5O-tJWAgjd^ zE7cxL(ck3rTjaPgqYb1S(Oi%q{kcaW?x8*xZ{9 z_S5E%WfokLwM~RqtPViwEbQqrL#&&-B(p!W9l#~RN%WVG&pw9czIF&;r(YM$#G+1F zOJ$mm_4iLBq>U)2B<~wE)mU*OYV8%*3mWpLA}EzFrLMd+R49t*|DBc+&AUtC%wAVY zTkNc4^Qu(P5MB!}1JFgB1l_uUTZdngPHlmr2`aXmLb&gCp#)EeK)7`#dFWih=d6C@ zL11Vmd3Df*=#YMS;G?usbA(x(EBispzz=Nmq93^M{z2bgB@=@Mq&elQ=&iVtxyc(?TblV78FGKEL%K!Gbj}8Wve^3 zRty_$gjLJ566I)hcVA&yp|d70&mXvUC-{XkE6?Aa+QcFvLM2EHV{2&VGn@2s8U3u) zU^)dVHMYk01kef_$kWfu_kZDu+0V<5{pGPwyzq$f;1$4Nsi~xctwcN$EqVjX;p~Ny zRw5M-V?U?@#LCbVqUC4K!x9DEfg}nw5fDd8bu>diDex)+hB{lP+iKnRsCx_1vK^yv3fO_H6R!j_U za{rHNj2G4#2U@9-mM;pe&OJjC7IGKcW;^5u1xSvdt0}BD7ZQLJa8oa`MUH7EVd4-# z?ZVbi1#iKs^S5-t)Op};Mc)6Ls+Y4unTgDUx>b=_Asw-r*JPIzB44Xb#;*IC@Q+6V zPsk6?J}o@*!pD>c*v}C0g6mj5uBIpK?y=$7%_bVgq3;YZ5t9VQMq)@{-vSw*s^?Y} zsWyj70Dh4Jqk3dfLIYc=lBoIW91)IOg8-!N*W_CC(8-6uEa# zNtbo6JTi7lX8B~*#wJ`X?C4OCdPe~1#-M9il?@r)M(?FX)sUKW3#%Yj;K0lF^bmoC z9M}jxuglQG!;ku0i|X|E1s;7-)Q3BJ3o82vJOrPP7Vm=j_VM+8b;6tnMt_od&bNv( zNp?_=%RNNS1weT62(K!cmxF~9j1ME^BY}?H7YjNW>5tK4qz4BHe}?Z~(`SES0mOpX z9oYM@&o5;+Wipx?%B2`c@_!iuZ6rfNFVZ~7*xQX5oIC}s2lOUB(FledP=A7WH)iPr zwi`3Ly7RgkUb6fE3L?%50dKzl1kcgsNJlgjwh(mjcpIt&2R`#da%$ax~hq;qby!` z%mhKv--F5-GzXf5Tm#nZASJ@$6G`R(`)EigCF?7hXfmEgk(niYI5rXKjVui}55dhW z3?me*Ii=o?PBre9N5Gzi4oC5^oH-V9IwRlbn1H^6(&Hj@i6n+R;xEk$22N_LM z>b-?b$MI|XgQ_=nps%{Q#{%n_5~^;P3Igm$d}rBMZnQSGET@1>U8}A6659WT^m;c4 z5mmX`nQxveD_y<7{;8pQPa*ch6yAu`<0>AMYDaD;CdOrGmylR2vY!Cfs(IG^=w3q< z*N(9YmAygav5COI+W?0QwSlvLfEq#DC}DlB88F#xrl-fM2pED^I$16}+_St3ebmAo z#}5@20}qViSt~v^03M4$0%^lcpo_084sY%5AJ1Wfl+^00(xPpd{Wan}H!=;dF@rfw zVNhQcSgXvPYQji$c`zuNo@rFb&#N5yr1%lwr~LTwg@_&h6fvgu%wk)Q=p3INxB*M& z*i|sAob_N%)ypUrqr@jaloU)B;Hvq*crI+-WsvKzDExr>}w^|qPUy;Angc6c}W)4jbT8dd1sR7DH zEFyxPPyP(eJJ8)xE~LdLB3YY4i$Le-v+oz!b->TU%;z$%0W$!+gj`a5qvpBTc6sSU z;}+@FAH9)X|Ct-G)xYCUrCMfF3H|9c6F^zRtW`?ui+0xN>=iI;8kp*Xi_L~>aBSnP`wb*{q33W_vgmYIMCw?qWn%lXm~t~`v!2a2oI0lJk=`rr zSRGxlekzjIeSri3e9|_$553C5$f581&9Rw{O1wxQD z9YNmW)9_s`54?nz#B#XQCkwLWhMi+TGGL%h@x?7$+=m7sHZ@S=`2~@s7&A|e0 zy!E_QAwAV|=9vdd>_6aHnCPQH`70MazMca+I4ACpy@hJ9+%XrHbknw{IMfm25m%{#PxYb)}UXhZm% z$);Q{sBpXVaMf;1TC5nGmkL2qyU2;-d zYBb$3QZN-=&aUe<5k&*LeK_h#@sS+lt#agk@e8OgScyC);upwbQ1BDUE(#*FA_^jv zo3z`|ECop)q7f8X;0IT}yJgv-p=?&Za8PW2wzR!dwRCO&>N=ewK?4cuA|?-F&B)vbFV- zBQe(~_E_up*Zb}Kc@JWHlRyMk+TH;Ycv)1kXN2VKJyCSzur1?aY^Uw0Y>4vJw=P$c z+FdugBb84dq7zN8T$YL%#s2U`2M4gLwqN^+AR3{WU$=z%hd+D!nM^(i$i{ zPK(=!9_yEc1x+DBJ~hyRdflhQevYntf8YawkMNF~UkUs>>6Vt|ISOqD7X|ok-^%#v z0Tu(LeSEv(JIRFbCVY5HeW2aY(FA?*QHpXQ)^1KCfCf4zQ~A}C!A~}SLi3D&HcIc2&M2lIZa(CKM=Kac zun_APn^9BvVIfMC2Pqq__kn+gQPB$uOC)Wx^f-IuKR+&gSdkm3iF=Yx7t!EJzR zY8V_1K?n7>#9>sU=w>tyCB>q{I*SmH@Uti!71-~!73!$EQmZLomr^%`z7Mb6?Tdkp z*h=K_Tqx8O#AoxJv5=)FyLvSz7>iI$K6c~{;=ce3bTcYSe-QX1Bo0wXzSn_p@ETA2 zLD9(ei@jpKZL_FLp`RkI~>!(P%MB@OkfudP>_$HAj7x7mO(C}^XO2SyI$O4 zWqF&jV?=1)mU7e!FF_@P$~T9EQLE#xIiDP)P<20X4V4EAi;xMwO7 zMx&0Pu;GG3OR}mqeZ!k}g>_(=#`aur`NWpDU4LBAF1-GsePN{k1aZsz&cckMpKEgW zU8{Zx!-nPybB2wT*efpuFr%D^U-_$39TS5dwcRBbjWcl$nsFHc0Hl1Tk*c@~42g`S zj}(meXUEPTiO43BFTpi;tQ5e?kF|`7v&49IwWh0G1L&s*lKtRdxBj~rkgd~eru$Wp zFe#<6rHi{{O%c|oJF8_7(~BN3HL2lc9j9ior&l`1yQ2Ug#G_WOXR5JyQ@5*V);OAq zY8GhFMKkU>tM^sm97&qdbLxbJ&0)*bxvM}fp-F}q=}!es6+Et*sC&u=N)7?+p$W}* zB6TepO4zu`^{TGo%3)-+af6Y1EH$1X)IlUDbZsh~sRLk@PSwh=_eMnikXz59EmSxx z2q|>^ls{?0!CE&f~Y_(SKfTvUgOOCupd`uL;&t3~G0Ym#i&KG!9;3tR^v>g}j zM8O2+#F6$(JXnJ{4`N?-OhgAxc9DB!{*H|zd939D;ZTb2M?GN9c$A4tXv=FNwZ=(= zxLDyu2pIyoerYw|j&f5vAAlsai+q&7uY~^e0SGNU&Q}wWj&RT<#UX`-fEJm3mq&AY zp{FtiI`@=|v7e?9-$}1Xqb3@Y-rG0(McWN)g{AF?kf7Fk#4eD+s`|RE6ge6lF3tWV ztipw=N92@ItfiToU(%zfW)DUNG(CyP^Urusu;h1zf0+F|3Z1!orp3Uwx0t=lG0`ax z{&Om#e)tjQ2nk{|i9&aShQE{oX%0##VvYo!Jj-YvVIR_=qoKKN{RPJQ!8ij2i`YiA zK#`x7D1E_}71-jg?y_iSt@6e+B14+hv*_$FIeUfU+JPgGuVxe(*2wUAELYeTcq^D6(h-%UqaDRq~(it2a`++Yb)EV)3!z=@zGTGZ@jVWJvhF)Pa%&J=6O= z<`gRiJB+=UlSpjX9CN}KqIEzduFXDIod%hPb|@ZC+@G?MzzabzN*X{4c7-X&18ZbA z!hiJYXc!**e60IoxW=a&q9QfFk@ALzw1~Tp#JMhZt?$>YfkGH93*$!5A|r2F`3(ZA zC7WLg7QCcaDCA8rv`(7Mj{ry$4OLQqF`5r267gK&H`$6EGtNyEkV-JD>n%iHU$?Fs zI3e@Us8%qX>juUYE2bP@aMEj;RX`7pQ26bom?_H9C#8=`$s{}qEiU|{i2%Bn69yAb z$7+Oaj~_H|v~y!!iW^(=qy=#ZNh{W-b!TKn*10_?Mm8=E$3YBnI)E*5;L;GnjSYqn)Uxjgnz^nmwSj|+!ZF8I<&@sp zH+tIex?Q84??bz{aDfp}1KW2UESrzsf>K|?5d!3g)}14WY$DBCkcC(ndE77l3-Sq( zxC;CxP}+avmI+ zkc{4{lSAFT>vq;rP8SOHcCADGiVEt3AgWMJ`OkI|C?w)4QMbyr2%2(qodfhplTJ;? z5PWdT;LJdQ%gTVl-;zkk3>Xjn|3y;><%e!S5H>A;wP`3E$>rLLDgLd zd-rz$m!t&BbBt0S$66ZlLPZ_z|W;U~|7>04-klJA34>e&AjU!(r4 zP`cMYDd$f0T_czcqzYR<15gib2c5QHGT8=#Ft$!Wp+s_PknliyixQ0x6u!mdT7q4y zQU`d{SAkKnA3Tic_{5Busy08@{PMtBt7@W*1o<{E7JimphME8^WP?Ey82KLUF3knc zqGOfePzqbX+!=f(kQsu4+3<&`E^E(t6sv+d^E+G%SAP=AqEtgj^i zZ^8&aUWlL&4BNM7(0DlH2Ady@CTuy_J$g_&ct(LF8H2@lt!O@_LE){x!f6eLpMktyB*2OZydiPVxL$)HjYFuzS-rnutVp3ZdzkkLoo^189nO{q`_{f~AHm(~uWp=k)f ztQGl=1H;X`?GHGf+Y1-B<0=)pS0X{@nXsjUgaP^3V{EtD` z3?oDVM87I;G}C}uV?iT~s!1E52eBu-LffLRHqg8k95uPgl;cwUdZeQq4JmO{QpwD< zh04GpfX~G1s7*fySldSf-w!;G0Z6h=tedvrwLtE^ z2tjzU#cIbuA!Zzo*A6{W*K>$|OWb@xZ#BN=3l5UcV;IL;H6G6=BPq;(6f22#sl2ZV z0DsXcMeDhMV?E9_sUzv8Wd_k68C@9A7a{*-l<|IRt>b;#3h=+ks=`(B=D1Yw0tS$s|lxq8G1)mCOLP48K`b_5K8F?i3_aP#-rW*TB47n&Zmyx!I8 zCN#O?3aVGDv%hFSDUTzfNu>be!{O$?p{?sg$_j%OJv9|KY$$svHJLTTTBy)bgNz!@ zA4pPcaoU2~?i#MU>}_a8gxxPF$B^@ReGDOUIb@1%DhHYsbR?3KRci?q&Z8pO6!e|m z31BZ0yck7Gf-*XoCX*EF0DQ#nBL8n@Bm~-|=D+Pa(4kt5YW2eFmm*1qDsVV==GZ0Y zYzN<4$L4CO_$g#BL8LB9*p@ti$|-mcU#;ypJSG1!cd}1YV+Y_Mn>b-?ncVa z!5QE~lL)HM6s1T9dKDlf zp2^0tz%z6qfB6`40%L*2fn|u)u5GbZUV&I1YGt{i<^}-{U>kVa3X{yO9=MFO7lgdg zc0eXyj#%Q_k3P5veq&qR`yr17t|8xCQkSZ>m(ltN?MzxrwT+@BU<}+Ae|3^gAg`m1 zijjr42rf0V6ocOhkFA{Q67BrtO!yn9$Fk95fp&+eUl5v9ARwupg=kvW=Yvl$^=n^j z9{K0s7nt|`e{BviTce1aK>+{o0Q&<(Dx3XA#(81ma-45T(Jf`kspdg#75RqTRlQ|f3SP2m7pjo!0hAd`bnQfVsK zj%D;Oei!=3kAhG0BQa@FQ;2T#y z2I8xmf(&s7eOk3m8*|vXpf5G zAV5*VtR|pqjhTzFKcn>!x*nvxBgl=TbBo>xl^Z1~h??6_rP6u&3T)5}b=j$%D7zHw zT-bEeo!u21%$}eHw)e;iHM>9%7T5|DQyRrg$W3}etS>7Kqj9eWDq<+5P&yy`kR+hU zdX+ad)g#Kzz*rc_TFr*$1H&8eQ9AVF(@?t>Dv*{=W1zkeXf%8h+#D;`Aw+D{`GY{~tw^M^3xwOr--WB-_INftYzY*(MDYm}wJy95 z9iPcLas?}z4x30%A*c{uva5(Oh&Z8+QRU>Qxa_79utcHeZ;ioKogloj25&i2cF2!KZw26$B>U-3(lmQ!GiY9!0#C{99;HEbp7nM&y@k`@Rbiw z82JLnSHrvlJUTK4`nmb!W=n5TTpCu{cVgR50T=Uwc%<=K`VFFr5uRK^WXd`+5yD*@Aj{e@BV9d z0QH|#0h17-s;-h3UdLe}W+}zqz)>N@ZKpoRSZ6p%^!%>3p+-RKS?Z1tbb=9nY=91s*vYfcq91q&8++(!Kmz0n zHt@re6V!v1qGnUsN^@M&r4%T+fXdJu^n8955@10yjzs&-J!^0-vsyty5^k_JH2_ed z5$SIpu1r*5XQ%s6m5bueMIE3)2AH(Segj>PZaw%q1ar?zCnW>r$v4T5`bZKizfEWf zfquU+3x$lw`(>UWw#7QSHPely;+UNS{Sh5`7cm?+@)=o6sNr&VIt6DqtK~B3PC97v zxD*`{RGMgZs{%YFiJ<&E+rlovKzr`y{wRAbz8kept}}Z$D#Opka*VMAaPWxn@q#8q z4kUK#cqdMW9(6;AC?dpf0`aS>X$>TjJ>3@GTDJq_K2Y=tJ(D#=0jZxH-)(QXzg)v zoNq9=Cg*O60Jh+HR|bXN{4LyOJ7&?Qn)7W4_nV2(D2gBPOF+DK9y&NNumLI>*w^&O ztVh6=q=PmUm87^yP%(TePJm@K8jd%c!dD@a!3Z1<2W1jJeZT-GzZlLYUbx@zK%M#+Q=-#G^JYqGd9R;bnjeUXT4Kp&NQ4AtPU_rqb?}kBYXOj0jNh5N}wZ z^plzgE+Ljf2c-!h_uSNAl6{hW>Jj$nP3f^hr2@6Njoog>fPa!yUD<{hbr>vS(kF~4 zptA6SEejM;$QIfjyh>}0j*@@W;7G^87eOX^6LATGHFk#sLB7ZxFc z2;`u`E>u=3z48XyG*V6PBf!QihS!rq#ea#_SSLyn_h5C9_u$7H5HD*>xgQMivnZgs z0wqby{~;^E|K{3xZg1FLBgBryR|Xek=xNGQ)BX{kp>vW?ee|nb{nNJdm|Z45(B92b zJQh4=Wqh6#e9&K!5Z%_El-5S;=^T8VqIE~F+m=1@eQsjIJt`QC{3E~_w1Nql?BmD~ zs-@>Vcg2H59I*q-gB3{L-<*kS88+0c(t7$dxw*;7)3+VBZ@kW?Lg;mVClZhm4>1Xp z3^|tv?^aVOf`OyYsrtd-HCmF2giLUB1aHG3u8n=xxG*KTi96B6BgoeRawFKcT`b;> zsTN!pNqG3bz_x(8<6YvFN-~DHZ&*WF+bzJIsY&!K9T#pkLs6GldO^!3IH(No-LCv-Ro z*H!@wn0=sY;9H5j{p8!&8=l-GZocXb(rxUje`cS3FNzgKEs=B(B(_aehS7hN=s9_*Kf#U8A90mVaLnZim4HbN29Tw{TN;-UZqESUKi zAhg&e=7mRu8pziL_=G8qb|q33R8|(+WzrvVK~97BANhWUvA~)UFN8ygFp!L*Y$Z4p z0qzYTsYrf$aevP7@LJJeF;y_~1%r9i7-|{~{lH%{qEqR$CLTsRYmupoK^lxSFi1=E zq%C#^Q5gW82@pYQa%8YqC|0td`?vS1E9Hsc4};%PmdV#>|E1ik*iO((q_aU?NAUrO zwyN0zh<73;GOuzeD{Ee^xIXxZ7{ER?jlN)C7MEdna%+om@G*EaF05k;aUGwH7INNJ z=GLD7J4KKVgKY~72Q-F9n*Ag85GPhxxtMBHy6gn+G z^*c~_rxcX>w(ivFvK<3%!!?wPK)((UFKGJRe$cb0Hr{#Tpotg^dU^sk6g=yz=X~rzh|Ln$a}Q528s4LxW-PQ{9b5X5_9R->O>G zvt&xAJq$MN!7q{X9x)z@DH;8M=^M1@@E|Di9E8vu9vom>q1+Dt3z9Y z&HZuk=ah?Z5Jkh0t`A)fOauxknUjlj1YeEz>;^(#w$dZLO@XIrgsi0Xb+iyi+|5RZ zNbtMPBD#7)Xp6K<+8Wv^1(JaQ*r!h*P9p(hUiwJ3BeRnN1^u(Q>prIy@L1T<|lBSBJ|Ieo1)szkj5|=m`J!!ZR?u7H|DTT8{%W zllgmR*0%o3Pagh5UOY8__^ucKo}9rOGqabr4sAYPX#W$Z@{^PK_P?3Q>t^WR+86(x z%+E|t&a@BA=h<&&X71(x3peqH|4YXWgGr-LKh@Ub%OAIow(l83Nq7J9NAK(H2`He_ z`Q^*oc>%u;?rQ9yad-cJNghe?|C}@)m>UbWeD3MXAKGID)0a~GZVM`W(Er=Vyd|j% zrIzORe@~L<2I>E^WKYjc;ey$u*iYeQ$$7hOgQ`7!&aBt|FUopLXVW+jm*e8s#Lel6@UK`#2Oj|2GNcvUcXW{L5D`!c+W2Gn3!vii+Kn zpM+AUS%xC_)i|_yAp2M6gnFcp16fD)R_g+rI7`cJ-v8zh=VE##=HzeCJT!7`yT|x= z(O(O#TLgd8J5?N6J?4={*q;y5-_f$zI!21p$kWj;G5Yy=8#r(FuFE%noo`-~XIIbv zrg6?sHb0G#|EB*LZYy6x`(W&R%h_VB zzGR`Ue(`wX7NfXthi-LP#_j4HQ+^cjDaUV1!*3uYdm zd%CwZq}yh0A*DQ?vlylwdy*P5g=`M?jW7Rgy-$gWoi!FFm63 z65EzdKDPKwdHAKr^Cceu>Q zhObiJ`WG?2z7+b$7cp)mwf<-6J$mNi{pkkH#vjDXW%ESoe};6NBd&BV84uM;De2qXsg`XEt9JSvyXpk z+b_`=9_0hPlx95I@0#}kd^Utv`aVDvf`|WTArI0@CJ7^XB3WGcIIlwxj=WmR7_`~C8C%Pu}98#d3*)gq6oJkaT=0^Km0Fxb>UBh&j?v^ znxCVNFW2!m9qaJ-=otEo9`UKU$F?VNTU#6Aws4;dZ~Wf;#O{24_e6Uz@g8gw7eRUn z%1^~w=D#r#cuw9GO#uH7;@zGGQO9sSLnm*&nZIpj_VN5A7RJ3Z?C z14qwaYELltIvv#I?7pz_uMsY`>At24)=h%cIPMF)gBp)F55S;E<1E;sdVa4cHP8+=gl{_XM$!5`T?4P zulpDMzISet@CjJ<`SF|xOa+#M9ezFU-aQl8j|lu}z)74NxB!@B>g9bsSTb(|FCLiV zi3J}2jnF`%U_?fil}0xfrziYByohA8h<6qP<$xGF{deqO{Z9=y`}bsY`;wIrMeHAu zz~ii+FrsTqkHgdPVQ{mB(7M;VEMn_9)baWd-`{_lY(~lc3_Ra2inUlqk zyZkgO2HuFca3DBG(?&DFikZANe!L7o&AQcEz8c=PS&oDwJ*$bERzPpg``7{WZj#{z z2Vx^bwGz-xBUGY;JS3<=2L9aNziQE<*Ho%g8UgJYm-Yj4R3%mDdseHm)ylQ&zE8uk5 zwHJS7jvo`GK{FZ`iir*_J{^v`@1h?H5Y_^W-=6fKSWrqA zE_}_Z)zXqZx|(pStyIwv=#o&4%&UPs4AG=fjz6R00d7A|_rJ(r!h zvfQaei$%9%Jh7xV_GB+zIq3T0{RIqV;N5|QI3m0ote42Jf%dJBV~8-mNCW%$yex9U z1EX!SYF-RR>zpdXIJX4*J~Bn&sIvSWaV35EMtoB?p6|B}Ru4JZ_wL^2MbqZY$;boB z*7V@PK9EG4%JH|v;ul;!vKEbgP_!;ASs@fWHS&#JGes|MUUQ~qctL&T*oEtS*?aQH zAAtjS8d;Gp$R2us2qeT+QZpP92qnX>g72@*&XIBtGbU~ZhQ3@@ z_N_X!v3qHLtS27?H=nqDX3Yw3<$+hfx3JjjI_Ke!?fzlNjHcZ`-LP@PKz5{W*Yas4 zmUpG|u3xbyvXZZZ*9M~C@B0#Vm1L7Z{#zo^5zzaBcNq}S4FlI8W;-^Yd(*j_JN$=J z2lik0?(6ms_z!IKfzTk#IzAN}`Q3<*No^X}0TdN7%(Sa*lRce<5qPE4DY z=e;`~$E(at>i34jcbjpSzri|cqcv8Hb|tiwHJEPxFx`=E{;E7;oATlq%kejB10Ze^ zysFoFH&)N^x1k>fk37hJi2g_rjQQ{JdE^x- z>`s2ExvYpDo^Na2LL!@unz&T2gx3@HyKoCRJDAAQEwI(NP8KPT2GRd6*6^A?DlMrr zpY?B}QDb@jc4dU#9?rJj9WHArQ}Aj~iMUwdn}KV=)l)+~FIZoJ$fr~;NZ!g0+8%;p z!V$jNIAM{6Hm%mKL9tu}BVJm_7R9D>n%&(QYzT(j3bo*Drelwn$ zoPCa84|`y&vvob0CC$RPe6)F%@Ew;uG(FwiEPP;knm%*tkza||i8}975{NrE9zUmw zxx+saa}n0re1`p=eslTy`jLml_d~u4m{qI_X9$)D28Q~Tz;s0&FVA3P{537i#-h>K zGJ8=tE!T6So;RAS%jvLM@pP-27pl3cRaa4X9dsiFBR%_US}#P7L6VR0fmO&0< zVuQ33{9MQ&L9nUSC$HCpqD0v)P&;7rlOITQ9Q`HL%(A%W#Z%#|e||k96x-)dJ9Vp4 znEfukpT_|nY7;?Orcza>Ix?%AB%xeA<8~OAJquG70 zKl>dlG;sLAhWPe}P>}h-fNW&fx?Ga2-G&)o`y%Sq)unag%YeJ~S1? z0FYh4U^@UV`Q2b}+^Be!7Q9T7u8>2oQPRQXR zetnAwZ7jn#B49aCiM;9p{Z1 zHB^o3`}dg%%L5onHEO38Y$lp=WGgP}A9ZnqZsYQVGqvFCM~9+OX>}yD1|Zlep|(^t z?L>zmg(5?`og*?@L3sUdLg&og$C3rNag~4l{=Ix_!A*dx?>j+s%A#AL`Befc{IX|-Zv+fj zy$s@PyV39MR&*H;kZT548(TzFw~Y+=yN-gt_ZXYT3@S|3@;yBu)5Q0MjjB=;#hOynY`diD^o9neT)-f0(B|;B(453} zZyVn@Qr|RMTwE8HoH#mq;u4|0xH!70KC*Fq*@?r$CocQ1=JU+ny!p+WHwPGd9EX`dA@)wxy1Cd%K)@^A{) zXsb^EO2`0q20Mo`1`ky#Uo|7A9=jtFAlI&gdv+ zJaFY{uRQxwdl^WaF8E^P=G`E#Qti;*>)w7B_;JmUkwBMViGA+AE6)l$W>GTFef8Cw zR(F%ifX?_c_8qd=iL4kf(QIU?0Hxvg^}@x#a&movYv!Erbn#f+9PbaN0R9KsTlkkv zl_+%08P3?m>s#ws^_-8A{rg!5^PsxZ(MaghaezpLGtPe9jh9`uf6bC{R|42q6#Gue zgZu>OSEgljw0mMpc6MVU3_8h*RA#ZgGoHI;$HA>TE@K~DvL)l7>L=WDphoD|?pN$S z{jw`&)?D9TTntuv3#fEaXdLPVU>x5BJiKd|y6VVm?bZ@dy(owZ%GqGH`Gw_ss;V+_ z`uNz)W{d?3jIed$$Anh`eTU94*lSU!1EsAXKbbGe0g*4a)__7^0Xlhbd4yb#9YT#@ z*zG~|qn{X*tnM^hCz%D5Lj%|Ayvp%tL+c~KH!*sE}6NJGnb^`)+{`Zo+HE4finE}}LF3~20i zaYqeTQ_V&=8MaU+rsU0UJmaFc!tBh>-tgDP{u0$L1OaAs0Ie?L>fhtyb|4ABdg6BU zu=uvWcK39z485=)Kp&X_90HiC#(vF%r%W!QgkM26_)_b~RpeR^)@ZqZtpi8n+4IaAo z-t_=}o9Dm&HQPHYRj7biZplXfz5?jz^Bnsa!+wxk5}nl(G5GlbF^OSUfEr4jCQd{E5jp|g+Jp{>7!qu zF=eSEHwo_+-bc2HlLRToy8<5{ry?PvC{;UJLeFC$MdH!PW4`NCV=+xc z>27uaEkxc}h(uIxrEJ=@&m40N_YI&~3i_zmJ3^>XC`DW?GmK{m_L-NyHr!{RF1$fJ zpT`z>7}5ptw=Wwa831-5I#ye=KcdJnw4}`+S~*n}&rf#j+5YC{zwOQ=q>=hK3*NtH zXbJf6R>Fq7Tg>x)YMRHwkj^Pa%Y3gITM&yvx$%57D#pI~2ch)q=9h%hD;A$|%RRT8 zvDkm;TCw61xGR_V4@0bTd>q@>A5TpDseR0?XG~6>!M<|$NBx8N1MEcak^e+L*{9&~ zU4q%eP}BX8Et^7F3^p;`G;kjyPptKA9Z5|2up%wHM#qsW_;NqX?9)-s2Lg+l=oXAC zE>kmxShpQf_S92PQ{(6sXg37#uUvVFUI_~gR91)LIy?R4d?@i$q}M|cZZMeYi~a1^ zP9IAAbw{x~-OoSn^n$7vBo=nf``L4`VGk5$o@OnIJ@qm^gTV}}1FyvcVgWdS0E?iU zjrw&A!EbB#}4d3sKMcE-};( zo+g*R(ZS66Yf1926CS`YOfkP^{v4EH+$PWkkFrTuD)t)B@!$6sT+ITj))5v_tcvoS z|M1B|M2^WISWASzBK%Vf6x^_>X7h783~Bq1$EnDjdVd?uskH3HLyZJGs5cL`pDzVn zoaLc!kH+ueZrno}-5UZ}b0|0@4p{8JN71mW^{jOdhzwxwOWW^ZR9*YGuoz(z)H0ET z`fO!X*R|k|d>%FGq0C042bks9J)dEQ7j7)|@9xg9X{8nR8T9cjhS-M%b7oK2GxnBU zMLcub}dUA}B%8g!!k?n66|-`(%(EELAAv#m!yF1$zhD*D|8s-zRK zBSza)P_1Ef8He*kzQQTc&(}i-ir_jz?2e*k zV$qQtc8XZoHG78e>De8n_=GqkX)cV+1*UKrXx{q+~&O8(#LG0p-H-~SpOUM$PVFU$2& zq?`FqTpp~#-KNq)|@8Kl$}aXYMV{Z>73p zUM;$~TJ77F=|$u@y0}(}t$PW`B=XHKk$GN-KB3$vd&`QB84JAF9lOKppN&|CyCStK zY8uY+WC!B5QXp~U4)#6q%$BSeh+nki1JoV@r5 z9=QJGBgnl5C@+vylQw|1Ap7O#IVmoINE+99l-wkys$uSc<0a28g{0*JGl+l$8ZX@% z5=X~C#clksuU8b7ZC|uo1fd}c;pE2hjFmfY(mW@sn}(XYKtjJuuU}QyEYXt@Y5Hc= z7LWIW;323+Ba*a!yP6q%WY{_XV9#*k;=%rCV^cjF{Cy}|TI4R9K5duV5sa!rI$5mS zm66)D*^|I^pAUnwx6~&G!zQ$-5o`2im#wLgE8ky$zXB2MxcqOO(n>g zPkKR@Pva?kuCF1bR9lMn#>$aI^DY*0dkb+|saDA?LEc5t!>iDAT^Do36c?hzIN zxklVx@d5bouu>!{T>mctEe#U<6yX^f!MI_x{8S3-w=#EJHMV{EMP@3uzjs}*oF2R> z7uUacfgvcd8)|OYW_#Gu6IWffesPx+bq0D@pJtEMS0s``=%%cO{-Om($;SAY{O-tO z!q*YIDbVhMu7OIm7hD9bcGx7WeY%VY*-?KYDMT`e=7QP#H@>oByyEI%2fp29d!uM9{$sZKoW`4`@0|%n%oS@7zUJ(rw^j@#GU!rhC=F1v zXN56WvVtkh({GP_T{tY{0?SYxf#@Cuk`Oq?yz^@q#6|KrvVWb}1O>5diAOJsZsOcj z&?RFS10-;Covn?j2Wog}WNLpQUfeQh0J5NXk&i{AVkl~e$<=GsLdGy6g)Kc`xUe$J z-M?qTmcjpQW_ILERt$^Bt@ThW99=nDuq$9y9^ZIjJ*~uyppiulW)Di6(YH0d{sb{y z>FNNxRZ3@?YM?z(PeeYU$7A1MXe4BUPTG!v6Dxz^$GzTry4P+&%=9PZ7-(xd!Bywst1i)e3vV#mh}Ol@qp{v@EqWi}r7BVQfvL zkjX^{*KFQ6MBr;#W7iM`=L@xUI4-^LLb0!txPA0ObfvcQf`wJK8Z?*UbFN)y-Xk zYgXB+XeiXZZJO=a6y3NLQRb4?dv9X7%WSP2lNvMG<+Qwdm*G z4f-nE$$((3CbN-vvBTDMw_J~X6#6B(v`}zSL~SLV>c(1ch^^>TN{eekzoSH>4M*rG z1qYU{4rd$DN;tFhU11iEg;BRJ*vpq$D&;oGCjCS_Mff@L4QX!6F-pS_VOXVD9K%F{ zObUReo3dF81_e$Mv5*W8m7zK?C`i!z2m4(q_{d<*&=!GIEI#V52U_G9#1S?v3DUL% zTLaFHYBZ&9-e!WSf7{7H@kk(X+L^-8rY?QxEvcRzm1wNL#NHS`?iv})c+rd1xSo6s zIG8#RcgtqaWtP;HIx(X}&1AIm8g@;G+}JviWkT2iGvwz|>hgx{1f9wfUH$|+eoE=x zyhvE$fwGYah5nRu5|h^4=at7oPT8%UkV?NIn~QqMu$Xwr0=<~(MLv>nV3bmtTRdZU zcsXA?hmJfWzDIZg`g|Au_C0ChpP+FPS^`Dh3^8XBnI`)TA25akuPM596+I;9=KT5{va zHZ7{g<*})KnS3(LRCHcc^@|2eMNL2_@u~m{$5mtbNTmCt4HK5EJhAcyQ;m#b-YavSeGbExGrGaW}>^ z8waoW8(%ik*UVH6T{`D`= z0dOa3+#B8!vD51M`xBNArU!Iym-LyFpe_MjnrgG=7UW9~&HXF;3G!R)J%u>p!oS>q z*K?3`5rE?GCB#5Ah_76=S1FlWo-diW6A|m{MO&BlHKe1qsEuHBrk0zHyeffBK3`zB zdQwMDaBXgRY)?97iYA_4Q}FN7C9fjK!hv&>BdPlQ+&9P^`5kgtXe8sGHKee@G;Cxk zjT;F$`|urlW#G5K{Nmz(s?xvE|Vh}dDFFH-7c{> ze{xUrXhNR--H7Su?2Bbt<+6ps_y)guv92~IZfePkPGU!OQ(K#HDovg?l-hwzf~ZU)4hEfSRPwci>=mY)j0nI) zk}r~#LBi8*Cfz;}6fwy{CPE^z&zF($=BNzVH?t|oAP;tCAhTaU4$c{Mq9&;H>ZO7q zf1yHBvA%$Wein%cr6wRhrL@yDtGxbmThh+ASYqZ{Udpp{2pCIH2+IIeHsp&LvsbQleEf)5Ym+Wg49%b3K< z@5Mll6n|iSGiJPfxiL=u6EyKLOIm*GIHnY?`Oz4r#4mm4j%vHs6`mZ-+sF^&v)GOJ zEFJI05I&EZd-}6OSFxL3`!io7B&n4JNX6%)mlDwh{ueiI zUaEsMimOCy-S7!Oc`51OQWe1{$YoS9!Kp-U#e@W(^E03VVJ0ypi)C^-%w4Z<0nsT>bgbC5^{^O1LrJNMqFcS5hF61vXO2fRsT_CvL$juRW!sQo(bCv z*)Y7PU(@^v3m2235TSwHU0JGvTo{Ise@+1+3icUidiL!bqNaywf7PqMQ!}vl_AJJ6 z#2FFO_BJJJdOC`(6<@Rw$Tb0@T8?EpWuFT-y*X&>1!G@!*F(acBzC%sH<6r%5j=73vY@7f&QyL~Ov4TM;F7 zcKewsdbZwG#`xTnl1V76I|dMjDFMj@`9C=KO|}U>n~x5_D*6WCvLiwRxCPG&e!)aV z!bm)z=8--QFr8GXLua3Yh~{6|d2hdo?of&*Xhz(7(}Wt0&;FW>n&pmr z14>U?l7=5xRY&8rr8B9Sx|W;YP?NyOr)+ZbN9|Hn5iJ)4SgrlTZVBx9J?;H~Q`_W^ zvbA{tFkeS5&l}Ld|UcT!cVs6Cix=it?6Md?A6{Z3VF& zAMRJGX6zhfqZh1*JLqAURuXTcIQLUvMZSW31FCBT0U!0dDbj*s+~^@g9nil7C%nCl z*+rs@^G(X(>8Us=* zIar%+iV3kO`LC0OjEvD%imtuw3z&V`y0h7W20%#!t&=ICrjkddlpa9l!CQ{hrR#wcA>Q+?ry zolB}in@9Kiezq^%?=Ey?{eRdqGLQ2w8gAJcUkJMJR}#i26GC8_!;bA_A5wFAzs*^$ zIAi{Ggv6H8Leli7bk|CDro0K0!z(zEfb1A>{U+E4$Ka>)^#w}ffe1(WDYW_U*? zL6J#Zw=DoZP>L*P7GoY(^)*quJeJ;2)5)W(6=xP@cTV>f8oW4?1(nO{(u!Rl-Am>lhI6h>$?i^Oh!=q7<21| z7$ZvF%hv%ADEb}Ecdn@$U@+2MQFtpozFQi~+Hs-=j-#~S%?~Zj$Nh0V;TSSOg{>>@ z#Zn3s1Uf_aavFQl@IGwAcb?-sQ9}{t+wd~87)SIf_DE9#U91~-;$mAuu-xKnaC$X} zH|<<0!OeayrJVcbrI?g{6WLO-Y-Mp2p;orRQ~HWLY(#ZNIQmMZ!iV{2w(7dMU$Dzr z9=U)b>eRHRNJB9CNK+d@VJ>JsqemSUszY2%dkF-~;iNRQ?}#)v4=0+!z9|r?Lg^a5 zTnlCh8t}9S!py?nB`paBg8ngq8Ut#xbl=yqa=7pIs=YL$c2;{YKiAm6n>$xg z6rX*RAmx`644V)E6YI&!7Ep1s7oDAyNWzGkW0=rvahnbPC1v(#Om}Q)Fp79yiUwyZ zKs9cUsJv&hLs5y^iARu^Z9!&f0*HYy3kKJuv+^dh&aOg^jw%FWa| zpfk0AzC$F`@Kwc9ytCH0*wTPJH5*Lx+9)vV)y(Jz6Z~s#9%~-jefX^zkl-f1Kbc{K zQ~igBtL-ciV(l%-q(g8&j*V~z+!|I+3PbySQ()l`nNI$RCHs?w=`Dk!7>os`{gIhs zlRDnfmNH|(^d}|RkE0ox+6lUsQ`if&6Fadv=>39iU}%ap%&F1~!v8gP%C=#C%3zEh z@27(_pI-?PXh9HlbovPu1-U5dYrpyE$-B7s zpT0TCC(D0#VDHhjZ~vk0`WSEVLq@pZz)|`3wSA!9*Oiu~6K|h7Br1LafdU(T&OgJC z%2hRCg3MWb7B1|(0^rcApBGlV)G}DiZj>6U(Gt8#;aa^37kAPgwMFwK*0;r#yOXHj zE;soH1}xXN-BFAevY?kt)ycWvutVg1RV|e@7x5S)T81Lx5I73{!0He@SF8&yfzm5R zPve>*uQ2;QgOLMV5XB^ln&$Quxn)axCga(>l^)$&zvsm8Pabv>X%~|N$?kxNB#;%} zM6YFXvVQjyeJF%Q>s7hBu=2*~zw2r0m4c)|ffa1k(YfDZZdC>q{~_=#P+4|8Z(U3o z=S3@86=exyJ*=$aX?9R(>lxXm$+N#@P8G!WGx{dFWIv`beoa(cdB@S|ZYRZLdWODu za_sOA)_alb5C+F*gKN=59LPP@kg^)fCgd^9=P+W6An)H}gBkKw+KGoMEyaLSWrrq& z${6M>JG$n9+evRcQ$s9GoDbf$){SHTzz z7>hIBaq;>C+#)8*#*3C|X3k@0)|N9fA1GrI1pv`P|NFZOU3`qC0i_#h8Co_DoSrwb2~#G?ZgObm1zxpS4&t>^ax9v&S`0LQcR`ypFN4v zq%Mn~emYbg9nHx-G&v+rdC&acbKES4e%2qchcWU;1ZH9cyk=EJ4{C-I44tV2osXa_ z6DmDKUyoc7Js`O6e$WZJgs?VPd2*6=dSYM$3=(bKQZ@ z)wB~W*zmQUMiB#r>sA#p-9t1nXPP@AGKzIH02J5{&U1~JY$lvfJs69nU322~NNx5!zy2SVmIwCT?LYH>N8Bu9l~~0V6y3l$ zvqm)VV%uJd0Wa z;c)ZS{gZq*_r)mqUAK7){@<(3_>sZ0P&KRiR1&QIPcN=9Zo& zK2tsWheJN7^dtUw_aSs)MkkD?>i@QSzSqVjeR@Sx4muO z?0@xu8n?(U>z7b;MyE5j6pLw)_A9f~tV@aS|^5FOOO1J*&l z+Oa=n_hi)J*$@B2#WkamT9WE5*zrJ$b`M=tJCI}j=8Zz{K#Utc_~~Ajr)?25+gN9J(~); z#K}cve6^?sb@d#)TPnL7=KjQPCy&71LYTDDM8jPg4--^LE6z!02Vf`+&E4UOVUqKL zxxpI^DWJ{gA5JYBuC|g~da^pl9v@2^s{|3}3!~m|1i4JxA9K z;$3(j`uO*fUjrXngUkp{84eTr)Nn}fjaFf$&nYKU6I`xaV^ z&~ng=MC)zz_*vKs>PfUy)7gYcXW{V%^spHi6?h;8ft+q`+Ql`EMQdmGu@R%lF_9F} zJ3w*Yi4me*3_ysOeEWNhTxp71Hqp%dieS3c9GhlZ7bUfUHdZ{kI}ycz#>j!AXh2|M zT_jTN%9iY;NO9;Ivys@A=8~Nfuj)j}`i<^eFqBu7J~aCq&(#ar z8b9YqUSqzolXR7sV!dZYJe_g%NGy&HEaK_6V_E|vHYN_ED~||lfE*sdIrE|Xz5t9p zm0x$&RhT<{mNLXCh9^E>eG&gEBJF<8&xF36m4wNC7$o3GsV;yv?aY<0P~4w$P(g31 z7EM=NYo3+tW9gX6s@3gC9;t~>yqbgf8*~lGy;(S9!V-V}Ouobh;hD66G3n~FK9l(t z?Z3`pIMg=6>Yd#wd@Wm>$<%)06^j4v-riN+-DfBFx=f}nbH%F>IHPv2q8|vK_ffpJ zo54*)nR?(|!MVk*X@MRlZvlD-TRBB0ji# zW`E0MDN=u+b(<|!EBVIvn3A8=cV0ef%4_cLjFP<>HGAr@oKc;A&FTMr*@2PsZyCS- zxZ|^_h{6l4x)2MLMZY?5M?w1P=eN^@6&tSc3*-l|#S@qt5puXadr#=_<#S(k>3JSA zt9j!olXsg@-xoos4CAAdk4QMQ% z+4Ga=rxmM)Y2RiQLSae}K>Ib%eYYVr44TZI%4X}*>Gz-84u93tv#O_uH0SV7>vEmv zva8E_sGjw}uRAjL2lfu|PFdj6=tGBgRW}9C!)Kc(z9?7o^tvtNT6pTo(Y>PDK~oNu zI1*nX*Xq)IOeGMY@fm!UBbZ;;*qD%lL`RTaMOX{~PK)M}eB9$U&3=1%W>G}-8t7XcGB;!MK%ICcXpVxwk1!wV@=o5cV5(jcVqn44EU(OnTnIT$Hr8g;>U%-&oP87L3 z;o!?G^WJ5KiWx~Gz$I91DY-WVmxG_8y$uWNzZ>hXqj{T`msxRMLl$h}%P;!!Tw@OZ z?e6bi*Vp$lOI@GO<1bJ5t)qXLuJ2uQzhbwM&*1H9tN1a97bq2)Ppd{H9Bl;;0S6mR z_y&c@(a2X6a3p6U<1n7< z#6K>RV~qn`zPTmIW=CefArA7$lobG!A_^lZ-CmN3;w{w5 zZ!+b2p|GyaQdnlOh5>a>mYtatx%l>a-pkgktv@0!S;uklwY|n<}SE2iXF~GX_ zc0i<`a(L@2xt*xNY8su2NBEq4c=mg({dhL>HTsi=s@9E1Eh+0_ATA@YwHPs+MUxE} zHiF4!Lqm6xWd+)IqCV6mrq&jkYPNDcd8Rl(3^)HFiTVh9DCe)SG4SPY2aC@gRd1#~ zBb9YU(uiE=GX?YUZ79vK-GGV~RrE5zMwoOs#L_AlSgk@t(u+v&Rj5#NvoSAo&LD@1BSXQnTqs5f5vSp}+K4JdszQ^X~&+ay`#NzB|#^5-6@M?-p z4F-mMt|zvgdia^azCOeRSLy5vpOeP1m4l);2kZbD(1hTaFt`!}tY7&t-5SO*Sv+>3 z=SFiO-<?h=wwW0y3s0r!Ny%>QD3Z8hqMw&2f*;05d!nFv2$kWXxv#Nz!=G2GR>Qv$ zL{JIA_w5!+5E62c5EsqUifN4bq|q1BYhhbz7>9*9ZknM*L4iL0kRD(e)Gy{l|JO@J zgKKQ{oDy@tW1-QeT1)m!$a)MD&rj9mR*b~l!lK~b8wdJoOH!jPw(Vbe*Ue5O8IcpQ z2lBC!ny;(P@K--wJ75~@szRQ#MstiB$edv4M%QvXiP6ldWV^Ba!l9U7K0VS(Zfwc& z&2IaOL6PaJ6@5(WtkW@7-5%YzX_3m}&ASTvax1Vp8v0g_LSrgvN1B~27fKa}Ayf(K z+bdd=fzVEq21w(z3LnULvagU)=gZz8I^?7$0QKOm`1D3>~92y|{@P z#pLXWl3vwh@(P#G98UB;bA{byYO$o|wca|>-u*yzOH}3Mj$F|lU(!)#g7xR~oDY9Y z;gS5mvC~3DnytEi?%&x%zzJ*Myxtnx!i9jly9*SIw5yh_WM!#%nmBrgDrKpz1!w|P zi3U#;@f8M7A+nGr$jh{YEOg+?%-X2gJPcndJ*Itn&$MffPdvJ_KHJ3)=X)_Y5)|m^ zh>P4Px8rz0P+hmCtSve+B5ay`JT9?I_S9`72WGrg7x1Rrx@p;I@zIOex-U zuy^B?vme=fEKR0YHPn@tcNoAiVK%heFp_O|*LHX|CrOrD-r2Kvk&f|-Gcv1XCmVL| z;x}$)K6Ir~mrjK-6;t)Kxi64kk!Qm00r0X4*cMXM*Z@@r+B-)>LYk0`r9%X|4Q&;Lh=g4$Lw7E3x502AN)}3t@Bs7bZ;7&^&FWR7UqkyPCSXVSZ_1 zw?dgQU;2VM4m&XyGCRzFcP#5@;}^GIv215ji7UX-G%Z@2ap7`+!(>sb6XSwDQ}?y) z%La=&v7<(+PF4K_D&M|ul4D9sXY#A`A8}@Nl5eU@|Ebn+qE=vU{Xof`C;@E(*Eap0i_z zh94C>SZ1;Q@DKrfq-pBPL5I7h34(d1B6|8PkQ<1ZLnqi@7?uu;!vv_EE*R_#4N~+jd)Bx_^ zUF3{x^)%-&8wI<+C@sp5&i>EQq??mSrf43hnyXJl=_FSsTWHvTEUjp${To}hNNj(< zv7)&yYpFn75wFDyWPZAakETS!Vim~XCv)#%BP5Ofdh|Y`%n-h{^vcs~%yz&AVymEh z>FzXe)5A**ou=;z)dLjz{0KdFn!Ts53-vwe#X!VC+aGCBj9hIc7-e}K8sqbSi!eZy z6;osmoFaN8C1|F{fE6{-x2qG$d#1W5Is?U(_biFmKp#^#wDc|T};V_p4ZbpvT= zpZ#eA*s{T*(%gPScg;KCbJn4GLqt#9A*7QF}MfCffIpp4^7o@}f*^S*y0ysIBQ`JHDSDNOhiQ+YsklOW4N#FZZ|P(zXB? z0MLuL9CKeG-y;dU0YI#PhfbhX9mt?u?V_4P4IR~P*!JomgakvSVd&6sO?|C^rRlTE zu`@R-D84m!b{NU*rEG2_iecm#!kF2EQAuFBw|J5s2}VatCq{-xPjx7OT(a?6Z7KQ) z4NR_%NgVUYX`-u^jN2jG%$FigyD7#4qcJ%{x(1sy=5iA=kYU{who0T1=YGwm$dBO5 z13_BK!_Ns|vFWly9mE&Zb%BM(+7X^ryGe|)p1;yiNbG=SooPuo+(nt$x}Egbv(+=! z$kHQhra=R@#qK6A&sf>FCOtc!N zSq1u-hIgm1z}ryiP@gxT)UMDpWyHnf65gzZb{DFbOy9k?w6AF$?>Ta}QEEN8Vc%#g zyKi4zz92hEKMio~sd%=rahnm5+2to(-pEY`4B3?L-?1Sxs0Ui2b8vzw){)+5nv;jG zI=03aYp>hG)T@%|W*a~g9T|40)c~z6VVdT?PToa+4$rF->+YbYg6aXiArzONaObhLJxNaW>OcC4H{GaPB9Wt1H{J)nkO+9! zWQt&+Oip>c-+o&ujJDzRLUY46n&JcThtyKxo83bkJ5?VZ6$l75JQC(-S|6gL z{z+zpS$*p|fBUAMwG-Xi)|NOUEH7^8y>WvakLlKhODB8H)a_Zz)iQbsEHW*_ha+|@ zY4u*dc-MjcU4!lQjtDZJ*r$x4L!;c_qP|#rwLg?^))!|?yY{4(Ol#UT8`q8l^(_iY z6nCCQMv9f4^^<$i_PGgTqXVj;DIesg+(srYee3!$j-e!~{g zjiH`SP)`r>(s;Zp<{dVj9vc2d=&gZX8Eyac=1@`q$ZUwP@77A`CBx;Qkz}%^PmhBN zA!PW^M7)%vMst_yoC)qBtIm|wl;mkDbL_4yb&O?2+7?e5A5gT0 zpxZ02=0<0VC zlyi{W)E-Ho*Rn}eJkMVF#x;xcOHMa$s0SEM6N^{uSQ=A5sYi-A_axT!74)dZNPk#Q z3aMT1z%Cg~Tmw2c-Hqmusio}+7FC9*1q?6kimeXS{0py`0Aw+#{vd&^zbKF!Sz_*R1 zIZ-EV=-`f?=D@F(PE>c3sp`%hBds0Cm!%sAA4{mp#jZP4-&r(cX`6H619@j{vljLE zb(?b1fSi^LM`UB>+6G>ya3SpZz})BA!{8IxMJ>aEcEL}AmSN}gRfT7bj(09!p~DTg zPU8gxa1<9F?i@G}Kz8SECf#kQx%dYZIpELlKA*7b@Rk`FzUa;z>uwW<4-L1k-m<1s z2esq=zI|&-b@oG75YVHh69kE+*g#562K=UJm5}WLP7#DLoRm8yNDld#3aU7cU9%)G zcvc*)mDkosnAY8Q?3-%O7$PewT36eKALT7ZfBVp*W4fTnOFkQ8lLyCZceY`ci*(}k zsdBMd@M0jYWw^e>qnwK=7l^a2oqGy#))&D9?o!+@kVf=@E#zVNX?>Gk#n4fv;Y8?N zAug}by~Ey9*CQOa(+m%V(5qh21?cb@8kevkCv2cF&GhBdEm zxqNH1Hd-#%q`Y)(etlUVnsBR^n`2XrJX1H6O^gj*bjo+2$7Q`o#$^)!VESXSDw4TAo)+BQ=TLNk7`^>e&L*_u$%^ z=Gbu39rlda-=|jg?20wWm$t?twQH@D8*0*4GSE+b|NB8ilJe%^;|+zlqtqW=(znlA zvw!hP!0cjHlz^M9kCkFIrYe=QY|3enm{>V)b!m2_E}}|fMTnBENk#2LcF>WkH-dWA zklPZA?X27pP3qBvy!;c@S=Rj{I zvvRPOEsyboZv};B#4dH59-*TcKpD)O$K6-nUt$HVT)XIwm4%s&)lJEONiQ1=q7kim zZ2&ePw8`4-H=_u^T4r@L-E8Vgq?*xc?`dLQSHm>*Wv5iPn3oK{J%4MS&1yo1&w5jR zV-jN%#;KEaQ%lY)n##w8Y$H=>E=6-j^KGpbFC}7<-CI%(iR0Af0bT&fiHB#%g(ZSr zx_#~$_BQg}5Q9KNdOEO*dQ{=VQ1RP44W8-~sS0s>P!f((+4;;<0*PgBd!W~lc_Tl$ z{?Pg+L?tadGrr;bwgBfk%R9E7LDJ8Wo9oe; zq$N*0pO6dZK&rkt_ZfB(Vh)QUyGzeyv4A1-(Zn2jA0a9PE<81s9pS^c@D7GA0S!IF za2!Vn5H$3hT@)8wL9K5r=mr@3?wUOUK%>|;7_*WCMt_t~^pltnX=v+^{g_c`Y-qJT zos%V#U)q*N9W#@EtE1cL$%&L^nWFvRTclKhKmLxb$u-e-Gk9`&l2tC-?V2&K^+Gk` zMp*dUOrS4YOt?UC<205^&V86Ik`nO$Yp}0*&@1!`nM4%W!jKT_q`sCHFl~|WcG3t1 z=@t4klF&0D&^($-quGQ2q%Wag&79yp!YR587U8I@C_0ZW$#_7RTsvz8Gvh~eO zV{co;=%|yK!)?3=My1WuoKD`a>$1_@Q_p(Iu8i>KZmaq?wgems zoQUw}LW_m@8tNnc{cHOA%v^I0|FNaMHT3J#g+gN<X;{O~>P@vg$YSveSrV zp#Plytp6rA;b#>>X1y2sdDK0EqRoGj7p`*tOW^zd;=?Xxk@Rz$VTWybL%Sbw^lnEq zO)L9CtN11IhKUvl8C-E`;Yd8Lx!pQIfC#oo)unS^XD?+X++~U$!lBb^8|pzjTC-RL zB;oKTN`HYu3$>+T)|3by9-#0UCKa`zkuSwuvj@;5s@3<@?jOshqbuI}scoC~Eg8>% z8j>Hm;T;}=s$4C}+1~|fedp$}_02PT4(11ZLsXNCdk+luGQX;A6ADdUtIV5B{p?f6 z2I}yS<3i2Aga6Uuq^%eV#<5`b8!6rxS$*gEE9>0C(w2e4i&ie7vPNB)(aBSUho^?V za>j4ZwITgQ2ma(JhX*bt$_0P%J;qCSFqu>xqp13-*&p^fD0CZt`7Oc`Hd?^{>HZUE zeusDI2XkLwRc4m<-%y4^*ow+)3^l2E&uDamb$}Vcm+f72#2^i7;4d?}DyVh!t9|&h zhTX)B4}SA+Br7oEn#6tqp1+xlEWWa_@z`*;hEDzJi5m{9AmkQUG_~<8+u0w9k!VU2 zNZQYMYTvr}%~vhPdLBUyX*qdk*bfAQg%oxXLZI(IO@2|IoO)bgdhiUmy<5Pz%_ioN zAS!YXGdDSW+!7K(-(#b7?_8AftC!SlOE}cK*}r~t_NhCgag!XBe^KO7f@=)=F0iIq!5I(Jw^Ybc?O^QkgRx;BffuB~~p$5M@mlNetr zX$`-`XPuaP5_QOLQf~25H%8o7J_ffo>_7@3{Zt&G<)*a{mi~^aJ>fA$9CD{NLm`4%*zda!q+>e)@RPvmVo zlFG2KAqmtZ{zwMHnUf*s9?Ey^Z7Ev8f(GFD>90zjh=xiypV0Po(-H+#2OT;qBUd8l z%Oo3_hM5j6CzIxjw{6TvQ>&NmZoU4ait7>^m}lbh5^tCabB}(^aF7}-s|R=yO_-|q zmeV^LZP)8sef%Cd!mJ$=7#In6!;qQa`B(z7`qtcM*->yt_2Q7|(vbaqc4-yA64ghp zKr3z`EqFCjMHN)pv#;Az1I$nINr_$l#a)~7fchNSd&kJ_v;PX`H^Ljb$zsNrVB*mn zyp@R%^84D6m1dAKb?NC3udBC_P7aox_lD1@nSl*rt$=KU@3ZZf$Ni|hlrvuMMnFkO z?>1BQ^(v07VeY1rLqbLzSfT=LOIfJTpi+vdB~*qKutD)JVWEd6+|aaP0`byC{w+q# z43nSz&+izqh%6|YCSS6X5q8(AG1CzO@tLH^vqqMP_18$K(K46s6*3ZuumzGx5!^ge z`kJgE_zonv4;hUE<)^8bihDGqVz@&h6V7Y~g`KQ;i zyBeMk7Q9fKfO}5k8Z4Fs+_vqT!JWxF1@{xPyH-m=5>;Kv^SOIo1J0p`;z?7@n_Snu zkNQeEWA;5FGC!F80cF?VN)o4wBxe!6WLQLU^e2mhhUmppydpA!6_drJtASe#HAJTB z7vOt~k=l?Kr2>!$+DnyE0B#6P%hD_|(rt8IK>bw~3*8J@dUwOXql@^zgc%T%1jmcJ zALw-EOe?rQ35LpzkLNVb3R-tWBK7>w5Ial& z;`mvb)1P19{Obyjj3${YJ{cL9Vz}ZS$I-Li$fjD$&n({o53OgjcKq()?aA2dE*@$6 z+pCh8LZsC5>~~M`NaKGeud&ICc+8BlEMh2v$j=t+Ov|*a=-D!J-d)F`3$$H*a zqfg_qy_}I#Gpl%^mv5U}Sr1zhyz`WU{@d|Qbm`WJFY+fzAth&T|-gDxu>f<_pih-1Ghfy!4u{hb;I&^AN?m$ zJoGr6);kXfMAqfH7`Y?v$8_$g58OMJ6j`nA@brhiAKZSG>f4vSMK>791ZQUq=kp-e z@ULtO*3cX3+&QdczIi&lr2g|FC=x%56*8-m81E&4zsPWNCz57dDmQ}tt#no^Z^~<) zExY3;B1FF6plQ7TfH$Qn!X3JN?yKy6%%6p?QDO29eNccZ5o1$%;xWNUfzZ&=G^z+w zU8I@S@JWQNp}&{{riV{$&ylaQm=roOnhCtco9l__7gMzj(qIEKkm2KHrQ4E; z{UmE*t&1rG*DGJ|^I*_)Tk}yil2Yfm73RzSq|Q1q(eD<_$5UFCFE)s#RXq1CvdK*r zsKB+FZpuD`c_C7FS#V8ovQ_)(ybd?l72u^DKyLXxb2` z!Sel`CL{u71>>}PsnW6klZz%yJMF49OymTo*NLRfA%4TgBS$yKf*`5hu4sZMT=f%9 z@%coCSCDUy!F~bxJNq!&-%XhpWeFI$6euOwa7~{lU&ES4t1!~*oYqwpiX2YCd{*sQ z@t@m@$s-9q>KJAj<8__f-C(cEfdn+zclJABzL}L0YTT)F(7uzbb7_Xn1%ESUz(CjT zMc)!II{>WVU8P-JQSu2v~ zRWw%|KGFzoy4aySQA?z5^C=?knL3G-XMXGGTAmDFD&o`tZ$okA8+y_axpeR)9wk6l z>k?(|V+=v@jufh8hP@aKtxO!LYoZk0kDS=h4K{q$?c8|5!&aI%KEpNTb(W3ujT62~Acau3+K#Zwcw!MC`!gEgd{Q=a@jjDYiYxVPv3L{ege~4n9 z8na>?*@*-nv%^D$txLap8Qqxnz>?eL<5rT3vr-iO_?6?n^{*(q2aCQfdHs%S#w)h! zzABHsi`)x2mcwd3{C1j6nvXMB_`IP4g&kDXhEf&kXh+)+&Gc02E}$E_5eNvw(>Cmi zMIvvz0)$%DM}K~PYs^nAy8W?#^pphVZUVFuq!0E)+|=~yw8`A1S&HKFO-V5xZJT|9 z2)$2k>q|xJc3=A18L83Lm``Sd4o9?9KMle^x>ml5uJz;OPxEVCL?<0!K<_J9??QhG z-9txL#jsXR^%d;I@Fxt>NEw~D-ERxeuoKM$g>&5D-79R(4B`Xyf?EZ8NPUiD(L-X8 zXPd)phr7WngC0F()~FjOaujG4o&w%K99iX}qpk|CvOA1b$W6%V6$GJcHzOwbxTh_d zn&`daitc5R@+HR~*jTfF_ksZ*s@}2=N$kd*~yKU zJp6vzf6LL1AJK(PrR(_^`R9=72AmM*vZfc8{5h?bSp%Uo*z zgPblH5zW~CN6&KC{$FnTH)UIJjA2)_%qSU-$c0Xjpl-V90t9dh#|@9BOdN&+)8~s`uFi`URAe!%fd8`~>D5nY7TiOo+!FZ$q4{XwIhI zf9ZyjAsLF($gjBXH1jSas-Cx5^6Vfu&@0St0Z`Yw;$t_QsIxK4KtvE?|HosuqTWY8 z|Ep|g`1xHF$9DE>c%E0-T@gJEs$YI_OM+#l=tV?&I9o9z8S+h7*4eF@e0-UyIj$6* zL+kPyDot1@D$BRceV=V3AB282V;{8EOtB0>S2eX&G+A90+GvVeqqwy*#)@Xz$hST( zN>X)S(|O+Z*Z$hd5KbUtvrpeVwkl4hdagbHik68TG5g_tgbk|6E}!ijh%L9TcxxhR z2pF01_Sr9PYb+q?)=X+5^*mlTG;DEIZ@{{~#+Ji%4ORg^b&hL$9+Q+0 z0>|*BsHi#l;!Cglf`Mn(&1@;iXq`*90mkmML?(3s-e5P#5ME|Odos`2P;)IMvTSKa zmDT83Wh&?mo7aX5FL=MwK-)LB-kW}T&!$5h8kUCtTRHR?g z3Jjb*KLV&^7h99%v@(;u=-uY+w>U0OqWiC~{`n%okLnVe)rChMVdT%ZMUp+g9aCj3 z9eIt;+|T?y?F`}rR_jjR)_<6@TE8VELf z?mH(FX;7y&H{*@e(%ZK4aWm25f}(2n=~pfhjE9>07WpIdP1IV$x?i!03)5zvfZ^IW zt1}gVc?TcmQtAzaxi!M&G%`F%mOG zKKPEF=AzjRx*DQbGd>Z{(zO>{)P>V0KTlf*il)5qt2hy2EE z;ktnSeD*EDOZ1^W(R1U>s!Q>tHfuR+jTyl<~Kcf z*#(V>{NtBxE1R=#W>B#fo#}$)@kTqd&Gt^rXc+%hdc)HyN_hH@-wXnOUI~uB_xhtX zg2YSc^GmNO&9e6HL0UK&ho zeLfN*>=Imd~iVVR3|xB^D{4#E!KirU?A@@eFKFp<^; z3YXTbXc;5)NW$V+B`gHU1(rChZBhTFQgnos2n&6bo0U42cL6d?gNsUb@PFJ z)oXeG$ePXhh~2cRyvb5_eKMMGKB!8Wr!mwH?qf`|x_>ho58}GQd`@6g46x#}A~=XK zyh+l<_g%iN-ZISGvW|=IB&wpOF-G(s(M**zQ7o~gxTIwPCQJz^gmB?rKFN}BFRN(3 z{$FE19DV02mxQTQ$?1jhU->r4VF#^Q10Xmp*DlU@p4XkR?DDJHd=r2H@oo*gZ>9d6 z??gL&%+7}Lq9(Xy?z7}ie-~2+$BFl@m@AuIxdpw%Mglf0T2g!f% z%%+!KF9RL_IvhjyW47QMeOqdjs`DPn;h^Y^Yh-!`?|cAN3;(KaL8%gd`SXAh7bu+3p!B&v^?}t=^q0g#|Wsa#cZPRZjqt-Q}x)~&rwr;D8xxufCMNPhN=V2S*RYt z3{;+7XcYzCqJbiG9NGeAHFHuh(#q)6;wB;D3(eyvujraiBu3w}W@f^4BEp)bW33&3 zNC8`{2?#mKMUC-5ea-oGWiFZLtD7F!QClk;PQzPIw4y5^!PeC7zDO*lGAa|w331BY z$AJy|J>vTzih!$F-padYQtu=TcTxL)79~YZ98XavRE%xmo^Z}^Wn6k0sFjASp~h^c z7);%k1|wIqAqro7s;nRk~w^EhG$MO}Fh^ zeV4La@|!i$7+1X)!v|u?#^kq)wRqj*ESvo;+Lm_Qq!Za```#68K>}k`JDphd)KZM5 zVR~3?Jgqp<^fDg6GkOLe1ecmfUW@rG6?`<^7Jk7eUUk@FtC?RYcgo+a>?2Ejqz4%`YLQ6W$SRU-~zCuVrh z7u^=XLCoTa)0CcAvAEM-GSizDiBz9WikptKNhw#}Ns=#N}2Job(eHLhxxQhnya&wCfSBmBJ9a9R`vr@ZKAE+kkgYgn)(q2+z) zfbg||KE3pU_k`3(LlrKgms{j$h3n}4g(i5W>W@_tnnmAa|zkhlKBU!oMZ`J}m>q^HzcQ`vJi7I(O z)-iHYn7*KYVl8}DqGw|1=>~7AY1FF*)1&8O>M@!M>aJsMbPa?0=5$>Tk`Lfsj?%80 zN?LpYa2C#R!z=W>rBb-|UoN$Ygewj0-n^e#W#LO5D)B2qK@s366O|%hi{PX=aZLRjII-xUx^voOXuO~>>|m%9&-~=9H@2~&K3x^{*nLjB z>6=$3rs^Z80p`d9U~R2(VI&cVhh`?knc}5H6^N+}fq~gUp|yRsoteJCOG)gCH&0x< zE1AFfo=5cDm_+%zk1s#mB^bfh&))VH&QCMREQclw$c}#I zm&2K3ZM389tUi~Y_sD4VznWHtV9Z0^IEw{cEum=LXIiqg#ol4Zt@emuJ1?_LPLS;- zhHW2;KDH_@p*7m2F?40^-vi#W1mqXunzLGa5!s>V8i%u3@&nId-FTUmw*47N27}z) zdF-9wZK7vNlaAeh_*35xefeQ+r-`KE}f+0|IlluI_$Xine`g5$w4|Hh1|O?8-akt?N@ms+RdMt@s$ ztcX9P!;~qF5v@Ig(A&qjuruqw`^=fXjLiO=wdrhD-Tyq^rk9W?R{Fy~=WV(cvwF9a zFCzz-q!X&U;Gb5&m6f{nbE6d=p|u~_VZNFAI$`)0c3o1l6eg!>l!wy?o9=uz^B2t+ z_Zdx%m84ia-9$#>S*f(U9F3Umvj_gGAiu@2-*Y&^#9PJFYiyY{^V$3NF-$z^ zTqm#>H$;+#o1R=^+Fr**E*;f|E-v_%-uZqbRc3C}K)%m{^&~Lv(BlfaApF9WhQOyq zkO;`yPd-?1)B0gkfDuJJPWSo|%xj~2tp?-7c`vi*h#f_@ij#o88poEI0@S?Ue(L(}Dje4{TaqjORsk`5V3?>)mQ zdaA0PT}wu2O)PAF1CvM?%fR|k&mT|75FKJgQ=juCF$!ibme`!F`2nZyc&NCp0rWF* zW%_g`qFNE&+ckM*@!PYX@=(fV(ow4M&N?2w4 zz?F58#zfY@6i*cDScac_hWvuOzRC%wKvb1bk_(IKG>)z;s!0ETm<|i$5*oVG7mw}i zV9?cJi!;Bu5R-O=3;wmc+GDQrq-!2fwW!qj*$#8|iKh=+p4s*2HToBLDH@%c5Su)9 zV%pP$OW3{wk&3e1mX!0%+ETk*XU5vWAykWadn^Oz%iFdCf0 zSPxku#LSgNR-oLS9rtDJ4$}Er%}6)ZT-JANwz)=kP<=sn9f^xv}$gOSwgnj;U?l@u>ndE_P^tO4}%uZAq95VxT7D8x#Hp%=(q;7*5h zFZ0%w0zPp@p^?QJP*khLz_7~Ug!K6=8ExFb7#i|(1nPD9+do45u}fxj1$^e&z4sk# zA=j-B;*Ofv9$0~z#z{C*Zqg97m}8!P5;MvCxGFcjOX0a2n1?a$tPHF})846^!jo7| zRSG*d*L0Y6B<_HjPVU}hY6fgFCyI=g6Nw`^itdXVS9=ukAF$1HpCYqFru`ul9}mVW z;0viE6-LFttcC;qup#g{6lqC@1H(X9H{xW{5+D#J44Jtmk+2O%IhM-+`)ViSNg)T! zZbs#~uHBy8UH_TPKsFmka)x7ywE)P1cSOF>lqFRJ6fu3}bl{qtFn+{YGSheBTUgT# zbDv~al8=VbA@xw0Nno|hxb zPPo$a`0UTkTsmg4Zp71Ah8!8$l}15YPvF`VmyOkCGp+ zofoX`x#Za99=)i0zU0WvYcWyIMXnLB6*=O()w9s=g!TSQmC8n$t7z=S&^!XgU7ED0 z8|GsKU}`GJhk#DM!+-Ff!weGsYCE%+sRHgBoPzzYC40Ahv-yrx-UKmQrt^RE1?mh&WZfXk8F zuNmd+vLu2{YtiCFP~*mDzcKqB3~Y`)BnN8Y{u?ZhsWUkRDRnt!Cz?+jEeS%_U^Mvn z1-sxN56R8_5BW0l5asg3*oz@{m||KA5dTDRp;=0hh)xS7+aR8x+6MunE31aDn~vq>-9gvm_sQx?0om?>%}7j7W~j$70#VPu4hbTj-D& z!ixfXV=Nkz(eBJx@_rVDKsX_BRRp=R3&d{w@!c66d1nC)qeUIFxab{uv}yvm)1%<} zK_KGg>+dzKyEK!6VQJUbh16260QFL@SQ| ze9p+=&SdGZV&oI)s`1xozJTh%9|;^gkz4i1m6tcidRDJ^*Pb>1aCDi*%Lb+&5Gmoe zbc<5_)14VRSNPOzTHKeJANTvNp?vAJ=Qlf?T4)`(=_x6Sf!48%#mG;92g0!*zV(5HQ zLAwXrnPt)}w|M+*Nk2clBQpZnSaq#TrvFf5rAZqXPd=S+0}0IuX|I1z=DVjaS!eQU zqIuWQWk1<4cI`lk91}|Wk8H{wZARm|Bu5YY&lYFYd4RWwEgQ@^?mEK;N8Fjm-RH_Ve$teD~-F zmTW}&K^IijEX_W#dBY|B{qgwt?yGO~qbwI=%AfP%K*?JkQYyGZ$gX2%`2}?DRhY}4 z|4gu>Mi8#k6^R)49U5^4Mz4r=@oSm(2}hGMOJ=|NW&x0ur7`9k05UT&YM8Ts=sTG2 z|KVmoSGVVP;{aDO*#AFPbt2a|2`>__KYWc+S2yrCLk#Wc3e$CXed$5*;e&^H90a~e zXBQ|IeP+bW%*Ohy*=mu0-c9@TnLP|HZd#*XJ!g^cE6h_pr{UK-p#Y+4M z)zq-XipJRG-$L{0AV!N%Eos>QaEGB6L7&R@&CVWa#Zcj?qy0DazV`?DAa2K*8+^`u zs6`RtW30$4mLxi$>y!Tv+f!u%D(Rv- zL;X`C?Su9jU>VSi6bM=ZYJzG&La(_Rr@~uFqS5Iha!&7~qytngDnt48Sp-CxQu&{2 zwIs^Pr7p9$s4PiNu|;RBXIC$~?YU?)fv}1k(=L5gj^w+IwK{ymaFGSxb)|)+BDafD z?Cc#Z+Sd+vr1XQf&oVjmVllAWn&mD!w{*0M?QR0Xo%9<$Ksi$uc@h?m+~F!g)b_l4 z74%%|ii4h_-$06^z|nV9c{LI_M2Cw)Gf>a4kx;3I6ZMuGU@;EYGA7V62>e$ItSEHu zijElAaEOMG`a@<#G)i6Dy#0$jA~hD)E~$L~f<(fOyOywb~NY+F^%rddcHL@#*@}` z=ApMtj=B3QzsO_sxseUKJ}chN!%c>^^xlJ~wk41)jV)MvU|xRh#+F%WA11{satm4- zr>G@GZARt?Ww&-b<|Y30?u!NjSe{|Gf8)8g$tGZxr6l$P+3-TB5pnR-#GFd<3}|^U zb|}VN<2VT#q@Elii~M`>u=f_$aY{#9GCQmC`Oo;NtRJz`@9m8ESt~VbPIGWJR~kOM zxdm-Pty_;QGt!N5_VZv#-o)JWptpl9sWZ;PfY%)W*}6p z#vi1pNF)*ciA6{{LPAr4Csl2eQ4g~OqMH3W9&U+x@06Xq!Fy(BT9>x#*5K^UX4B}} zKljYeQ73)rGcNmIrba$x-?>K)medWR-Tt|KPeT3 z=?B6U7ip{en{6u6*G~yR$^k))5q(AE+-TG!Ef5lHP^w7Dscy-B-rbbepo5#LL z7km^wBzEXD_yDzfsA@<|`u|TigvR4$bPWS(9V{))9&x>B>CyjZT7xyl4XZznfUVw* z(U5nSfF2T?9gakDN4o!CD-^!ouEx{Qgrh}4SJEx7@Lxi=ETW#o|114c%dJ+~=UB@9 zbE=2`<|w@{o=BuOt&`ZVk5YB|F(d#DNLPIu^EGSzN&mFII#zfm#v`tfzW=l;<885d zPF-x@E@W^tyD0GAtcqhT6GZmvs_;!LBH_!}eJskCvabUpkZQte;W)muW27_NKGKPB zdS_$b$fQOJ1%z&3jnb*?>JTPDz;6^foRnfwR_f`qdz$5&S=T*MQ-j;+ReoFfpsGZj zBxa*ZC|--`sVL6^<5EGTEIoU1=xP24k0F;D5~8Q7vEXg+N3!YUqH8BbVupYCw$G>A zx82y}M1vdNiAn*)7U}Q)2LCmT>Yt9+{%x&AnLZ2pGpI_kY8nkOo?||ky&m|-#3%mZ zbwNL9524SDy&qUN|AI(rL4lVv@yluGC2Dfq(dEskqT05BLw$tjZpqD?Y7gxyF1TwI zjA$vqOw@D?qbA{ybJI2BESii&U@W=|wz(aR_2rHD@I6MMQ9SliQpld^@*4U;p7V%Qcr z7h>i35#Ls~SAfw7dysshqx-||>KjC@-uMP*0186%1iRE^FHK9hl&;-*^}@rKre!G- zUUSEy{P{z!>V#GG^u@*fD;k2`qtgyFt1(P5$F7xv8*WrNu#=f=Ci^7T+w`SHciKt> z(XMGX*q2=_X#jI$v6J`A-#rA#c{yqf>|YRD6vuFQBPtDaq|SY-EIXzd)deL$Kc$#Y zuk-4pU@Edrr;cQel!W##L?f=eKfGywf@pGkXqq&S-$4-#I%kY?MEgHETyTV8^hAg0 z455R-0@YuVZz|kV7|{v3hf-rnQc`g(;khbW0HsgN_{N}*IF%g(C=y2_T9g46#ah8` zp0DsbQZ9T*U9McWzoX5SWYoU1wHpF8EV2NM4`4^oV^rFMwqX;0mJNz#KJ&e2|aeolyuQO8Z0)e>V`tT|Q z10e?-f02&zS$T+GyQ@JCH#sfqri48+vTZ$};Q?H|rE^uo%#DMI(f8aLQ&KPQzjYcM z;K1Xsp5tw}O^9&GM$pBjt^$*q_=qR&()TLg?RTvDWhxa}@nf2+OOO4}jf$K8CM$Rd zOL_3Hcr4j^Mp!XqEh+&#M^4A1XAL&Ww9mbmfwd65z`O7R-E{JA#T6pkpAsu%lF}t3 zSEjZ#s&S=T1-F_^XmLE$+eW{K-;4tsO27z&SV6#H2;Q=YqmpmQKGCwHbK1Q7W=@`z zbP|rPG#$3NYoSB0|J2G?hK&dhFcP76@q(OnB`OLK9RQAonzeOhbn(h&kE7MmXvy+$ z^T7ADv=(lS`YZM?E3@~X{L&K3ZjQK9;5SKCiEN?=;M>i~lsRFK-cvs;IshrGB$%;*GNOVo^TmgO3l`_@1`i07G2PWKs5_6x? zQ@8=LxB-r<;`9+pi6z_4Ahn`-2-k=kaP~lI*=2@qsx;&%8uu=lubMg}6~}C%cS`NR zvd>f0#E1BykQQ+29o2}}G+sRhn_l|rK2^E{|eqi;=Coa`-c@6WN z_pWo&e|h_clONk}*@?)3XVwF;(tPALtK^%V4#)W&9D5RYj2|P^dGII}scIA0P{3Bw zk|!{tfCwW##G6TV5>M8zcU7`cupYmx*St$N1;Ld!UNxzgooON$!5r6Rq{_{L72B6tieh5_#R^+;M zgN(-=4sAbr+2&MFZ^Ck8H6C@10e$$Z`-gE?lVL;ex?pljj^_ZQhhkIC4U}Iq15KW2a2H}m`t zoT6C%Szmni(iEz+fcaA<{SfD{7GZZPV-N6W&@T=A7^SHy34jLb(e)sv$M?PndlIFW~ob$&0B>pLu9E5+06(@wbUo#N^Z8J{NFWj5*1KXY44)BpLZV zW;D-g3DU^!KtW$0Edv3~MEhN0-n;L#XSB0j(J&@OaV{Yj6&%SUL{01DV-NEuB?pt% zh%fL3s7}GH#J!xjRh%%ekEoR&t3oJ(uefL%YvFS96E_tCSBhMIS4S-DN>5!0Y(tD{ z2BOJ1=dWi)q+zAT_dnAd1QBb}!w;2wRK{EU(OYvOzLyFcZp~ghIQG2Y%^tA^up3p? zAEg;d(z*_Ta6F#r4qmV^fnmdOg=v*vy%961x_RWdzkz9 zMXLWO(OT%%qLZHpB91Jk9d}$uLm?nj)heGsAYHHwgmpj~4zqZ$HF3?G8*{Wvb(zf~(p!aI0`-FsXEoxIv2KF?QA9-avrE#t!@=VKo z=7 zg#K6A=Xp8(vLr0E6BmnbZqg9$W_B}vGN0~T%7ZM)-= zBLDsRMx-CT$d+mNm;Gfyu4)Z?6OK`N;NGqC_ZE5^nukxlXv1xMV_<7>`P6+2 zPmK<>0LCN?)7oKb!jwJf(`*(>FSr%e+WU|98LF zs7d^CDKH=Zf|rVI9x;G(;tl&R-Jsd(k-H{{jL8`5l5Ws|c4z^wG_J|M1HdrSukVKp z`+?A}ab$kzZosf>8f+HJY7!dub=2q#K7?BX5qH!iuIp6#Akx)C!~DmX>cSG6H^o%= zF|zA-%}&Sjf7J7aos@&___12F<)tL9i^grelmYiaCK2oEFy`xzPY81Pq{}QV^9jR^tw=YGkMTJfc!|Aib*Nysl z+W(d4Q{038?#Ws()~}%N#Y2BSbx+eh28DeQL?IdPi%wlK$sxFesLxdk z+i<%E2l$uW!Tnb?@Tl8`@V<_@U-r(;cG}SOW!D=CWBEml|E??SUUOxnvLM~$sd8(I z%}fFA^}0q0G9=j)eDf8d`+!>%63H|=Z+rtqjL4EO$-2o)z)%g* za#fv4A}%stQQ!hnJtRcGmUn$dE3&c=`Fa zPF~*@#b7vQR{l*5YbUbB_{YS7BTNOdTC@H3Ha!~{hN^OF*OJv+X17O7XZ^zSPoKAc z25MnW8weXkxD~Il!>~iTC)h9S4L~K-VQ^@-RpY-_O+#K_A`k}y1#-ge(_U-)2O{<0 zar;NtF8c`*ebKHYd+5|U^Y6KMGc<@7NP-GemY%Y8oC>SnGJW#e4kClZDnB}w447%i z|Bno}Sc?1Z%|O`!`$JX1?=Y{=m^fVD)QJ=erp{egBgOF6$5@FBFr=4ZzSb{O z^Ic^0u+T7Ow7;MKICNep6s5W%x*XUlA#KA+%rg}BB&s4^+#_+)!bt|Hs^$bGn-Mq? zx>rPTXkrCbpnzW~asr>Es->!LAvH0HLpS@HnUr8V(IhqZwv} z7p+OCNVGF4%-ZHUk>aBxhY#h@8~DU&k7|W@LLMa93TcK~h@d$C5Hk6Y$tVB(7#sDJ zw|(&(9W8U=wpcL7-!|C6d7}N{QFb2&wt31)AGsDL4p-8A{fqnM)LBpMz(+DGeaolP z5X_L(`=&zrp$bNFV)lYndUV+(juGf~zAC?=Dx3hdP~-%Kf!xJpCGIJ*vuRs`0&ei@ zLp{5||L#TP8Fc8~SwDeR)+4j3tO5BpT zX?f9zeZg;Mqc+US#fx2K9bg^#m;}QnoOD{Iw)%WmI^V2c{kIO;gpI0N4xA@*E%SHi zO-yRac66r{El9!w0n-?nzGgrmVx6ysBg2)qd z6(eZOnBo70n9Y|{$Mg%5%K)UYh{+*ZWFr}BpcCZSNc z7eBJ> z?4d9Q%c;t~#d}98Uu^aiC+(u!Gb0_;{)WnAEs{rDWdbRbg^A^F+7ZAQ#HDu2{eu8Mt~{BG z!p5SzEDWlogi@>dkD-FEOWAeGl`8-qpN(jeQg)1&02-Vy4HT9xY98F~u^Y(Bl6HRZ z5(4{$l{Iza4eOPe8cR8be!;Ax7gyi1SkDLE@Y~v*NX%lHCPmldEexNeDN%jJ+j}jq z7Q^@mF>PUgCuPp+DxyU zo^4Gwfz(}&_+S6!)511}HQoP~&Bt2Pci;b3HK|G$HJY+x`{jDjd*08#?7qf?4opss zL_D-@><6$@9sokY0^$F|$FJ7>2x9~sMF%%HP{K8;FR^+;cw;{qGH6T1e~>&i19loa zw{7PXrQ6*2qC znt^0kCt>ZLzdl^#u4*mGL=vjra_ZKGu8m92oaB`#A7}voCc&n3XQw|e?O6+@N3!Eu zHc2{zy_F_u2=AB@%0yAJ@geHKiIn2%8ENt=uT;KfgniUE1H#);78bkMw5ufc)|hMg za*hGUMMnXm6j1P=;s!rO-TiY^FCHSsBn}n{W@y84N0l1A}Ecdxs)Y$w#n*ZSEv?n@hRUS-x0HKKRju>WBP zQdEjhxnUDsL{&BPz7EorKE@vZj?;jL1kMy;qsT*nV4vRxj%qr zcaMAz4UZUafTHzK4~U25GMo#03wWw}z+tsZp*Pjn>`dvC-(fk7%^qILE2{`^eib%PBm6>4Qt{QIsd$NYPau}ohZiq{=+t3D`w7?&#RCKdhEYZ|&vTDEi0 zFx9(pC8e-p$8l3Hhm1SnR?$-n9MMboUtb$%n-GI1HwC-IEvg2&$YiVDD2mG4-s*M7 zM&(g3JK|x^roMcRZoTtpi2jw;b9X(o9Yd}Ue&E)fB1ErcV{W0-u{1r=H`$gVj)RDO zn{9Cle&Y=GjC~O?z;A}ms_`abw^Sks!joSCn-F(}Vw|Lxh;v^PjRNHB!~{CFfvZke z4WB{i;<#>abVgUgPrqQ}_A6x{70$TJ8u}Sg;p_0G(fsmy1tV>2^S3SbztL!v6!n>Btv||lk zw;OcKY@bul(!1!Ld8l+WFvLy3ZwP0Y=*O$RJ!(x?lXvgq(2l#6!qobwo%>oKUU0*? z$ES;e;7$7X?8_~&c)2MRYvGL)=sLQeH}BkET@I8uH`)WmqWS2(W;p;#vq_%vVH$p#KZK8vCxM8YM&4Swe2HF>VWa53f?{ z^q9f>wFJwWmaSFJYfBl?u#5sHHremvGGebh^kV3AsguvabI>7$IYMHF;cK5ng9$-W z<&9tFUaxjveIdY~sV&5RfYwp@*n}5j<1f16MLt6_Ym2<==pqv;Ez%x;=@A&pSigoD zj=Fe>*8f=OQrPsX5Z#{zZ4Ds-kv9q%C~gpmeH7;8ISC+gd|$Ic6v8xDX<6GLcQU#c zKe;`EU@kChdZk>cX#=MA!T%3}k*(7!=k#jN)VCN**ai5Uptbt}+T!6{^P_JaDnunr ztIEjQx&^`JR2d`QbU(df;~8xG(T!vzmNJgr3Gx2dLl;R?_&W4X0lz>P6p-SQ08tUE zq{`o#CL`JSi@@-@5&LoDhvu$o+!m9}RFHi2y5fx7U>62JH_8~^5w3hONMhOuMtxcw zt%Qz}h6O*3vV}bKc<2<^%1eOs1Sb(50~eo9Rt0WO0?__MGb;b0HprpDoHe``c-H}1wR#4MDaVPq?G*s5mB{M_39$fU;QYa-bs%DKN$TYF)? z8ZXhhKi`$$-Kb2EBQX`KWx*V8*3u?BY^0H)n!A1G`X+z4=iEEtM8bybukY>K0jj5@ zsMuF&daehUIkcx$d?)67eNcc>U6@t#M*-E+A zN-H9ag{YPmKp1aRW_Yqk8FdX z*$Ds?2Fb{7o1-RPUWEQTJMXj0S+J3=BHs8 zjl?|IzLJ_;h5`<>O4{&tS?HJGr6qhh)n79Bchwaus363q`%c77)J!(ehbofW8$Baj zPE@*6(g|4$h`_u%j?`levBfm2jGd6dm~6CTq>bRS2e6jyd;zd6kb(2-^mDls=!-= z)&m!nhbbY75?IVu?x%bx{)qpu5Vk7^;vBGJmk+OR-IS4RyO8?i;l^nzI+$#yr?PEU z<)WzD6h>QV#$_)!kt{M3ha0J9iKSQgfw<@B|1&b4%Ttmvx%>KT1oLpN%`m%+Wg)nY zzR~KLd>C?nrjYwi zyYg=M({?zeuyEF~J{531wRoFrBIbGS;`DkZTggH1D)W&@xB!^DH&6}efniCwnWq(z zd)HZdE_KwR;wX&(qXk6%-IrxNhozW&&}I$@Ks^|bgtkIoEWmdK@)wZ38rFseC2=g( zJ!0GgZ4|+3*b>wt{N}bUd7p3|_=)TwDl+s<$10$>1`g4y%#AfwZh12sSFFmH^!dOs zpV8EW2n$PMs!io<#Q;uvR7o_j7vA*lY5#|T%d-P=VF1}i*jWCX-k+kuPR9_kD;x_cw(_<-qIx2=|f$mfMv6tA{G4fj7Cy z_nQxDb5dO_T8b+)oNuQ!-)smh*E1`hZuWqD7fYs5SI z4Qn22HoRFb;0-C)++0R(L5ucho01#rMnwO%fy%BoL7T%l->+L0(X_mMk+ZdKRLtJK zhwXTi^bzv-eL^0OfCu*B1_T%kQx(KjIYk$kI-$k+j!R;iiJ9l+H_a~6iujE)vdqYj zEJ%B%-H==~X*9cC3d^Y}H#9)e6ysk=vOBFr#7G54_PhzgunoB4d|i+b(%VQ!Z5dt8 z+U_GpyQf%))Tz1;1!b}`Fxf+8(d4(^JgIPBh);II4j(`@)9&hh5%PM1Bt+JuhH-}+ z%JH%g6@WF{0hvmviDFu4g2nf+sbF}JtdVN6RMlF1&3_(Lc(E=WpC;*8o{!KG#N*QO zt8>7-Lx@LqT{5XBb+l@$S@3(oM>-@3e>P#-CXfk?0VkFY+YOF+M|i7&%Y-1#vEq{R z2!CC9Sy{`BOn^Q%#-?;hreY%l7KtH;D^*@*h92|b;PkU;a^C)MD`mG{Wx^4 z)W}D{fn|z9Dt?)a1fZ#sbVa5c_p_?Bj=F~YbK%NPgMBs|ge!9;-)=_QjVH2$>^*gl zMkbI0jh?vnZQyZJZu@pIARFA`9JMKphR?NZhkj=*Dq?S=F$2Re@?1q=Lgxd|5bbCL-kkwLTkV3IHfYZ zu2jU@g;M$Lac>&m;6%+sI+*rIK5Ij$M45sAP5ZR`V_SP8mGj)|DcCnwFHax0rxR?P z?morLuR=}tc8XV^SF8omI|?(SkbsiOEzOBO%-u#%9|dOZ2qX_|xoG*eo;e;*y=P4^ zkKk&=9^|Qk3wQL}52ekXr5m=-Zfryc|7C}!EQsnUR`QlCTD!Q5n&7~nFVu8*o+{pk zkMsGdWPIG=r^r3!f#~T{{kQNo21V+7;NOn*_cOWvX;o*xG&qYLh$PbvDhqR`)FJ>q zo2TvuRzmdB-r!hgf7wXi?5d|fY4U|^JDNltq&ZGn7VN790A_62J=3zS$eB;hqbv#! zeKmBsl;LyGBiBHgDr$1TwIa7P7LYbL>gz*Y46(^O7hgA{fB)_+E7o}(aclAW&YQb* z#howCX>9y$&-9)}Q+DYDX^K*>7Xuu{E&ioZi*5*8uBw|4nJe zo^i5yKq7m*7zOxI3Y8SrK*zczT}A&}dKTnG^qD{upe9}aw*OA&Wi zfN3VMz2FZXjXNAYI51B&>QIk4One)Bt2YQw{PLMv2x5b5b}q(o8nK|%mz~xq0uaCn zK>&ii<>PN0r)Uvx<2}HGuZAR)(Sf!#IAbH4o;0|4dSf*pAyYg8d{ujsvG8@`7HHE? zp^dV79#i?*l!GSQo3I1M;|f(eP%2)W4#F>#`$Fi74hJVgKqJiquLgPNWfqA=I@tTn zRMg5QqLtqp)|I<(LUt@Zu;t?38|i?)-~M>CkPkEVTiKqEsin~4ky8MGrG&wq+{n+hT%63NUD`pRc=vi zk2uRx^fw4mo{=DSL2ySg_>Hw_CDE@Q*FbLof;h!v{A}WFk!S$jYGH!J(b$K}dc-=# zyVw#3`T5t2Z%m&M1VQfkvg$Qpvsb=Bs3UbWByv8Q3dLmruRjrbC*o1_kV{1O;4q!I zL>O&kCzAVwPSo*BDG7^E7~!H}m)?T+``U*}u5xj-q-%+06JQ5n2&KzY@o*+c?GMgK z*iQFslK>Sb`}tH*NZkv|GPe6=)r z&!p0_r3BZMhoZV=C*cKKk#^k3Wx{rdIC3^cmGm zrTOxtuVH|RW0U+A`+OPeK$@+#&m7fu)b=^?`{Va~QFYI+Tvh$|8?V{>_mOZGTQ}LE zqf~gu_^R-HK7kID4L{M2QEGWY@(d0mv&x=1cKa6}L!1jQyeM|uw(nCvbUcCL>P$75 zIY4z4jLof`4K>#RUgU(QLttVE>aZnk&ZEaX^Y&Dr`VRX9k|ifP2$cH)aSo5c-kb59 z4}kU!@My%7gi~BS=c9-3)gN^NDzL6L=iJ)SD}oZ+tB+qekw-ouEB{qL^dk#v1VJiC zEQR$PM~#UzAhfIm$}LauaKE-Nf?ye;$ebX^x;E#&6A(lLoeqh$@qSr9(Fs&31JL!E z1LH(GN~2EEeSjyOh(;qw_==~~Qd(-?mrt-{Hr(c%b^>}~b?33QRnm=4ARX4KTzwqr zKBEZA$?A1g5`Odq6a?9ToxMtWo;`!yQE0fQ8j!7uWYpi2c!n&|%6*V<6z#tu9*5l3 zwHPWwcQ8zk>O^VoWaP#He7BkfB@Pfj^M}d6l(dLr{n0S>AaJShjE9EC-X*(%Zraz~kW8)%r`0m1o3lz|gtk(sjxHwWsdu+8IG>g6a2NJe#@6 zc63wd5hc}YoKwB%pJq`l9sDR0_tN+D_J>(I)v!_s=3M~OP+%P3|7ZAsNa8v|C2j${1zN71IDqOGCE4nzq09x|W5TKPle zPso6yc{=JuRrLVyKm}V!k*@bFN-*9SRT{f(=Ih}SSD#y4Qc%mLO3YIAFW1@Rhmw}I za4NhMN8h}Ts*1mVCZh!j)yxx=#snUP9s;6L&EBGDl3wq zUpba()mQ7Q`FG`e^>2TVUfml1ve2@zyQTNCWrY8SHIQ>K(QpBjrJxE0C0d?H_eB^? zl!geuGv5S_8?BrtyPDgmvj1Sc9BnUoRL&)^<7nkcALxg2PyegLbM-T6MC>S3788HP zQ>at$IN}5Bc)~MbCL`gW=+AR%1}Y`U(^UD6Y$EW5vGnr9bEw=_Th0eM{Dg_4OWA7U zvOP0d5vevQea}AT<^oh00QKaZqK7VxvE3djE>v~Zzi5C{l3GYNZn zP})}08>qLPxSI+0r>;v!*ohQaISh!y@l6ehJqe4WMMQrP*Qsys=GskC#I?F*!ey!? z`p#@f7`hTkT57|h9h>h&XGwDVp(+LLt?pD~KdN1(Bki^8wU)0u?oRz)(|NXF)c5Nq z0SKz+6X=4h9^%9^45~tVuNt>2vlS*yp(Gjc2bVIj7A#+e@|^VgMK zwiV5&k=K`3>?31`c^tA8CCo>~(gd-|zCQ7QPjrAn?y*iEr z3~umJRusd;tcI)t1I25KDsiqGu{c$~OTZuLmTuvPLmuL)@SP{Dpek$QKK=Vz{-fLl zRyV#T=>0&uhhG&gVNIDM%aR$46%X!!EOIboHD$49qsd4r$W_(>msf)=6i*mzQ8ejf zg6!FG7#b-n;0eUTQRnnI<~L*D7jQCyBqB(aKQRsCk2H_zehTIY)_iE}zmQM35_QLA z!a@eqlcomnt!Q2lkbNUa;G(~n=J&uUg~kx2OSLdL3Lz=8OOAFG?@2>~$Q$0$lSTL` zH+RHOc(eNs#{snDeu}#b$|%K6j^yIbk(Z>n)2w8;oA2if&3G0XwJ_!q-7TAmr^lbOC{J>vPIxmp+ZiR!ABS8^-gLSL9iZf%OSM8b%=5f40u{)c%IO~uy85@m=hmw_7& zy*;drCl&^%^b!;!qm&dqT+E?b@tMeZp{bcP7yIfrBA;X(zhd92{8V8t@|X>&5#h0X z&-a<*MJD}+rszf3!Yq}FIhDWgcr3@6{ZZp<&P!(H?aj%b>j^1_frR0t{c@%$>N|Hw z(^1LDJ|0F)!nW0ya!Hh6Rlby_a4G0_&e&Jc!}JNN$wok|cKwd!OC^Tu&qV|XRZGHX zC1nXFU?2Uns#-D22n@irnS4`#k%h5r<-OT=EHG8aGn`ZQ0z1kM7tmpbfSXZBs6{}^ zCkQa-X1-;>30pa9<&?^@EXu=VsRW4)q~aIHUY5G~*91K+Ouax%k7x?qf?-NWpbTVK zK}*I_78J8kQC@(<4ysuk#P(zOc94Df{l??W|N3Vu|MhYG1;#%9Y^BIC`pwM@uwN=E zXj@iJXa9pWl@;~~Z_Bgq7?-GQ#6HhF)jZeeG_KGV1(^uT6-u({Wr6_hOqK7X^We}I zV_8=%m4Borxu10eEx$JQci^esMG+}fMc}J}>b%IRWmIMhh$z^?eZwNNL|G?%p@o)4 zd7*k7Vl^gR0U(Sudn$iDqcz>_jIgdonpCcr18p|H;pDris^E#WpbTD9k%)of*4d;6Sn+p2~h#qtgZU(*7~E=w%J(v!c9*8cz2G&52J zfUuMd^Nq0#(1+udm$~$xN|2LVTVlC}wNaxx%$~A}g^mq*BULch45ikLrZS1}O*fd8 zo|NpQ_0BdnV`!3$SyfhcyUW}XBw-W7Kr@u@M7L+dS-WQcUxzuV=kv$ltBAPVQN`h- zmWO6PK)PYX!Fa&%jtpibERIK`nP|51RS8By!>Wsu7k0~)|4}uo8y)zUDlNwfu&Tn{(J!d%scFW?&+`3@t*VziHytuIyKei2 zQQZwv3zjdL>Ks-b)o>NeLNHL|<{S1$H0{Tz><9x0J4aNvx_6=-7!{O(#XlaV291Vh@QF<(yYZi1@{sKIt=S!6z!l035Xdyj4ZLqzeE!w-F z4Q(=t)ZNV)=F!0Dz<3%>PVF~gf}rC@joK#{zi6ZYRBhnd;m`If0otFW-G`Q!2AVbj z>)en0L1JNG?&!!wK4eEVO+_49ckCBX-3yhTK({{AzNp77-8Ay$mWCjWDqly;GnqH` z6X^k9w4$O`z*kB2>(VW?)US#(el*ZeF`kp7cGH&A&l-%S(TzI1de`o@#$Efjjw*I+ z_T~HLXc}5WSt6I@Pfgu7Y4$beZ2^F{9z@FX_gu4Z<83F8W=zZNz4W24Tq)V8z>v{V z6ht5|krxF<)2I{%R_J2n6r0icCi=!gZwNYB<*gbbS+zY4ydi|X;8(C3-&0N{N~J`q zTv<){lODt&fEcUzll=|9O7HGi84&1RY|n7GE88}8O8caywpq^b$;E~DtR@7O`qkMrpOp$k<~RPwQx(InEPCDOFiXsPl;bE`PO@1 zi6&!PMsyWuLX(5f+(e)!udt43*n-BsiOgWCQB?q<dv!|EapTn(;y6Se?|>RQ&}#JE>H4iQ4>7?qzUaIF%+wVK}1K4nUKBr*Fs0e&U3 zz)mIsel>E>++B2#bfEmpfHp#>pnO<+npI5 zY)|yaV7c5|f;NI2e0=PF=;wb!Tys=lph7Jli{3__XSKXK$AI$&(MYa@zwNvLsFe)zAyMMN7=x)cr`q6Fk8d^K1Pw!~z zH&^dBe_K0^sSmDQ^(2A1;hLWNhj(c3B+5r7#;68b=L#*;{rphMn7<6qZ#;gV@y zx(=g?%);8t?=E8Q$ntb{;SxDaJ+!QE$$~R3okIa9_N3vX5m)MVptz{rpQSox%@4FF zy40i=?E@W2DG^a!=kCRJU^ZSdSPt6Tui1pPFk~z2hZ_sqdD9__i8=v743Ul zXfIW7Xz@f$h0i=;<(NS8e@PFVFdcYI9p_eQU3W)blcQdFu)Ve|{?Cvn+GS2@@0k8j zovr5&>zzrJ*?y}szRS*kl3eiRI;KwTNY;6th0YlJuyiYbO5jyeT?x@eqx^tS?{xhA zqP)J2H6w%*F$Yy_9few{qGW)#@CtQbOM3t{bJhi$bv?I!q`P&~{vdtH4K1Fg-1yYg z^q~XYL(|u!kfQvx=I9QP#?V~uMbKNh=~?&VCNi9RaCg0Hm?nwS{C3c;Eo`^;Ya_6CstB$$BZ$ z5AIA2AJ?Bu2Pae~;2Fq&2dyy2 zgG^*dYZBz5R~bA{t;%PtM5{79LTz|kZ?wNTHN7!XCod9X$Q&Mm9SFMAsIiDyFuPUW@`M)>d?T*A_b+(U;w5#?2L%u*$tPLHx%q z$G!HdNadq#z*5$HN9i7%wtkBG=d`c8H*B(@WwpicLJy^>uYInJMoi;KzEl!Qg@@h} z`VIVi6aEfjndktdmABB!$9v8xKuS5YqY|aYQRj5FwV-F%$YRd?e^-8>i#~J*9S>`t zesrTbfm7I(L+0cpxKD0yT9pji(j7!lY%Lb2^G{(d%`jj~KOo>p! zWfsOclmOALrqSS%g0$$w70f`zT~+;j$HXI^Q|F192Nx}NyV9i3ko|`V3N(mQRtc8i z7u1DX>X?(>Hjz8-xoO_AbZYAQsp0Op+&$~ORbY&pYV-u2H)e=0fUl1oQ13o<=e_1P!Sz*IZO z3Q~1I>-;2ZCA)pT%6Q(%--O$tLg9aCbuAtLAirCZ?8%Jys`bh1D zH;8?Tn+{8BB6<@YW(_@>v=5!HVAlyWc|-7~x@eGQr2*o!cufL;AAnP4?eiS3^w{c) zco@BCGHYxgLD4j9FPQqYfO8rGI2O0`Qi2DilP+$80i^NWzXbJ;s9cKDi5MP92Yp9=WqB;MoHL)oJ64hX~b)5vxFRSsWBHNpHB&^E& z$K}ztLo(_oe}Y)Z!K!UkKrJa{6d+awC%rL^@FeiRfyE)EZ5FBmDGH{upEF;Xb!D4{Dm>_C&WW5S6H&6Y~J?TH}@kqLL};Qlk|c zIBQeH0m7&xAeEf*4Hpr60#a#8!O#d&2?j-HSW-^=k9Ngzn_6FEH$4UHf_2bKJ8N2r zaKNgf6B4OvLmw4MlrIv(6;*u1oN@GWkBg*<5~+@*PWr3B`oYgaeevJBgO@Mbvq+8t zcHaoXB(5~7CHx_a2=KvF&n*aF5EcOB6>1THzYlShpTBtLCOhqEOK;#&-oE$@#-Yk0 z@0f3y$p}zr40-dvEog5Q-Tyd<4T;M3R8vHi+tREHxF+Gw?@YC$MX$>Z-ucOK`Gt~^ z>oy3TxA1;nQ&v7)B#nprOSa^{!j}AW=qkkaU>u*g;~yBL$I`0Gc;mzZeZX8*!xQpQ z(3G@1-DhY?QlJ!)AZY-3E=&{>0q{*k7d=5#SKoZ_wMHdbTxVRh7q~=ETrB%VP|^hY z1xN{Gg_N}NCwj_{PMYduR=4Sh?M&O)9bY!$$Gz)0au3Y1V#x?F`~dyU3f)#}Fg+*Z zR{mVIU->1+$~N3Tpa(hbAchkGX8~x;@_{Sf);Uh<(W!dEpHyxO$JZ`6e^XeM^S7L_ z<$|I=sk`K)o}9d+M)eIpS}e#eD>h0=xHe;P5jZ0dr9|sc2DE+`Sw#XP#3SKH%A;n( z*NQ-D@O8sw@2F6$+3F?4UaNl~-5LZRylv~_RaBE8xb@GKzeSSSakHG|&wI}?f{oei zU=0FgAI4?cl@1NIdesg^+6<@=RdYPfzGX(oe`*R)Em6ha=B#h*C;TKSgsx_oCS?1F zpduM|XgVTkRFn1zj6$w^($4^6yVYW|U`X;rv{RWPjJHVgl)9Y;P!-3rkKEbag1cBq(XxIgYEdkyU(lybwIMh5S_^*Em z7nA-3-t#oZeva^=Gq2=^?a@K9bDZ30YPz|-KfbUFqbSpu)M;Bua&KYA>xzpoQ^vbw z@qC}N3j@5H?|->H(%!2=*vOG|<1_1lFojqxhl!_1(PS_eTqW65 zp+VPc+e${`kDaDEr`)?nk*(muM}bokadp5pC1wDTl}BY%nnPMejagY&roBxa`zCK^ z9}BI(4|FuzFjk0jBFvTA7GXR?$q&%ZDKsI%)Pi4xn}TXQY;q_;@@E<$OVqhM;O4wF zYm(`&>)|MxKBGp&(J&P_icXzYfRzK!VzVj*E!yW4U(*A0pPcTwIUP|Az(-qGymyU+ zagBDC9Rig0{GJBR08MT3jkwRC?Rr6?i)25YbuBA~<$^_g333o<;o(-(@Q(cKIDo1?(H9ZhMgn&a30j9-N-LppM{k@bTARES<&f`dl6Wb0o&T4=WUgb`5M-=Hbdu>o#_v z{Kkn*1XV&S2JGnz#$LhTmv4tM0ugZBkFWZ~bjsDBMUE*GzCZdlTqNEy&wje>U`_FB z^jp!|TZmId=T-H`=1~d0=r9<7Vw1RDpI^Ql5OK=zMY6KLyB}7g67SPj+_Z81IgXm} zeJy|b(q(s@cF=?s|G{%L{lGvPwW8McWk|sx^U{=sFC_6@cU=OLnZ;*w>HKwDGJxQR zO^$mMdH#mJp+zMhsQgCjyq>N@T|LO$1*e^($RjZ;uoLbJ&t!F#Fd|jYfl$H5u+J4^ z&p|)GMA0Yscf=hez63_cg#9AoO0T@}ZjqxF!kelBYoZ;$sYy2CP3abRdCxt8S@7e8 zkj|LY$5dx7)0bbjVeL2_7B6|n{)5oX#plk|v^pJ{qxxkR(ESAnI?df6x9^2Ny;WTI z7Jj_}#|;z0=U;a~0t{y`jq}r%u8JdztnyscjNE@==cL(BJ2j_P}Oce)Pf>J zwRm*TLi>-|hC&EnH zvgjmSxD4<`DBg(qB1;+yBa(}x#*8!)gsnRkrP8yIW0sjd*>P>HarQ!SZJRMw{`%=J z?4EPJX?L@!zNXKqfQ30Dk+6y~F00yHJer3jWojW|z-=4n*VKVkTD}#Z| zWyXiG=Q0| zy37C;Q_|de|JU~O;Ea#I?WD8=9(^kGcg&A}1~pvZQJ{HHl|&hu2R0aZmEC;U2XM0@ zMVi>@Uzaf}C{|qE!7_!$67nwXG$|(ma|Hbh*NnZ7UxZqqbSTu=2boD<;8c7L1apMy z9H@?sw?zWKwYg76AA7X5ZMdbS_smKD+NqUG&sx58NcYg_-Mwt@rz@XYuzbkvis5t5 z8GBrM0(GZFJcP_Id@A=62n~Py}$F_X?_VY8DhTOt6S3mwV?Zpn=DK+u?sXl?w z-M~%N2g1&%Dg*kFypQTOTbS+mz+Ho>m)U*NzJ`_#J)G#C9n9}>>`!c34(LpN84}6i zV9TN#rVZ%ZPb<1zn|9?o3OsT0+S_N%7xcRjbzxWYF{=A3ljDPcJGM%45E=y`o?5*& z+#dD?j49mLWH)9Dhc2DJVG@RZOX;BXBlbYQ7et~dBci0;mU%-OM?IT@DGqj&blTW& zq0_!WbcAIg;!D75!FFgeRR`7OiXu&6asZvVsI_{nvC2C#ij|dl1*Jw9XKYzfl;^c* zv63eB>Fv4=s8`ueweqb<_N0*N^inBDIdT|egC2TF+6e47?1y@<>az){jIYR-st6A`);38lcSxcu49) zy%PYX$QK1Qse*+M97;yhP)kpu8bM)gD95{}+s{TaEYiOC*;%ZuatczgDU0(1D(e78 zN3@C)&029gvuuBVuwrAY*)wcwm<`;D@e&!y6np(YWB2mENp02sYFPD6-4dB5xL{~S z7Wp|5XiLDXCx{@qx{&8FjnvqxGjJK4Od0}4!&69or&(!6rJ z6*27sV`bF28!VOynCAAe>-9(pW~`xn>9qB|yG0FxC`PvEs5r9V#1vP2Hx~2su#%tAfIH@dWPU&9`oaE%N<{4&dN7E#0+@!-BY-0X zK?f6mn0@nXRkpH4HDkhZd(3Rm)UbaB8mJHUC5jo=lH)6E))q%TIFV4bO3B{H!fjVK zNXfj4`2;w>caJ@bUcgt$A|gAo8ac#Zdpau6iE5~Zs|<=+Y(v7Opa3gz4{je6?kMBa z=S@jRes21?K;BUKgAqx$s&c|_WRsMjU@2=^2xFECj%)|nS*xGAc1NR`zU0m?&)nW# z1lbCX@kO8U7T{yE7)Az|ABqDj%?fu-I(^Z-LoJ$VPo=UX%wGB%W<-5k)LjrsATx1V zwaY^b!YC&4gQ_m*bx$C-#n{^RQ&xo=i;K?_{B}VoCY{NDM)jR| zxVWoyM)kXVA~D6D{gJr0D(*C;Et0C7T(otakXC1}BWcASMOzMTuNI|yUFkngY~j&D zemseS)}PUxJHfLB8-t-m*(NxBuzKxKg~VR`_0a`bv;d9e=^w>+%POz@(F~h@^w>rG z-VsZgl}g98e2$$OL98a8f~ksi{1|r0%b2x;T4|tFfMH1Ha}r_DZBexTKOadK4E4Qd zU-ja!T}&CbeCu3Uk0d<4_LPInz9KEV;aKGh&%gv^A+)$i&#81E@<}+%j~;%=M^j=K9>!Ie#r0Oc~qgI9<5dxiSR!OJbta+nDcudZ?>~( zbJ+u|Xs4Ru!|%+SLDral2iA1K*xh^#`X`IOt?8MGyeopSVn*)smHarvAd@UOC5f8Z zk5rXRRsH<^&xssnpO58^d^?^%1twaQyh&}qU=65c!eeGG-kRc>5())%Y9?Tzi&68q8}g)qc$b7e zRl*{^Ae08lysA+lqHfZhCeJ%^kL@QcHKobikH`5G=3)X_bHixa)d3=cTT!y9JILdk zvN6n2GNR&+Fh8X5=TXxt&R-N*$0W#ESY~8{3-n%+P&Xow+RfIV74bjx_DDK|HFr+o zH)T?`;>3>pb$EkgMISp*q*;rSK>NxN<7qrIpf!R7-m?I^EqWf|=9v7A@b_Sj(<+2UqEHcOMaUnhhJI)O_&=ix#ywXpQ=uPX zh!YR->%WOGHIJ3nbk6?Nsw-DWS|X`qNEK&*5%$cE#PF1Xb_6C3vzTLD*R0tKL_H^h zDQUp3!dyNz*Mz+whXl`W(goLCdDa>q<5?*4kaO@EH&(uM9|Hcg&KBvNID6Q6n2d~d z;;Sl8D3pLN&=?UW5R|ocrrn;U2M^uvNJiXMX78JI_$@8r8>rWVs~yXx@sEWQ?#5gG zKia+nzRt6}SMRs?{?49~&N7gefIPJ-}vqkPBCg& zl3F?Vj=x`}U&*qeI56-!3-tH>5P0D%c5x8L&a5!9Q!c60W0~a+eiv-!aOAzST)+(_ z7cjkNRs2GDuKkbAvj0CqkLT?yM>8@KC^Q@)64F`Q4s?nj@-zoD zLAwQ=&pKkgt=dMhtNOmtH95)Ds+RUL-PI{fUUI5G`9fur8TX9Dp-W3={^`@Nvd=Lu z{y)sHKZn7$9hVpX_jUF0He-yOEI#Qu$ryf{SM8C9MP7C*qfIY9ayU~z@#dqJAC9ZK z`A^pmj{qcz3Du8Rrr$xj5=;-GH;|^M;#Tg#_Z?ZDs1rapA-CS$)?9j@cOxe|$b6md zEx6NhQ;<1elA2~>bbdR?&SpX_Y%I}0>#O7rF6ztN1gLaFpzo3MQMaLbta7#el+D(p zW&U?cI22(g0FqFs&k&BhvZbxIJG0#3_Y2FvH_&Ti{v;&-y~or$ z@I;j7`y9L&M|Ob#8WR6(7W^Mh>K} zyYA^&)ub|}*Tekkjt5uX^62rV*6v&OROVM*A}~FVUh=+Q^n08t*ugA2Q*nRA33d-^ zlxJez7|a=vH?*uvgd6rezkc-YQ|(tA>`S&s%{{NaW7D4fpT6eNiJG7Ht?b*j{HjFH zy8VAhtF42VExmQknxT`YUd10N~jGF#{GBq#eOE%TvxPns@|J zbdaG0VEU+iEHhF)$Gwo;76pp3Se2Yu8@mO90+~Y9eop-dEWezy152`^HWcO38>o>VlL-KRSfcEVvX-f0Rw(PS)a+l5vI^ zVspsN;k5(nNzWk|C&cOkeVa5;1K|`_i8=B%O-p(vxAKXb?(bQ}c*)Hl+;;zP@zFCc z>A!%+&oG2f-F|c^VmoHdzO^?#7M;4N*^0K)FR`CxDYlqgp<^Q~j0$KPeWhBkX0#*n z@2?ofyvPd7xNQI@-!9Bt90GUH#T6%j>2^)UO%=CR+);54PV*r=L)Opoo$Y>Arx^UZ z-7om`v+mNLL4r^GDuS;S=pXu}diq%v{Bsa>F8Dj=)6eL~xrP3o^OL520{!{; zZ3E=8;)CQ1>7CE)d~@e({X5h6XJ>z!$mxygyBzT>6vJj|_e&en;^Cbbs;X;G;M3oAA%%ApLjn4_@8hpWeB0C;jK_ ze?L?DhVpBvj{M^EkJ$>QBj6sQ3|ysqi4>J~A7~S|RM!ll_!}PLpgBR3%Av|H(l24< zfcig-Sa9|GR+}nF**TSWhu0Mtzz%?MNn;{Dk?PtSGmM(mNz*kqU$MnSf1HU`AG`Ld z#@00v0Na!A_3h|YC5*xdI%f^;>fmvcLmO!4gz_6+VSTnP{0aPP0ThrThfr{B99#u;467*huV(${e*OjI% z*S=?~q#4@tl5BN!doHi3Vtr4WX4qt0*c07FUOjJ|yOzrlm((7HVByQMgsK@e`xxqmijxZuEW z5)gT1xzT}>Lb(DVx9;MR}OCLz__1*mY?i*_jOwB7j7R=Mb1$3hc7-< z71_A18!J_u9@2EQ(?M)1M|%%Xs3-vuMo;tP)wYB6-RnEj7O!IuRE-Y}c9{Kp2C=Oz z7*b;E<#(^(3=I^p9^Jb~g%!BYfN}(p6lisw7Bmn; zf%{W%%@@T;B8oPNV4&uvS7zkmH;gDpTrAwPGP?Ej4*c@;>&!QqXn@fh2ns@`a)zP= zl7>J@22rr>ogj)8; zDtg%#@;&4P2pYPBA_|IC!7VT4HwKprN~opVfaeIO*v~3X%}5i5$~Y?-o-UjCK zgldY}fsq<1&g)YBcz;zd3c5{4_KqpCO790Idt|fsg3SY!OKn#lK6bcWkx{woxMa^L z^q_z`)9XwoxU1z~%+i5Dpu+}3&A~OKMU@~{psj`>`aXlpOz&+;V`w>uS}LZSxkrjP z;lzfWTM|Tw_I1UFdO_yPDIwbtYYw&zZS4fnD(YxZNCul(h5AQ2L1ss%kS<%kKKD zFF_TUl8ycbIGAF<8TBBYTdDSy?o+VmiJNAyxgZZNRdMLMHNDnU^v(d91aAM&g@|yXtrVeSR}Uza@%1~mt!}13aTu`?Oj8}*b>RgWfY{8~iN4htWJPP})D zj7&vAZUIX@21&rBrAfQ*Jv5XQ)nv{7d-pOb>@*XKi3%gVi^8>F^n`I8Ob)?C zz_P&cP`tAjmwtA@fIv~b!UHP zHLJVe;x=5x5W82d>=fYKU)p}xk@b~eLk{QG9=Q8Jv@h;p-4|r9z?>2?qqO9Pv-t>c zF;*}<`aElCbTab>=^kSFy3$+h(kG+u8Zl+i8o^`XUVA=H4>uSGRZ>{;E}_0}C}yjQ zS8cL{zgjVBlY$cA|w zIsQ=P9VAU0jY9AnA*>)2r=bS)n;JTnhAPZa&M}&=xU`lLF~~*6!dCI0Y|*qOB1)o2 zvq#n-HXn}i-tvtbm-(!j!4xmAY1mcSF0|;Vo9`##97@S31kgT$fJgqW)X>{iRn^tofG*YEjA3N< z@=QNIOq*tUFK$N2W}%2MfIxBX`R;!!&zvhiC3L!q4W1gq zZRO{#DycRYSx^EBo%iDxmykix(RDqoX~p{tjAh=i`$7fy&avM#4DaINdx{b>Z*JQi3~s3jAi6{7x~*+x&S`9Me7130D(k&0I{KLjJuEP zF;^QXjSF%)@VG!bIs&|mDIS;emeeI+h$wzh$3*&iyM9?_na4RwEb_>ycuS}WI9*Dr z2{Xb{@ykY917;j%@b;-TFi{pKI`Yd5xKW(gL|(#JB?gj!(415p1|z|Q?`ym}X=xzT zy(5HKTVP0pNz~D4{>4>DupO_JZO8VC-EUcnX!qj=QEWH>p_B?dXOZ|Gd?w2u4$r>57vKVEmG2D^?~XCRCZHypXh3NsvN$1-*SC!~%89t%vTnnUp=>21 zs@9En^sE4Mh|c6qMp|>G9xSKTiA76*5|Yt9`BFcvV+eH*3a4BMpx?ubu(T_pLif84 zbPbGN|7cGvR{fz1TWdG3T{bkfYO$W?Y9Yt=9PB$Wyo6ycZoPD6XYoa*x8=?QOhfC$ zo+etnpX+PTpkMS;qnHj;iDp6>EF5Zo1`ItC%4rn(^H+i|->Jsp5;4I#IqF4yHJW%@ z(GRVwQCKgg)~?)GbLAM8ceY&SFVb)_V?rbios1#P+6bYfI`eKE7*j5?~S9 z&?DCYo{pVkE04bg*?YC;trrvHU3Z z-B29ALvjtUdAolnDi=*m-JczW<4cvW^@++;`K-EFNe#pKu84#HsBBAzP?H4Ci>oof zdf%|TR!Ss^*06l##P&d9w|oe;u96@+{Zr^v0r@je;}kTl!fgO|XW9zge}aIOFMeE! zW1>4L$)p9dw+1Kti9*;*Y!Xg$JSyZmI-fVfYnc-UZa7C& zKYt29{ze5B4?$X_U@>p~Lh)X7R%CdVpQb&lw_vF-pmG*SKVWr{FvDcc0qM_|Ix}G` zxp$wp$(sBjIO}&CSbFq)#j*t%zP3$`xDhkD-rnHIIz^N&j!GsUQ2-b}@;3d*A{|*f zrt5t=h3zBo7*WMhs7T=BfM6t1q4>|V5XjGoZsBph4*9CodQpP9$CC84+E`7VyIwO6cbx^VvuO`%ZgB%_9u7XkPYf^`#JlUldp zgeVzCLM7tb(Se?xx|a4uhD78?SRG_?OKaC`WtbIJYg()BN13|4Z007W&0G{no4inb zh77FgA<<-gdyMXu%#9Z(=q+Bf#{{$)Tqf!rm}@FExSG~qn?Y;|Ez>dvI%NG;DQUebdHvGZGH=K+crg zTe-gr$c+nT5<7vnTpDx0OeC6)G`sVJw<5Kb(Lid&_paWX_q)(v^uY7n9PhexPz~4p zsRZ^y7U{`Fh;CBA4HEU@CstZnpABPJ?`%Cg`6t#hs2wz`q-((aoD!D7(n%4B@&7?+ zlUFChf9c0eH6A)$F)g&EWl8?2g!dmZ3a*MAvQ@?^{-P|oOi7{zFQ}KEsdKly6&VH_ z>$C5wJ$+Y2T21h&?1s=rLxmrgw6n}G0F+hS-T)~PLqKikAN(IDwx2@S8v(py#4;7^ zR=*I9o4;(1H4blR7VWyFYxd?EQjA3sO;6e1(jp)R!Oq6_6E>y=WaTx6 zeb)H%MXKgkiq}mULUfMuE2w+ttL;wCxHCege~J_kJ0 za6M8d`wj8|$Tm%NwS7wz?WLGG`O+u&&n`}kFD8vhj$E7$VWU?y)Z4Y*AuvF&qHZb0 zt1}+2#G=HS<8o}bSeEk((%;hA+qw6A3*@LQ$`hvOO_>b^)cY)kRh#lqWq$ggIp>V}f4E>4y=I_?869=_L( za4L38ZadQTF2!+FtMZD>uE#n_3zZ_;X4G?^p|EGhiDQ@ydn+Cq@nkHcyReRlm z`puHrERpeW&$I$fp)Ai^e8nHMK7YX|sONJBR<}JlC9ZJ$szCjWqQA0IOUI8b9bTN= z$evY~?^|*YpXs>Zk=iq)1Z^wFH`i@Q7bcnu$M3>q&>1?nq2h+OpmXe*;+bv!0&a=Y zd~@01(4qk?t_G7xOQ;C z@W%v}&Pcoa^VcdKC}%baBg<)T8Lhcj3S%3D6{9D2#pM6LDNldjn-+9j=zmGB&p7)Hv2d!dHQb zckwX*U3tL@D_uCmN5iE}YxL4-)JaR+K^L8l!U3uPgP_5usCrZDLJyA%=#qktSUE(P zRg?iNt#@5-Rvx=Z1W09Q^P`5z zlRhEoV$l*4pCJS>FVi@9mbssa&tO;Pp3O$go8JT^mbY*=^VxH>a_PCC+7!ecbhIT* z|Afks$8_2@>mX>{F-TC~6Z}QAWG3%V-cO8BPGvSkF>#dei(fOX5ayxRG(5B;p7JCv z-Mpzb`eB?wDIlJTNV21GRo>FpPlUyt#=NH83Cwu$&hE{tI$JuiQ(AZBsW~=%CRq7g ze<-0IT~n(aI5So`_}2Vsa05!k6)eA&$;eF5z?7PMrLLbq(GE~zE#HlEl8K%FZRoRI z5Y1r8+_ZVp>|!oG8hVEod8o_f1VfXw`aMgRUSm4uoqhl5gOW|g$SBwwV)3XMv7F-n zWKmbaD9Iva|4Z=LP*6i2GjhITF?_UkXM^c$VS%XmRW%o`mRL`}?*8Hi(uUbO%6ujR zYRCXP%PYQUcwAHKOEiv=rtMM{W{q|Sn*UhEXDWV%(WesA36z&QzX0MtNPuBm0_#m< zW|S6!Q9_!}(Vk*JkP|f8gJBjdkjyjmQ-(0WJXrbXm54;)1kg0VI(7GK_3uVfO#}9+ zi?e{Mp)?5PrA+MGl*YA#9c&s#!9xY^JJ|jn~-xyQYE>;pa!_sSIE>bC)7{h-^ zGDcb&ypZZ$zcN$F=$LM+6N|g&=|rseIR%<^v&sM@c54I^Y6-jem*ARyJn7ep^NLikR&A3NSg#?FN-*>7)bbetNy*{rc`K{E)- z=dA95Xn?uNF*#PiNPBU+=NMc#@^&sH8H>R76g!`+Y3jv?=5KMlmQc`B z?9I9e$5cbvh>2AZyM|1NykW%Km7Swi%ElL}4{upHTwmYF`I9Tp;Rcx>aU#bVwutS+ zR9iyNScP!RNOx`R@HeI0G=Hh50^>7cr9o#v3Q!m{K7*RhtNKJX%IxFR=N_RILSqmisdD)u`9#Rb)3t zlu^bRj$i^%nO}w;WiL%NiL=uYFAh|H#hsJA^a5tjh9Wx1ct~MBR{mfDSX8Qc! zIH!)hHosDlKQ#B$l2oF~s{Sk{nOzSzD^iQmSkb}a2bZYpS;}-r;|*Znlw*AHXS|1c zlw}Ew@>@4bM+x2dV-l%iq*?ub4->vC>F#1#_@&+r4Sw1}?!30;tbsy0XJ{cf5 zgfvCV_>+%|K)S%S!dmk@@}4tD80u>Y1_{Zj6aX@i2C^Aqq%EP?Jiid;Tok|Sf>Xtx z(fPsI{>{R!+c0Gn>c%iaxw9uy^77eQHr1rccc5T9j|n~ZV?GhpnGP*&hAnsUJ`8Pt4gXoJ(VEGs7r<&xeVG98YDVM9V^UEsK-gO#P6u(e1$|WCgHf&^vVW@JNx}cb`2Bh?J{knzi^jF zz7qWeQN9|p)FLn=^$Zs_!wsZpSU_r@eD-`rNUk6{1Kv0wO)TL)%1vCctp>|WM*sc0 zWT|-4CXXtg^p(FzIUo({01TzYv%X6{7D?)M1}hVaHFUBIvV*S?qR!W zypuzEMdNgn!DKa+BiQQN$|DOjstfOkD_ zS>GrZPYaC7N5LZ-R}dJ!q{xz|az!jB5Xm<|l3T}MsvXN>*vwMX7<&|imlQxGDU%J0 zmh;TrH?g5ViWx@q_z4|srFTE5b>%xa6bliXcfq^|!5~uDGP+LS=Yts~`juct35s8} zX#E|QEp^W^5sL%L$u(l}&pXZBmI3U+>DA2e<{I=z9L<*i85hl% zKQeGW?I*S=>b`pvI^^`^X3;6uxpfDwvMS{$MqUqX!fvpV^m+XQR+14}+4~IkX85#j zP5xxj@|>B|Dce!-{sD}P_DIXJ3tCec)}nCXr2w98`~=-+7!7vXoGw;|$-V} zm-On$ZJ${aXFih&g)j??r65go0nL4+~{yT?rDeNkFRJ!{Hq`kAnrfG9hOx_$uoMXx02 z7|+B~qX8DCL8_=%L3lhTWI>8ZFLeoF0%1;odqA;U=ETfX4_$mjP1Q5Tdki@V1W=}N zX%*IKeE7`46MZE6P-6AKM5YelhLF?TJGolAtG_Yy4#}{i0AfYugBKn{DdZdx8B+{r zrLf8D-eC|OEZoJvKxm*Sp@BX1;lUMIIoGNPJlSGAf8nMM-^(`y>mYMBM%Fz4E@A&w zTd&0(2FVIwcg*n(v*!wHf_xg7Fkp-}mL*^p>uFMx7ALXSF8MLWEU#3Ls;26~?PkyGZh0l#H=`gOFOrPU{KafnsvXO2} z$a}xkuHRXmBiSrnA)2EzkAG&pp4#~Bm-f`BAXtoJsz?oH5CffY6(O_WDt2K{w$y|ty9d${b)-&=nkso7 z7~*lj)tMG4?b%BIo}3_}~CG)I*-iU;Wu-lA}HGCDd+W z3Oo7Y8TwYE4tSX^vo7@=Y zU*qhM9GUzS&&xJ=G*Jx33d}R84ayP<(stzID}e9@BF*(zxZ#7J98%nfV=GU6!OHXc zKU^|0Aw~WQ^%yl7y8L0w(yDdoZ^>`0$yd(z_#V!Jv3H~%8{)CT+M^AoV1?D{i$KU0 zQ$!7Proc@C!00z+!UJbl+RhO@G1kK?*4U>HTS?1PlG=quZAAI*bOwOGey* z)#kX5YWlat4=?VZXYNTKc62avhI(VkodH)8@f0TTf&r*mCC73sP%eN|?-!IdbMB&} za}b>fF+j?wPlNY^jnpgTJ|(7NsQcUGKckweWx|v11ZE|mUUk`bWgr+7-#lMAlT+Y5 zLZ~nQ#CJh+0r+(7w*VlK!r=Bqv_SWULx{|y0A~pMSkz{UHH(wn>^nkv;;X=Zw~a;( z;hc*<1Qk~d+m-4U(D2f#WU08ZSX;t8QhY@ud8hc2KzfSrVxcG4+uG$NI8=sht@sgQ zx~Vf5~JlgvqwV7u`gNC0d4yrrx$lVa({!2$&InfI4V- zUL?qX%a=G9Vk!Z46j+eCcR)t?e@6g9ALKOU@DqKp`Aep`b33&l4lAHD8Q3UvP)a`- zrUsEnH^@6mx*QbYX$lZTD6}3Q3ZG{=Xz5E~uL(J~eQG~rq;pKh z)s-q;0-3X(ZjZrKI8gwkM1cckEUyg-cJb6seX9 zd(^eB&~2_i+I$2=&mjBtZQ-5cvR7jY=}Ig#`+4GY*p^Yd)V8!j^p>kYVXtOFFYLOL zkkjIWhR*_=@-YBjL|_l3E+(@IMWIZiTYP^w>Lj9(Wve%cV%vd!t8)B{JPCz}H2Jal z&-(A?_p-y=m4gQa5X~0f0Mv(K39-btcy8aCRN5N7elv=RR+JG0@i6DcyPTeVS?E3| zrVOTyyT*@TZX|jyFUHYoJOXGMDHLMmn-FWyn&G5(iCUt&ahGr?1^gl10NkesMl-YN z1O?rcQb=8PLbOVNQxky&Cx(MEm@m2rmf%)`@VpZ>Mtr>J%Xj;)$s5eC$iEc7CveRj zdqbl0*w*)RoUCdw#ESga>@cHnMokU&lU+R@DL$rSJmx>7ln6pGM9tCb-HFH>jHf5> zo=+cULjg?1{iI1>t^W^mrBVD$59u6e+vYfreE?MJv4o&it(6rv1f)QB8*-lnAbdmN zI**_Nhj!`Pd^5Vi663;+`qOS?&-~|lPM<%M12qHLZVQ-FXw+1`oIzF`#HK-m6_DDv zBU7FmPM`#u>;$(km{<&*BYeg5_BnF`dcLI@1h|xTj3t*Um$afbcl@k$ymZTm#DT6> zGZ7N|^6Pu9v!Yl>=4}a-_aD-%a6`ih#Hem%7Xca1zNlN%F1w=M-u2P>PNECJa8zkK zGmbR?GgLgT85s%COra;qPAx8m#E8uuBbr|$k@b?J@QyEs9wP14>95D~tIgiFi!MPc zKMDdkk8zFt18W^#P3M}6Ka9r=rZdvjF6&-<`()4j2lQRR({3U)XGgep%_^ekIZ!zP zES;0$At25DEbKGDf-wFGFlB61k#trlPR`C}S#V!mQgIi~k_WHqRCS$N$7W_K94@7A zG)bkFu~c2^E{A3%iX5D@Ac8(%m;y1;h>W&7(F$5zP|Uno{Ac4o{*7@|&>R`;&g0?O z6E|1Y>mh5MySwN6ySJtF$YbjS9*k)5VES+gX$u&&Q`|$iJzC%|d%A#^Aqdpy zO~MDIG0xyOmfak+ok+sV`0(AGi;n01RdUtvs<5S5x711BwH%|m4P)n8CUgSVR1w|F z?AC7P%f1A*r0?*qmi9H*W%Q~vlhz&0u>7rLqrpkXkyR=_fVVEnC$r7lnI*BjCIKP| zt!+>AMLQ&IC3>ttR)riTAG+@$BvWcST)fG!Ow>OThIn`Jrupvoe3teng6>(X?3fjP zO3<8m1TzAfThGdX8B!VHH?g$Bt}}pA_G^WxY0&M397!-Lhk?ZQxD^JhvB7 z8DVs!Rglb$#UJ}DV}`u+=T=u5e5CyeZb^sbth+}88zt7ccK8w1(N6$RAqk*Cc_*-% zfLx+55wXgYq@9zuEK<{EW@6L|3)o~4;O8E#cw5DXp=-fK4Ya4LfN`%*+Sq}8)-1V^>owYf;utSSsfv*4+SKH2iK%Byksr8W4ml7 z8AZ@O=p+CYsC*h2PT*)@q{*M3!%l2Xon3W%^o4b8Lw-7CaG?QO$C#P-s|W7fC9ov# zuJ}X{U8B@SXV+2qfi=k&rZ!WkyDrr@XvmAmr#*)wXWdeTDL^nPI|15=?P$hSX8HKb zMjbvCHFLOoYEkG9%XgM)d}t`Z<$tFeY=>!B51}>WaM*q)7HdVCe+}LP9t0P*Hpcshd=rRSs*%3C zx%lH$)KSbXbgiqmo$qyLa>Qa{vb?@8-|Zyo3dQe)qINv|^Q8rg6P;T=U@~>sH(z3Y zNG*PaD9qbG)hY9$dFTHE1{}PAY*$O-Q{gJ8iI3@}=VAcW6gyvs9vM-50+%PaO*d=eUf%Noq}P`^PvZ}}6<{h%C=m=}jg=hx^^ zExp84C1|J&OyuDH1`~u`?NfCxjIjku0ozb+1kuJ4bq2xraxnivunfpOG(i}X7XRm7 zU^^&g({K<9R(dsz0uWB`b>bntsv)7o(w}FOo)pd@r*wV}2CJc#dWnxkn1=R81uZUz z!|kq6uNNzvBqzBsp+3YJOlF=-Tl^%O%sZc#tq4Hulw@T168~g|(GtQJ!UwJ&)bzQ< z@V7U#^5R2%G57k23#P1yr9NsgV~8liV}rJtsINq`&RIHSf_d#x$1$z7mpg)v&_me0 znr;&m5<|{dA=W)8D6Xlk-seuf{T%m}{MOJC>M9g2lxr`ojHfrKVu$*^VRiXsYi-uA zoLdI|Q1L$}zOaGkwrnfZh$I>X_NLTf&B}K)Frgg84EuYL?@U+Hah{Zpuw1MRlGGsD zc7v?BGe^HhPqIMInjR=3WQ63Dx(6rgqjZkuKa?e?W0o+=ah(_I(3w|tTWV{BDA!aK zTgLigj&aFI7fIyvjU=1qnH73U_M*dsYtolXeAHoW7GsJhY+h2MMxJ#(E~<)^a{2Ek zmesd_!7PQ$dGf~dzN^)`JX(;<(7L>RVrwI6H0F(0UzilZm6^4-tSFj()b}$7KN=CV z%+^ak=Q4S$WDv$aagk>O4nd0VI+b8BOKOXiA387y;>=Aua>Z8{c_opHPT?7`U^QvS zJWIJ2N*!rea2+Y?SrFCn2&zj(27C$Ufv*N(G7ZphhrlBigw%9NBk13D7N*Y$uPe>Q z1l6`SV9W)Vi&jl>xlk6Rnh>B<;B}aDSY+HyI(CVxeu|qbWosBNqC^tKF9MZbi&S3# z7)3dTDx+&CS28B%RG2O+>Q`+sIio7V-JMdNlq|)p(&Zm=x%}o9eXL7X*=NEQ=oX~uGm&FqSb~uZz@vCF|Cgu@ z5V4*?v6Wl|a7Sa=^;UdjgJq+EDTn!z*h|J{Mkbs!IknkSEB`?FuWJ z_iDlNlXn0TXshf>Qd@AHu@=!bGOY*V7&h*1kxc!) zO3K|^FvFT>Nuw=_V^uXZCf&6)$^KqFWQRs}x|55og{ZUcy;z}wEhvLIz#7Z~UtRHF z#Vd%eOTqLotx;i~WjL5R$0#73!GbQ(3)~G-yY67s+JqCO2Ajs)rKy%cm1yux2ezgS z1+ZYLaTqG)ctHn>W_k1_xc2kkdOB|i_R8r>Gkz~EUoO!81N$%4-9iU>$0lXn?K`>k z?>QiDDSm5W{8a$GUf@_#(tIGS$=-DzBu}oJ7{5Ftgw0Hi_|^m7;41c%XZ5a zXmu1%5jQhmNi<79Z#Q|}D8!|^B^$;iX3dLE}$2`Q{&yX$rI&LSpJoz{(A4H7X`;`&-o%7hU#074^4qRF;Da0+%`#8|o}=*n zQHi}R6_*XivqiiS!L{xteP>r___iK+ZYKe>tJyV}_Nj_y&V%fmND0Rv80ZcRK9(B${iwu32Q&JXS2%}|}n z?PttrLEDf(3Y7_e@ar*C4?D&09KEt{iRFYX{^k2lT-Q2cdfx)KkQDd1;>+iI%Lg@w z38}6uv0sm3)ZWmw*I2SU?pvm&KghC3e_aeze@pId=@_QAI~pEAz;Ts9OLAFwA{5t*hm6p=m!JuMw((BcuL4KW+QN zAp{O$(5$Bnk965%rZF9y>Y`5+P`UEDO~784cZ@s5&tG+=W{E3~f~?auUMl`0L!x5w ze=`^$E`E<8HN}sz3ZulcWB?#NXdU^*&y!T-RDN?qn`%~Gl?an}Du*8fZZwi3)N68kMz!$+)XlOqaO7fGph)wcNf z0Os{g&-3bj5$fJ!&OPs22qqW88G_aStLwT4M= zABK(uxf5zC)WlDXtlNHE2rncW51k z;7XolDnr2UYr_&0G6%#ZK`@EG%OMNh&J5ndVik|P#L1zQ&?UMW`0RB%5w6K=dcp+z znBp=mDb(%&|F6l+5oU6ds$&XvZ)J6XF^*=OhjZ5)p4E9nq`jV z9*l*Rsz~u^fnn+_Gve?y!U&_Pog^t)s+o5Vv6fm%-pR`+&`Rfs-<=)=8@Q-NF3oSQ z4249ibqPX1M+^&j?gCILb8+swx}6_&~*epohI> zc_M6@hp%85KMZ2{gpFxZWTUdD)^Y?jc*jXNat=R;u^PMh{aRZC zH#5^92m=3E*U?j71-3khbrnV~Za^!H`FuVl`i9KCJm`sB^);Gww9^OMoR(YLfzF!m zV)G!H(vp(*<{OJs-IUc~X_b z8avW@-Fh)6$Er!R z+fA+N%T&pp){d=3(QlMALGKKOkN57g&B4RO($bluc6et!Cz{V8&&mOid++&b;_{Tp z4fV@bIJax3fPwE&r1B82JMBzHk2$i_GQMe;M`Tu4IC!OG8FFLWvSt(~6t?(xBrNph z1)DB4*|7Itih0ouj^Mdp!H_aGNf^Nf^SsxwEFYEBNF92aO0=M=dT*{xTD2M#&Hg9j z0w;@BvJo(qk9VRT#w8q{6uTFvd9&l71hzJ5{9J)JD6l=SWYCEqoS}_)dg`!S(~S{2 zdpa|eg(R7N9p8vz6LyF|s4{)|$SpBAJRng{=|K6P`HVP$^BIF#x6m7r^Y*_Ht8 zotBP~nszQ$M9REC+AALh=Yd69YSS*SwGL#LajzL(#`yiftIfh0m z>c8B^DpP#(e=Sa!bK?B2pws&P(!LyB$b@y9+DE3g5Llx$pQE**AoYV;3zm^f%{e4) zG-kv;od{(v7YYK6#}Fd*+Elp%JB!VUVBHfxvpu|6*+1;Z0R{gq#kO>XlVtB;|CA+q&dCHnGS-I%*lPkQ_53=89ConZi zbNmKyWp`AJfV=NKi0n}36N7Gf8NZ?wRRx8;s&Xd<;h5h|+bp^A-{mwPQ|ci^fSK;; zQW0KQYP$uMLfjs@sNjn%G6|eHCY>>CfHD&fdAfE;2P-OQo`C)fin?GaZhug@;RVc$ zGkUb}j1YD)vbhXd)AXlf5lqsMFz-x$JtDDe|Fd6s{F<>7;JY;~ng3{* z#)riRUGjOni`A>A-VEmTr_Rg-BjYjJ@2r6MDh&o4T$odEW=pK1)Dxj4R0feaP9x}B z(B+O2XcoPia5U8S&^R329n`1LSC+0E#SOr{?Zhs?lwCv!L@#4t8@qz~3sT5(FOD7- zz4-K+PF-VIN%jK}@{4%7(qC835pD6m+V-!tCDG~|Z#R2GBIvo$JhU8niz2dSG9rps z@Tb<>yy+yob7P+=O5j^AA6 ztKjz2+PemNWdNM;y%CHKPQR6_$HuNE+vfE50^BPpDexS>Wo^V1 zI8n4rrh0w9WUHLcz4~x0D;sTnAgAf%$G?7o_?jlGju|_+DI32fVF-5RuSc9v3N$mI zjN5Kml_rT*yTJNbwtcu&usvHiIHAuu&v8r;?nBs6R(J+SNu5mjc!S^*pF*;wCP;8Z z^eBVIba}3Un$-YUA;cqSC>;bnQ#ixT=^S16;<~<;_n+K#VRh^RO2q5N`x&Nhw6?JAedrkq5e~fy&N$>cnyy}l^we}YoycSNcV2!~V=|kK7Peo0 zVA#^htGBE@cpa@JDRcClq7J{U;>n7?s`#ghUswEYA>9w^l7;dLXb_{9;kklY5W0y% zEe#E@gQ62d9TOcvp-C#PMJalRHdmJduOL%krV;@vWg$mjRqE)|CM0c5QTPf(W`%$t zoEoljaE~iQV?2CQd3c3-I|_Lc{JN=E(ZekbmrcDb(S>nE8>M@}D>Qk3+7|94#YuQ2 zO91U05iip%bel0_t&wrrgkclgW4HkcL4et|GiyJwTxI!5j?Z4dKZt}mKtY_Z1T zuP8!oI|#X}Hyg!+W|Dj2sH#h8={+A<-DGKbr-qOtW39ESOgI(x{n|IYIQ8kNn3~vNRs`wvblTa)4Qt%9Znlm?oJhKn@TtE-tKquMJ1in6l z12!+o+F6(COJHh60Ta6cOhsq6O@kc7G`ttoDmwD`4k9A5hn+?H19}rwM#$%`KY3_F zUnbMH;n3wngVP@uf5YDX=8X@x=2;I;3~+Fk9wnhBUh&D&rs2Mxww`ELzC8j~FsY_- z%|vc>=jOGe^oN|B`FW)e*?n)`$lMajuu&`-TOp%(LsK2h@d|Q3_&{}0`0nwW?!D~r zZT*+8U2^qGrSpkX>+f3szFE(%fl+K*#bp&&(wzxfb}2oIpAR0T{NzD?%+nz)M0@mR z;BiY;B8Xt>HPj7twbLd73XY(Zr!NWe>a%6awA6LCwKukeS}WTkSyhtUN^j|^bt~7d zDE_!M zcjDmf{gm9BuH1}-UUe^Y~Sz{qVl8PJex%`4#A;}e2Em30e$8Wvm!|%KKpq({Tv$xk1Z2vFVqxLRkCP;mbbcUw6}G+K~@iMx+q!AssC`EvP@%#_{xfF=aVIj z_sreu3;r=Bq(C~JllH!6SbBPkq${RY z|2Us8P@`<}Gf9re#jXV%Xv8S~-2&2_tDBXGP_CuOgLCgU92W!^rni?WSzxX@OZo}K zr@#;t5aI`7L)8OA)YKv3n@VwKRi(*HeJ@@E4?K^4_BXWFx7RiL&6$o^Tn4=g<5oNM zgW1Z_rWLD;A2%hA4Yj10&Y`Xx#z8nnaxg569*lLhrhtF}P&}z?UAHaLIGEfrks7EW z>_Vb_b4s$yZ?97%Ac3vAZDSzZPo;#aINcLpHs}i|sW9y?G;~-SfNOEE;>bCqI!8@I)7PC^)owa+P8Am9gZQX?!6CnSRPt>3 z<*P($D-<|K~6f%)0_*PZgX~^1zFCtNp7(JgX)F-Ob{muP-6Dw;+t2^pz z)64p5`*iqJQGyg1)*DOw)~ca$VihaX6BO5wNrVpE>e< z3>b5<>h`K+Z79}pQF86b>diIF>e{zfCwlT-6KyFGgs?0)oCS%iTps`GEE#V+IUr2# zTR@k&3jXx(*;kow04M7tZ08fGIaZ040mUu+GZu79<+AopjHQ-@N;84rVq>Qi?q}|& zfVMB0&3b}%7xhH6I);W=*OWCa1!$4V<4Bd#w)puW_V(J8p_ldZA*|TPW zz-lm|$#19_YBGedbL7!e4^u~iyZ3BVj76(ZlPwITufk_}}cC)f(o_<*n;JR-gp6-^n!8C=P(y zmXSZuTK!aAB?L@}jj+ZP6(WesX_cP#VK9OpMiC2bpGq2vb`0?tDuq)+5k3gBXzCW2 z;b8Qo`kYl6=HtZa6jWboui-r>$}{og=fvTMx+t5k92+^)PQkp7m zoL##j-xm9Bg-mRTjSOvXNm#4vG}dq&td=diZg6NvSGcZzU6W#xVX^O)R!(!KUROOD z$2_02{+{4P4Ye!rqT3b_-1f4FE<3aaBEqz+5dlPQL-(5F`wx_5H0Ko%Vnp)y&-(Ho z5#cP|YQSvsWfiAp$q<@_fPFzlDoZuDN9J3#dQ1_l=;gdnP-txTE?(Arz)0#f}> zS*nyBy!U8p(yCq(Wr4+ORX5x-Fj$tVY>{O98lP3LIj}6VGTjoBRq$oUhb|ndM~rXh zu4-&e^0>8JsCrwx?Gt}Qx{e^p;gi>{*%+Bu1Ao#lqW{Cz)K{)aSIXFP5h`<|$7rm7 zdHvpOhL0qs7JvX@~bNN^hyVj{EV1! z8t2sN1+H}^A5r~?d25z2qDTEWF)@uou(RQZBp;cvpuz%=AvRn!q&}740Rw~%7kvcOYFa+6~;nW`j5a?ksQ(QC2CskaH;FpS+4%3~E$uovjXaoxfG3In|WE5i42|7ii-8t7p6;j(C^Z0WQLlIHsQHHWu%HLg9htw)|M0>$dqyvFQ}>#x;Zzi#Bu0Q~g4hM!K}dtf`O_+RhL>K10y;5)W&c7#?L!VT z-bhWx6Kp`)Y;37}0T^@^$M#aCD|w#bZ3}4KR~HZ(b8;$jXO+Z{R@K*mozr z(IrO(=~BNw?Wm@%v_-zVfN;y<{?69wKO@Re`$%o5Z!~*(a?LwvXup4LqG$I+!A@&} zS*(JXr`*(A3Y2E>U?S$_@_Bhk=qt7C8(II6Ic&9 zKxYSy?rE%#bn4sfwj>Q+w{GqD4F`vccP?xJX4^{@9cmh%!@H9Xn)f5^hm`5ZvtuK8t$AZ3T5S%( z_wFH%iWxx@-5m=;_yr8-fA8{JtBksJer@s5rB%&w-ayqeF?3{TLn$6=OYn#qIX|~8 z-rCjxE*0ecHOnjS%{Zb{721Lpg}|=)OkEv zj)b^qITC6`#1tlMuAyg0i6rkEdHT*xg%uxN|MVXb;*1>)q29Cu`?51R&UA`{o1XhCVs z$f|kXn4QXNDo#WKO+!o?{m{s!AN@LfNXoJaUYSRO{*17lx^2u*Q~je}|J(v(f$?wy z&4eg;XYWWYY~e^Fav{l+Skkd$w3G~`YZ{tltJoBCsp!;re?#q9E+sn`8iY9?a{+?S0X9p1g5pi!Tik#$>;Vcvn8J*8N*dIvR|S9+wJ^aS$DF8Vt{=X6qJ zeK<&dR7>ftuNlcDMGfgsqT|rg+Nyj{e%0ohw1@PE^tvV8cy2T%yl1?o|Y=nwVsNfmbIXhy}KhcTIR9TpJvSi{!AT=lB%%s?KQRZ|yPkv4j&*w&m3Oz(1P zb(I>kbvYHjw)n3xmz`x$Br5e}*O6tlWrLE%O_0kLrD*}FBfY9ychUB)%KAh~7-ve^ zRgH+6;aU0B_vYFaGZGX87A<;AwN2$%bXKY#mgd|yzN_0k2QU1BqZh@ZC>rxqzyFaj z2g&RtJ?G+gpZ&f^7JA>b1WBvE=w+}0k$Z`)9V{>C@*;#^@Xb%&ud|w)SFvMw!=4Kb zjDLilyCTsZZq6d&CS|}`68v^3Bg*@95fhsojN73 zopitoVCf2{1}tZc&f*iq5P;8HXm;)gloR%gIsmxV+x5#b%RJ6eVv$Eq#alv6RfJVi zO)BA~;+KuIMpOapU;D7+%44x^A&>me89+Wbv5CBdt#THlv8U!!&s0N8>1t?4VTubihv+F7b{AQf zciDvs-k%A1TJagi*Ru>GVs592OrR@ic`|^G_emXqt-EyJShWCCgQ|Tw^=7~DiW<*`$0ORr7&LH|&{el36dR zHm}&Wa7AIzC*BR?$p~=b2ml>1>QVcl{^OL@t#Tr+cdS{rbu`n!h^lq@4Fk(Oo7I_u z$w)9UbFmE7zfQ2X)Hov~etkF;)HQ=0pixXmV$8BoL`9QwWz~F^V?T)s{de!~Te$eG z4-CX&x%Zvk*|c$L>FD^1lUU7Dg*-d3XXxCqB@AT@g+@IcaP930(-0IYSN*UC?eiCB9+vJKQnA!D9LH_}=aT#Y(Y;WX z+yE3ML$7zgBPwUCozc;)#e)+{*!pn8)rE|DlBd2K8+OaEPYUn`X|GIRZz=hXo=B~m zY?2a5qAi$QzIIFS^jEzPCZK`Hc;MsiUd^*67?+{T)4|$f7tWjMD)#@q!3tmc5N6k8 zC5dWwhh)1Lv;2F*mqWltujhCkTPh-R<7sytqwF<5(!6vwBnHIT0uf| zUN~{QeOh0)xuzuOR5xf{(BG%w9W)**h7}qrbXVpA2wp%ni@pT&P3}pCgb$}=Zo3f@ zu^y~gwjjeTbg2ZN8s6pHY3~X`;D4EQ08o)FPDDiWqN5V3>Mp@xp#QY zsC;6p$e?75D1)j7Wh4!3B4S0?qm`#}WGNaf-MS8X=L?`vuH*(-Beb%HtSH!E zq2&xuIJpRXw|*7Y!pbuE)~X%(P#>=;2wd2Xcy1A@da|f1l=XpsGvtsAJs)x;gYvHZ?M;>wHU!F_~EV!`4@{unV3tqv5+4@2#1P z%)c{pI^)NPRJzkS_FFP>V3_#QO;)oYb0&=)=lW07whqCz z*0dmXa~5Qtq^JqWLoxw^doZ9&bI>4L3wuQjk=49Fr4ltFxIx<>8+wBtC#7-8mu28> zHGVs8bGGCBgkj%reERQ^222Q*#v`h(w2+Zlf>kUvVko6=_#P|N`)9GFKm)64IV?u6 zb5|{mq0`$l2^%ejvsK@)5;ge`cd7BA~5uivl7sp`tyN*kslK2N$hoICfUAna-}4 zb>D2}BYhW%QmSPG=KRK?I3TR7+$xlz?mJ1)UCMMg!%H6cl@Z|;PW3O|*KwQdNZGN2 z>Gh>=SLERE3^+t*4RAcTOvmX*J8;fwdK!u+9L%|fgGP<#1oeuUYDKm02}ZAio{(4D zhLjF=zJLE>U*-y(r*+0Jn|b(^6@55DN>8q6(CXs{CxEV1vxxEN(zCXWUc=XK@42(| zK3`Je)bUn7xQ~;A>m`w?WNt-k{=k(>kF%-6l=JDNHudZPDT{-=HxP} z+?4t82!V4cdl^`i+1)?7GgH1z2;R%dE1g}zItdkIO`G!DijAF%QjYyDbvt&Vy_`zc z#S~46>vziTz{QH>ddkvO$=z)RSX`|006P6Tg@(Xw-o2?rZsVM^gi>V7x~^~Lz)5YJ zqA0E#DV`&HqN%POU3meJ%9W!(V+1A5ipR-s5PiMZC%@ya)N91EbgUd%AnJsaE0N-{ zycyB$zD0dkioVW@jFi4EmyRlLdZ>HunSQ=a1h=dr4(4Ao<~{3m%IG+cJwN8Hiuo$~ zM)zFbgeZ7^^vAqE6xNlP^*`jz42zEo1K$GVA7l=kxN9yP1TOUaTI@nhF&v_7Vkfp= zbph%W&4U0F8ZuT4oir;~ ziqJQL{iciSZUh$cSL^Ef3V80I=7HNBj3?m&qYYdJ-2j;V9A@H7CkZPeXcGsqi61oU zQ9q)!GI-_rKte&KgmNRmWYTO0ufqRBWqz0(hF4~>Hk|pLQVd&`OR&_r2Sx?jKjY*B zoEH3R>FgCECAcw^rfIRR=eGc#1h0VC_%P=K?vz-z_a0(M_unpDYHQ)N=wRtwjr&uE zlH#rnaS9p}8CL;lXZA}hd@;sE8hJ!*>< z=YCWMqt8!lM^7 zp}`e{Po2{l2drBZlli>5YwB*JxcKmceYBqul)rK6uy?@eKp7-}9Vm0uoOmmMr;L=xx5tB-(hgQyZ29@Hd*|Y{?!AwIEgAv;d$|g4KXkuY~U4dV;>qBdfU1Zz6X^d{tE35=hvL_f{WXHEKu!U`;?& zHFbp%U0)uzf$u}YnSAF~^aIq9+;EWs##%`rb0p7CXGyZ7x521?taY$J5{ayN5$}K{ z6AH;FFSt>tf#k){6G@hlj>}gTBd_0C%lcMW?5Nc!qtelv3T}BEMj3^-$Ob1A_UW8S zu|UsD08=nrBgP_YN9xD+g_;e5)jKVsGX`*y!*WWy8DJozJNgB-TdbQk#t#c`VtEbHx`UOq4s zssB-Lc4*63Thsjh$%48G;HJme>^8Azo zeifEYruz0RKiZ^%g2hs-js;hDxgR@K(r?~+Ex>8+TG^#(djkNVQven;{qFIG@!^Z! z+okLH;;L2GZkX3!@qppf1BQ&5eqgyQUN-b6o(6iFnM9JMr9;P#hyp;7Y=N;^w#Ho~ z*Q8ip^c;;%6QkY|&aQPY=I2p%U7@az_Rjm~{)GXSVwdk?;Q=9i;E`s(S>=MNV2wV* z42L{T&xVg3jTLl3HSdoo^*}KpA&@Jt)1s=H3jb@3M|dd$@|lpMGcl600HL*zXbr3? zc#J4{l(hVRYd_MwI~l5LL8o~wMeXUQHg3kP(q;`+J279jd_i_R^JGFc_-f#%gYIo` zM^F5?{|d@`OqL=b47*IDnvu1vUrK=iQxhYKgCPSorm0CoE}bV}%+@j#Gfl3+*TQ!A zi#4Qhh&c5c21bCcX)ymr_)r2kp9JU`wkUlGxIa1%jyx-bjHUGX+5(yDUF6}DHUu2f zh$&!!%JpaXB^sMxHv?7~~pe1efj?ODSMZu7`j2jh-*>~D;%WcdiA$66U)@c@ByK@DLr1M(xTrZK#g z=Hz$}_aL|W(8hu$s^-9rn?No+O@s2L#|5 zP1Wsywq#QT=(sEeQLk8P2qOSRM?h7zr{6#P#B=T6F6~RHDtSNyBZVRVHuF561VL_er5SURYb%17GO|y!ZnZSfv;%e3 z^!)?rhoM#$dOXLf0<7_clhYF3Qt~XObiv?~@r$Wv7>B$!zwfg3i#8isIfg;1Rp)k*>ez6e9q-5N6l(WCy~Sb! zoeV8M7?ntHhv=$8Kzl+d(Si^kLF9~W5o-)Je~EF27oEEVA!{yv{j;m$jsWL^>uQt zG5pXc9z8T#%xk_a#Fb^02R(4vlDzYFD-i;MmZv$qa8zZ~klDzqM|2e|1lapoT_uK9 z0Ht5S0~Tgd-`e+?4GSk%-r2xw;XO?^X3xH+^86d$d&$;omhaJ-a8v@E;_@lMNplLX zd(7#R2BD>d(u)RZKCyfjU`J{-PMVT;(PIS!!c5j!hH86_-1Wqojl>K52YK;#Zb$+r zp6M!>ePqK;>i=~B*_aORt)U1p^3ws&G>;iccxXz;io>O0K)6VGTIU(C09ykqw83H1sm~BjYTh9;ca{DC zf}b!c{Rxwaqa+pY*laH9VM;&qxAqJk`S6_0TGYRJNQpI*n5ss(<8oy3V`X)_MjGsF zqQ@w$j>hAuDTB{DKh?Hg(9}CJXQ-ZfVCcXXpBrgRvvSBLAuaUu@y9qX{!zfY*BI;) zf#IzW?-@BOdG9w{iw}M?N7Z;b-Q)sDh#%={kR*|J8M@n6UMOCu=lN>Yr@$Wo4B*-JJ2RC6ZR5n|>+LGq# z3EMXtGwzxe7ZiTHn!33A{JYtn$`zR0d?0>d)b@eH*j^t8qWShGEw3+&#^- zHSfQ=^s!V{VcldpVdM;E!i9swy6cl&@!^4}u1mhB9?Box#O(OTvJOt&c5w5(Ns6CSoKC!0osz4G;{hvHkv@v@m*nXfZTQTY97!A}nt3%=sm=#m$9Ub%bq@qs%5W8YgFxxFf}&_} zq4ELa2JIIGs>8xVc(j{D%Rj-ZigZA?f-TNcoeaXJ8CLJ0bjQ%ILEq*a5L_<$cSSJ^ zokJ!@D}K}>L*`Wq_|3~caLPl&$3DX=pK%kSk{A>mGg?okcG!*~tYjI9)f)cANvQF6 zepyE*!HjlYarpEbdnV2B_b-%hjL7mc77~<5=-@jc4&U;>%e-P)mZqInIhMUd^1Kpx zU&sK)oLH}Xl#F>ubC|2Dj-vDM(uEeeI$^kji%@HewnE zBB#W14I59tW=liHwvIe&)R$gHtOMS1aT8c{3?Py-%JXOeY8O5*NlAC2Y7!cIliW`{FZ0+_9sQsQF=Ro`rzdN*)jK zaps3rf+ElJ()aza#%uO}h)COAu)8XMm03Q+pM7QJnV$ym%dY7Ni6sc?UVo9e2Ld4=jWam{N>iy$Zrvq;>eP9Ma9S%Cp$2o2|SauPyMQMKnZ%--q86{5Av+m0t%3 z0UukXJNJ9iZlI5(WhUbqvJ$qLZP?-9?UgA2p2x!+q9b2qN!KDw&MX@5H#~{+2%D0{ zyQe}$!s;TIY5*smkz<^G+cJtvT|-mjk*hD(4Uj<8dmmYU)yUkLeD0~?jmv;AtFzz; z#zg->Jmg)m`Sb|DkD29FddRSxc$X0ZQ;MV>Wf^c!87dP-nc2J;^=UUN7$7JEq$+HPsEKx2I1C{NOwGFGkbb2CI# z*J&gVJX6YdRx~NIv`fp0NK|pTd&{j8#IG)V87*xD>~~ZP z4AI$6QQ-5}M($>A`W<id1%9h3Zb)G)SHJg|3w<9vXs_qVC zMt!}Ye4A+)m1AQHOHLD?@>L54BxqO=G%p-nK4^OoQ~I-hi$b2mL(-rDr$I|3<(nCN zT#2)SdmOa6DCZmnPOFX+(gHWI(-~-7J9L>9_e{xDEuMQE%L1WXxvpr{mUZqj6oYb*z%_d;f zW=}1aQ&Cn*V@HhxR423X{CPjb65r)YCHBJ00nr1;0`bTHiX~RF> zdbSseY&>s>x1wH?`8mzV>+JcT0y>-*7D(8&wbC~L`le+WVz8%O&Vwdk=J=ynk0(Ni$C7+l6od#bK47$` zMNMC}EmKgO(9ce$%X9s36;qbi)qM&~SU;l3J#A5Bk?9ehk%i)h6Sd+6Op0A<(r1 zV;_uo;Y<#FT=mBa@l_3FV$Y|=Imu{7X%bP8%m}2#2Oy~{F6Oy=tPm0#*?$nUxq@kG z!ewf+5L!|CjVI_9pK_jbGIomB_)x~Wk6+phGUdICRgRy`!JB}JQ{BxmauU=k(q@VN zbW3K!h-5$qyqP0$hd){Qd|qEyud|T>#L&l~T=w@=^r~X_Qlv<^H-Q$9|HFR(p*%QX zG_?xal;GjSmz|wIk8ic?!QvD3L9|FPvs9bnPH?ktNspadQnyamPepR`NeA`Vr zN-i&l5c9X+Ol~5etiO%iNnXN)zo9&vj)Av6m3c&UZIU6$SX)=ta@#ve|3eXB|AjKxj=Wg8Q_rKK}iS&uSefCxzbP!&WIAWTz)hAki zBipdtFI1fvz^#en*yo4qd#zMUL+OWZ%!vBm9__%bI{Y3K4>gTnWWHPZrvlb(I~T}u zB=t%dyT|Mw1YA5`l@2zZDLeAPYv%=Xhrnn!!T*)q_;?o@(dLz31#gsVm|0PHAe6zB zhT|Iwe@*qV&8t^ws8@~G-CB1PT?lHi%k2iBDEHGWItbqbCrDA2W-2q7tO5kdOqC|^ ziNT$sksb}k%TQVHm{0_I*7%y=J=>nBl(@>VdN6TAhkM`}(C!GTx_$Exja(CnMckfH zLbX!s8oZWpG$BNDPvyZUA4}#WLBrY>;mMNTq69p)$@n`3-^r|W_+&WE@=?P{+uav> zZObwslIGb@rEWUyt@q3d?~?JAiw^guqHm2yEkO=j>QS3Z<$00+B;cO^v-;#Fb6PCF zA!=En!eT?()`7n8#PIL-+mdZ|>}+r_cMjNVW5aId`L;d31Puvd}An!cFDn5krpctL8k!T&k-r*eZ2k9qXKAT@5)uP~|!E1vn+Dm6sfS}z=kBm~? z0AY(7$zmlAqCrhiatQ}41wnov%-GOZBTA)HT2wKpzn#6bVfw2B?}}6~u(nkN6!ODV zTov4CS{2BAj|;q+>1yqhEDhP4eUW0X zh?=sSVJtC&Adf^3+xfw_KD_JkVPoyeXwh1-A>}rlG7tl+mMG|5iv`o5v1-DJugawH za_mj(+2|>i8{Nf-(*xsZ`sMd#RR{nd8p(-#F%F=pc5X$!S;@E=% zeVBdq?G?c*I#YbCTwjnT;fGSQ56aVWWw(6&p*z)1^GCPyH+}JLa_cAV=kNadyU7vL zw9q~c`GC*3i`XbPMTv$#Y$|`?Est)TtL85qK7VoAzd0kPB*8Pq52}1J!|`8LO^4N^ zVc&0TZ;spDn}CBIb-x~OU&q`~b$kqPbHokeO6f^?9Mn`3(nTZ&Qj=$5Hqs0cFuJP8 zYQFk}=OOVBjbk_NpS!3ZeQ zG)T3851p1DPe0M}*d>o|nv>p=p{KW>nUfVE5r5Q(84mi})^`{4>0&V%$^Eb)qWL=5 z;Xm23{Ek(&?Yw=*l6NpqRLPI5Z1uJrmUosud&Zn#{^4Cii%uI-;Bqt&!YdI^(B6+> zKo)lF!lkt71Z;9SBpHly+o@AMsNC)A9bZQ7s}@h)9PgT3rvv+QQ{DA-cY&Arw{<0C z#)Ujk)o34uMYvE5_-$#?z(lY=qq>z*P^>Ohf&iE%TAZLs0XS0v5f&=`EHGfah;e7? zwJK?e%~xU+yq^}NV6q@+y51A`q^ck#PyY#Y(mpr{5!p5>#57n8@(8?r*7l)qQJw&$ zTetv*3alVC%y>0&j^MHYr6w46@xzz`m=~@vf4ryk^X`RB!ynlEPUBQzYEc(y)@|Fj zP9yMpNsiH+*f~TO(wMVjwKRIoeOgT8#pH4gMCLF*5OlG`OLbKA(;OLv%2Fn@^*lTz+9NbgI4Hf25+a&RPu7UFq>&OFsF{oK_f zp#k75g0{NXE`7sw?R3MB-MD7UV<|@T4XI#@#M+TJB7SqT?3(pEQk@bie;D8~Bql7# zE;h5}qGc<`&U@{q18L4t>RY1%o6K3UMU2yP z`~^q!G^fK{orNA?7iPPL>PG8+R`)yTFum_^%IE-6FYF27E;Muz^ner)sccydX++fr zCm!Gm`<0|7NNikc1d8)jzWh_fAV#CA1zkkO%fr<}&8Y4MJkXU%5qvtMdbD9w2FNG? zDUv}jhFF?b>t-7yl(QUz{J_`*U162y#}5zcd9xNMxPLTMs8U?=a#LF|^Rg$#>n#w~ z8DES?L_Mr|?h5(xb|$9qxs96KnpCaU@ZgKg3;frZukha@-?;ALbxRfFY5#n{YJ+xg zji4qPix#R&u_Ct4k)|4Y>D)LBE>Bs!r^mkg~UZp*|FA7>}x6p;ePZIY}w%ny)yPnc{Y{q9v~=m(+lH^NDp667F_tG+9%pOkW{nzWQCZ7#Xypnn4f?RkMoWkbVaO`oS$fnw>|$UR zQ451zoXwRf9UF8a=%bNfcrAz^=sy05Ybk6O$GGsfUTat0TT&#Tjr=&OidjJqw>^&% z`?0r$bIVqnsu8l~&6hG4c@YEd>GRxmMgwh=eU-5VD}wBr`Sn~r_NtmLv)Q@*6TMg? zX(~8H>(^%*$G#j2S*#FBlK-erBS%NInwOl+kREly7C$s_roCb7x2cytKcW}qXtin2=y>!7l_|n9db8m##omQ z7RW+tO0%0f!6VtSp7ezrrGKd^sbHikUi=3At_QS8Lz+MGIwCn;sFKofUL;#&+BQa2x} zE=|#dw-ROGtq2$b<-qI0NUUc%AebAO9}kfKY++>>#RXS(s?mS*#D|Vz`Uzgx`EM4tqF6$R0=g2i37)n)mGkb2Xl(Sv(Yo4V&C;5FBNHW5dbB~tJbR#NL$b4jY-5F;z zcUt=BJ9qPpQqRbobTsCvj1YTD_|r)Y*qmI~1k4=ZXs?4bgL)6pw`VLph5~{ML8~^j zUt6~RxD_;mfg%UFA4Tt@v2NL}Va$sLKnx9lM08N-AYb{O!I+@|NUu}?>wRLcK~`f7 zg4eU=KSxYZ9fwN);sHH70k&X4I7@ak`n69W`B|Q7@^-ef0vB~lyOGo}VnP$hqk{Oi zI@va2b|>#gMe;cv9n;9Ns3@n@(#miY$x)hVa+wd-I1chJi_mAp{2$k&v4RPp0mh!x zNfcxV-Iy_>1g>yO(o0=lqus3vt($Cg;!#y~Fl9zK(^)|3WdubDOan7gX1C?^rekB> zXf3ns_SVuvwGU^09vMstSEiCR zFABqteENgK$DT-WJO$TpX12a6cW`m@q^W7VBV}OofD>g)VaF~wW7<=(8q4NB1mU2f*St}WSU!EOQ^cg z?Dl!R%+EeGeC$J!XiU~D?|(25ZZ?s(V-CjD(~+Np14_%7zHnL9Lta@ z6hzuSn9Y^YuaJcZlIy6fUit&5pt$23l5_C`?X_2SDQI^iPs=NT4J&uPXo~|$!pxo{ zYTq*jn@ZOVrwiXhQ-oSW&|mws^(0Zg#(a7p?CAf=V8IWJ(C=p)Gl7sByz5-N>3O0z zG5(jB?Fm~b?p;3o)aM=?Dis?zLnM<(lpk{} z2CZld7|5b*TA6U^sdSv@_~WeH9t4xp8sax*o&Bg2tLv_u?QY zJP!(hMHh6eIAvm3=a7@N}B0&>xL{KV`Z>jS`+{2e$JTY*5RKqtQ+tA6E`8dNj;dfQ1aI!uu3ymblM z+Q1*|UX>@3A<9bCtvqWU45^gGT-n7z98qO7n1@QQXusLum;Qxy*$7r^)HR0$Vfd3@ zo~yqt!tl;b>oe-(jS{_Mmh|-|F>E&43B&=OQH+${)tOaETJ%y9iADamN_eDEd%sxJ z`3xJ9eSii0ST&)x9yg;coJ_JxmdUOg z+1d#zK2ejy+m_4FPK_wJ0bxN#H(RgFa7rT5hLu{|K?6=I^b<)-mg=y<>|J_aMR%Wl z->{W|J%uB%qO6eo(s3s+A6r1FYsO^I>2){M-HV)LwwvP@`5CTV8f2A+DasBmP*SfI z27@{|*aRFCgj$A8hD2#0k_K7yq^QDE&w!WB+hfNxHjUzfwTyYU16QLrH~&o7KDg|4 z(~=ecaIEydEQ#rn%cCQW%=dWCz@E2a#L0izmYON0hz*WTtP+*J4JJRdL%*l!2|YI! zst=da8B5euFVcWZjvk)yF=B;{BJ+7W>R!5H_}%jskIrh&uVQYkI-%2}@>CaZC9eIf z@rQ(w+-UMq`S&XEA}Swc_;~E7A)|q#vKEik^9OKO<+dR~X>N^gw@@o`iktGZW2d;v zDmB7mDc{{ux>Ip{!8|iNgeB9t6{Sm)*=5Ne);Oo}x;4l?=qbIAcH?GiA|SIzXp7i` zdME_LV4y4VSwzDKvk;Bsab`HD%4{4>)q~kkyan*~GbKPeazvvS+!l1Q#XPNT2mTxv zj3$*q{d(?H%Xts7#<}?CxLgIy@$9c-wyj1Zr5^#*FS>Z2v0@x{Ni6+LmRTDNcwnQ^ z6f5E@?TtTkQwA^PFfgL~-pJzT3kkbe2$|wbZXy&~;M?(EheXo53Z$-CAkp8C{5>;a z3k^m|DcYhS$F#<C07}I))9&mS)Bn1pC#~GJYyIR z?leiNCTGWaxu=IL@AkaOPSus$3WhTfkf^)J8+Q{+Ci&%Dj>y1W6$BC6a|Tj`>*XQ&QGS9u-$TlfT7i}3 zdc*{J&ycz7j3uQbDWI*QuR8qb=J#Am*ehc(RkwgU!QIFR+670b+1<|~yWry@v}}U) zREZyS9EMxLVu%wI!8#X-9zE`M&*fN|ay*7x9mJGD!3S<{w!}dFTX5C{Sq2*8xRR*2 zmAiGABtpVb4%B9JA{adjGUciqr8Qlmb6e>Lgk;6iKQp97EWOB(p3;NZrBwX_0kRGT zb4*W{OCJDY#Tn;Jx`l?#`|Kku8*5Nbz12zT%=J5auZ=kE%g5duRu=b|wklooK+69o zLyIf!QU{_SzLHOUK`s}iiF#z|rI zRE!s7D?2K4^(&T>w!EnG2_@O2YRvEx zpDM?+pZ)M~YRHqqab0X7PgS4)Tr4?&8q&|}eh=SB!MeL3_t`00q&GM_8gqC-&tY~+ z2hrrbI2?22WwTeQ56#q;-~?&^Ht>4E52Bqd+Lo>4`LvjhPmo2G$Hrz|G1ZIUcURt8 zK`B7Byo_{7m4g3a!=3i2CJRO~pc7y=-RU z9_>xkBWZz*<}5zc1?Npw3e!zA%ck*haL$<1s7xvcI)J(qoGxv-AUf;j%l;ke0w{4< zsCmX6MNpc?SF{w(RN7Nu;5VwYg;~p|3kxu1Z%-%92mEAP>3;=gW4FbKTkjyT;znYr z=^dv3N24M46Wvksp<`dMO-a)wa)B3;B6sj!L~ftne(p?H2Isr^iOWyl-gQq>ik!K0 zpn1~emrUl#vsFj?|Ae+oN;3V4?hZZIFkJe}&@XmGBwjrAYe|{Y8l=cMr6VDiS;-nx zP6!J``VMesM@_w+@JduIhGo4~kBi|;_N?l<ci#v`P>^!ZvV`sn7)Sn;8 zXO?VVk~7o21raNbts6>zPJm}mm$b-0b74^~0Y*b`8g}nqQq1Q2^UK#4>oGFVu^qcd z+nCmhi7x+Dy^2*Pe%&&|>$-Q-#yt}r4^EcRyLyhJatc#M!#Nf$P{i>QC#8s+v#*(1 z=Ev|*FdQ_D=|5^@1&SfEB7JbkE^k|U?Y9y zJ_aIKqWFxEx&}GD0p#7LQ{D&2spvEF39<@FNw>v-a4%OT+za@z7$IQXOV<$Md?Hw- zt+Xws>qrLrkNgBou!;EJ>Z$}8id4(-^|F!8&$(4}0()lp_TV=HG2o742(pb-*kGZY zG)2bD4lRUGr_v82P|?}fLrV7gwDhLd=C)n^biCm(27L+>E%E%?9+Q{W7r5b}wJVwy zBPXw)OexQdA3UWB2e(F)Hn)a&ey?oIA;fJOd;<_e0T#Q1%v~mTzT*m zC~D{-&DU9I=y6IrPoR|Wlr+G;_PaP7s{J(Bn`MqQ2qWUuD%;d_SL*l#xox1SHzx8d zFk2c>hz*5P!E^s1*++mT{=EW_GT31fmF)W)0{IRUp$=Sw#)Drx2yB7D? z`FMs(7gcdcmMKrG3`8JVfs}q_VW6FGQ>)*)7cpC*LL_VLD}8HNlwYWs>{j_i z9h3VMgNJTDAsa=sZ50cYM;7W#g$Q~L7zqktj8F{S6C)qrUqdwpPnR&p$*F?6x&JOwVTu=*bFnD-E+-Yz&B@{P z$oBf-f=N!RCCu~h9m!;tlK5z5;GEwI;fUj&^UDMn;zdV^zj*3vQX7)Hl-~TR53K=d z>?+c?E?xS_NrX3Ne|_M0ZLT|joCb{;v@bE~KywzvnAEbO?OzP=)q48@WnBKwC`%^? zY4PUO;>?}6bn^=mPdL43s7T$7yocquFR$g23G%S1qZf(ne91b`W6gwO=<8NK3=*jX zGp3m2${LdT@D|i474mHaZV1Z!G#mBQfzqQ)+j`)9bdLb=1gwyXd;TMBtq`B-#XoYv zJn5yPi)ssLPDZUWb4LATeF~!JlPVPM{T!Vp6(WVqFEfx@@A=JPkg0+DFoWZ zZCt~bwQJ+RomeQ3l0iTdEo0H3A&-a)FRif2s>7pAok?qqvdNmfwdVSTBU}5z&8g;= zHrc$shA=^)2~Cq8`m9mbv@f1EPgcp;j8LzbVUfLPa+XDwvy|x0c`UM@dHr(T6?Ew9 zDsp{Y_q3mCClX%Blaj~=5kYkjaGX6oaTxG2A5prx=v zR}}1ATgpIlaV(zzcBz42YR{6!dBM~?>)Bgdyr}O@zuZ^3$e%v&n&Le|P3J6KY|^TZ z31;Yb1+uBOi0AEuHqLB2S7ZztSZH3^5?0U^tLBMlzR=+IsMx7uKiur)bI=dW726XxX&V`@f-LTKjPSC#Y%taZ4YPtcJzg$8&Al1tDh+Pm`I_ zR#}d(l{f99!a0E{ofFT~Guc)1)_dM*foppG0~fx2oh$?>dxxQuVY;n26CxUwVVGWO zB7-_nu%}p2!?J;`IPO<#;Rvargl-G~s2>A_3nPb_(27Z{i}4Oh)0hO__L!hL7R!}> zBu3P*W{eyXSu-l@+UAk_NG#4QlnrSWix09jbZ>sPl>XX(CQ(JV%}oF8|Bh@4dxcVI zs}K&m?xBB=m7V$L_q^)rA3ll3Rp(0Kxn{0%;A z>|9$(mn^}QbLTOnoJQ@asit%H+6D?bjmoKnt8=0%eLS&b5q0vYX5mKn&!j_D*^ubf z%7uzSE+jBC7ZP-u3%x)kd%VM+v5r9~)oC$`uT^p&inkS{K-+9%0;=H#o@QS|6w=7!35F72tJBd5hE*a$Si zv4TM%p{bvu8$Q%cl^5P9A9?<`?57L0gqca+SEZT5r=~+KIXCULH z42e*o*f+gxOJBAnp31e8GSxU7Q^xtML<%t(8>*pQ2KaHMMVzD z)t&jbN7?(Czo5dZqON)nno4WyfOo{&HU)D)H;(Aa*0UcdX z-$N+{F9>8!i|>KD24N~TzVQM9m_NQ3bUK6TEW7fXgk`_ae3N{W{FqhJgB`9WYLS+V zLs$u*zj9OlM>@==CV_0xM@giuzdJ1(J*(o!-fOglK#v?(>W5qo3!JpG7P9l0_4>6` zT%DE3HYL3*Czt*r+LIUT?eHYU&hNVE^EY&lHPzb~Rnl#P6+{gt_1F~p+o7o9XNwJr zm`F>^F2&*r%H?|AX0euUXnDUB)?1nl1q-VUIRzqv%o?g>cGH_7vpk)g4dg{5duVEq zDIvJ0EwZu%5r_sKqSq390pCPIMbmuo4M>y0w7H;ivBN8d7xtbtX|axG`O)s_{*61= zOx!SPbamMclW{!{9CKT4i6mC!v)vu3H!fD za!bdCqyZ6I?jG9uuc zxdhq2{{%{4sqoDlAz$ZhnH8*rAlbs?DP0keOZxDHrWju_UHm3tG}*$djJ#riv7tTu z#?Rnj{eVqvN`6Ng8)D{F1)n+jh{wW z$NG+*$$WE$OS&NX{hf;D7?28Z33EDTS{ds{R)X~InixBGXfZY1oCNJb!qU+3A|Lz~)UTF>O>Iw5{ehb(e-$M(`Z)Eo9aTDh$(tcr#3p6?4eLQ2DTXd94Yv?AG+f05p?&cN zB-$ew5`dd!(FF1|0LT?rW{o7&h~sj7!+j1WOtffIcW;;T#zo4Uj%HdCBdY{Wd{Ph6 z76#F#xbDga9$9H)e0bG)*WGXt*hOgWm#&iMCRDxO6uSYLmVw;^r7hCLo4Qk~zXH@Z z*p7!~r<&V`Jdx@zG#AE03|1ha*KbIFA6v9MG2FInLuP!qc}s%?oDkmaZyp%$xM1Ce zi_aQ6wR7Xx@L=mH!!6@2KJYvw)h!MUzX>Ls9O~N8-LNn>vAAor?bOt=dQOxY8W*(| zdIk%7*PME=`;?wV2ivmCI$BnDq?&UWngDvSr6SaEU^nWk&SG~RwzS@a$U6%;gK+?c z1LCC$UJfZ)8p7aX76^sqgP@Tl(xS2%$G8l7+ixU{EDaKl(|F#`ry54uqAg9sE3%@= zvD|VSEa4b`5=mrBXUk+`c5Fju%V36OS(8!1;7p46uEt>1`AxV9W2|mu!ugRE4Fijo zEixJ{HFWIr2A~kQjfYpX^6CBs>j%0|n`%!Lu_8irOJw6`-f+w1+iY2*#$2e2{q0Hi z9n9~8Txcn}Lg&!gZ6N=n0?Z;G3ipSDJ?nBhgm@4_4WdTal6eV{OA{ih?|4NJPKNW? zRiMdJa34}+&Yt5dBf&hmmPV?fVUUuyz40{E+gj`In75xPmm6ya^ zP6y7LBum-U4YCA^4?rW322<-9YE(5p)|x0CPGBfku#$dup_WZ*Es?H~gqa;)Ftj9| z!Y(NSGYJqS}96NN@U`UnCsb^j~ zvvqC4M$1Y7f3F0jISu2x$5xA`5Q!%mbF!#tjxf}>{lby1z3aLjdSJ~B)6C9T{kC`S zpStGNaNf^)(CY$r-Wxm=X79;_PX7C3ub@7Kugg`P%guEkc~i~>Zn>NW&!k)h6m;40 zg`jtk<^eN#FzPu$9xTk9BsF^M>pIxK>&TA%!dunZ6icU5Ifb=k4TRWEraQM7ZLwH? zyBRM=J1rr?S*~D2Y_})Y+2J9DRS`$>QjODogD1Ls@V1G8{e`yv{$^cISfY-BM4Pf`{DQDlRWU&jGs`uA_^*q4~k*SX*^xsA?v(c~Dlw7}7n zBUBC_(>eugm>;G_5r1RRm|8CCZmJS%w1+TQpAC7~f(hO=hIOtoZf76xM0+t(Pv0Jl z#bcfmWu~gH+4J95V7B-47S@hpx)2u6>`t^#w-a&y;Pj61bH`eiIIL@D3A^O*#Q4Sp zn=%y6>F-{9;Mo6(J|Qy11F$w`*a?urc#^1mEQoIYW!s){%5SYfCSIdMt%uO zE>R)R&3@ri80HZ_Hv4HQHT&Uzv%#IxAoG7Bzkew(KRv2blc{X@Wk(*_g1TaFz@0)$4;B9{FV?itQ6 z#5`Rr#D_a1P6a_j-a}aQk84{OE$iOg)zBj`wwWMI*Os%EgqX0dvPM&WXzJKs85dtB z%5X8<9CI|82VZP`k1nwc<5&r%^utZdVs;^i+(Zz%a(foSxA3i-hnvShR0oi;{*4_= zN2L1f>I=mimgJUq`ZvNKghu!G9O*xM_mZpEp^}ILvX$}yP44e&Yvnj#k2GhtuI?v& zLkbu4RBy2(BK>iedCnDpEul4>Cgm z6VAU-%>O;Ieq=n|#9@}7giI^O=>CGC=&6e(g*EL);!2XEZEpYuh~Z|%W8XS_TGsFC zZd=nWNlUt~n?`;m44<>AXMKmPxw5?c;K=4_wXt>2efry8KI=BUo&)2NC@XUBihEu< zi!A&~qFaj_EYlc`3}s6{zo@lnG}M1Jro`e4S_ak^uAAOyioCUasAu;gLv%!)bRAr= zX{3;g8i8n#!-f9#KkN^g7wa@2sZ3MU@f>Yx2HFMSb2`0i(+~(6HK5(;%QXFY7_Z24`#ieP@)(2y9&pRzdRFHD&jy_LLG zxrIA=+g7yzyr*=jCKAI$)Z&Fd)?#tGVJfn3T4Ufhw&R2hD0@OrcpdwN+PbK60&k?8#0O?ZEYHR8@G@UiH952kzfym<)3Pn*;n6p}C*wz@`Yt z{tNeAbyS!JM&Zb@yOb<8W>s`cv3KJ6g}s5Sz6IcnhLO4`*QTTf)NIK z`LHJegA9RUoxHrw($~)g;_K-@aR=i!S?+AI$pDv}ZeMv;=BB~0ZMEGTGMKmKXIY#!-Y=w7=;1eI8kV7?|ZEK4f!f<`;57;sqI)v?u4R@XyW^H6SSP)I^O+*Du%(GHk>93E} ze!BT**jra&*Pwu5h-hd|5VWBxrx&1Z(qVLZuJlhsQ0wXIH<%MQ^T|rr0`xbr1R-s| z*CvS!|H%}5J;QWevU_JCrZu;Hap|TX3^O%5O+Q|_k&9U-*S0IErH#D@PEqx% zUD=c+o_qA#Gdh7@A}8_>|AY?^Yy^N75YCAY_j!Qi+RwES`Bv8zrMQ*^LUh6Ixq}thYj%fuEoq!n zdh!gO$09dwej)yw#>EjS@dVt61cR01ulJeh zDep%d+=bCWtP_j{6WzguO>y)uBz;sIVGzJkXMj_t-hr|QP*aFqQsnJeL&JqW@daku zhM~gC(^_e z!eE|=#ks*|Y}6tn1CkbgWZN10*7w+|C?;2XsknFR+Yjz(Y~^Krh3KckTT>A12N6Jl&T;&=B6RY9AGUpt`MCBFeNR4X| zkqukb#q*oo5CKvFF^z&3dWMzbQl=x>ESU@Tn0;$U`=>j)mz~wR*mUk+{dSHOP3Dps z;vp}FML@>!Jz!ZDG!AG7fFct%jYmC0fBr3#X(0+3)pw8~MUbAmyrMhDyl~`G^2YfaIEwb z2<^mjVRm|o#}=hsw^}|{YaM<6+aix8JVo?1f7Mh3zMpj4T;x)nXL&V4q};8dy>o{E za9lQe#>M2VHKemV6}8N#3vMwW?};T3XKfTddBZ->(gdub+Ob2pPXs$?f~3~hPowT9 z5C^4EUXQfZ)|@$eQt^Y}#Rzf>bn)jXaZ;A|`J#y=O$Z-5S|37o@0Vda3o(<1?dOv68%$Z)d z%=TWjl2*IYs`oCdyGWKTS(1B`aTmr07ck8@HYTQ-78{Hy!F1x-#E=j|NXVDuCts)m zLLiU?jlTOmXJ%)1cgjk3{QJTem&$6TdEe)K%KhBW{mi61F>WcH13>y+kLe^Vp*)G3MttC?1Qi9Z*pmfbY$4oTA~l4FpV@! zqoex@TuY$25dEs^B45nY2IIHv zmAg*&*#3~Lhizf!H#+>`wcXFQ21B|Twj?sx&ZrFn^J0xp{3A4+Q~|Tcq6)k{%3z(U z!5@BJ%ivd#ccCyxi3>R5V;HRG6gg~vh!3z*hrkNCjX4q^@eGiH z{cUkZPLbG-{e<5;u?{~;tR9{sWix7Xo&xz`&U}n-h%1o0)RpVPx+oel6TIETCJ7!> zWOs8i7f)>PLxLG~qMGX;m9V5)Ytz)IN!@4gp=)-9~Ec|5p$az~$oQpK;t>h2aU&1LAm2B;Kczp#&T zEm1Sk>A(I^Go@%2O}~&^4p(1VM{exa!w%oGh=&L>IU*)<+{8@>_cVr_NA@mHD*s-? zA2Px5lpc_Gt`f0u!HUW;Tm8bl$9ipyvz&bEv3;5R7i->f`Hoo`9Ox-<^?~X)!j9qK zhUv!$V{=nkJ%sWBZjk29Gzijwho25rw{PZ|_j6XdFSoAWOx1ks=wWg^W9 zU9`lF^oRrWp54@-mec>Y7zbA#z4R{q%Hx*F+FK9n4_t3yB`_0Li7BvScgCIJFAZ`2 zuq7A=b+VJhY8(Xlnt-KVTtL14{lg;G7!5vr4b%!k&`wImR!7)COn`#bibhtEpiBVg zmlgNefRsL3`#8${y6$T#`gI>3yUPS}{dV)TH337{Ln(zRN#*g6#sQ1TtR8RIu zO9H}OMr0+x6NTIo(6ijCyD!?+U`Bd}KE00lQ_&HUxu7BY7n1vGef*ck5+)~R?v4hu z=(l)A2m~bQ3wK`FieI#nz3=~yAUw$hF4&|iTI`ihP`@{uaJZ$&c(>62D%+)&w<4{73GB z#QILecFSmE`)6kgf*;B#Ad_%?KMxQyf!NA=Y(z`GJO|H zrTMVOL8YG0!E$+=5Pmq=ghG-^qt6`=C2!;a!6_{kIdU=`#N^%Wm!av2gpe?)8aIB} zBxfA_)7iMOymu?SEG&a%P&&7lb+ZHJh zK=o~Lr31KA_n~)#AVbJ|}m|IrSi&c=b9Y3vKaUHbNZtjW7?^pxwn8rblKdApF zBdbhXqUKZ~Gu+k+76}`Y*Z`y6^7JGIY0cdy$2VCz3-}w6EMZxb{XrSZ%vL=A$2D)T zWZyyPHn_@tsXbRLXB(@(W9EA?R?Wo!B>&5mj-yLW{E7d} z|0E${s8SYsm)#ez8;JAL358i#<4OsnUZZLts@h>Je$%4QHbV9Ym75YUh$JV}IvH8% zK6^1mu?hHA3k^(O0LH|!t6NGZopFKQ6NQ>`QP&ccOLY_dIlK1$sj*2_@e8|iiyHH1 zHNP0)xu{0ESksZTWsM7S=+E=RZL8mSz|TBa<1?-kl)k(7H)jRSVLC(mwl+Y6+Xk}J z?3ilerr}JM4aT|7*um?DU#fYD$S=xs?v=h>z9Z;L`F!54DI}k`NW|*`P)`vq7t({{ za%)yPfrF!nMsddH`EdWhZ}e+~2oz zbQdGV47qEuLHP=duCt&EGvwy0JA<*MKUniqRJ3pmBr2{N!lLBhn;0f2lgn$I8qqrq zNeDD-HT-@yO5#xY)g;|;40FktY?!T)iHUF$1yVrK7*mVAKZ>0cmn{nUjiygu+DhW8 zv78yrin^$7L7I#8togsKbGRj23j11oBgg{^s#KoGFD7ucwD_6(f<`01^4{b7b9UtP z6+7C&Vvp4LjLgLf4-sVbBd3q{L)OCDdEfDU&5l;T4k>@nO>V|k_OC^1;$|#R;h}J^ zE4oEPGhRjY$4oA>u*Rfge{KxCkE@c5__Z`I+&&f1R7bdcLzrAw>tkTzUbRK@t}D9w zvD){UizQRzE&0h)Sx1$W7cWWwd!3^%tE;-P>0<-UK4HGdDRL+!yi^0Km$1zKVK>#H zd>uS=a5JetPSvWcj$yvxt+zpPg%OqbcQFyq}IRf*yT+MM1wd%%zv=U8o(nU5dv(Ry1$8SG$FlR3~Fm!5U(dj?opdEw(ImywPQ?)+V z@-`l#@z^4N|M5|WQ(AV6zx`3hj=G6{{x55Pl(IX^K;Lfy851oBK@XuGB%EStCFCXm zxU1@R_i+>dfYueG+0K#98?ORRfgL60sgL$1vPZk@u*v+k#=oEJm4w7(Z=fT0*#?f6 z6LDVK`_5eQP}Z`S*8azr{HWEq!z7f2I+c+wML84IoQnrHW1LJe_{wA476uXor1-i|LERdqEquC!uiBzG+C z*Mv;Z$MRolQ$@5Wl#PzjrjsevHn3@oV50IG1)EfPfepkj1!RIG(jxrJH4n06&z(?N zT?ZSWAYid`R;-3`Vifd2LdC+YX_TQ~Q~h*)M{YaLV3UijI9FXdjFzZ4c3CuPN#?Fo zQ|-r|zMK=Wl7Yu$gvNZj#vzd3_Fw}JQp3O{>jziIOl9X~Q9GdRI<|$=9jg(tg#z=B zH9uZiyfsT0S2Hc@sR#0)E^frN6Bqb$6XI~Fg+2jlJZ|<_Q^DgCDg;#d6)&teCM*x>x@;XkO0|8FB| zP8pj-zujoQxAWzT`ctN^_ud9&HQpJ#`&TIl05X4F(gqpCmn0nHT$XvYiS9@^wMr|JnINwn&3cw>N3nm8;4#8GU#H^C@b^TE3q zdw9(bFnrBOv%TuV&28k++OS{drMNV756>UO?n!~Uzt*X&5Qr8NES;xowF4`=WE4I@ zh*ayjt&8IlkHz)i+b1@z#abQgFEk)}F8i;99$-RI^WUy%4W)A`V%O7!5C+*Ppo!N^ zlc#V~0s1O5e2`Q_q6S~kQr+Ik<4>qqgc@WN>EUKELYP3zQJ@$Z_b07fZd*vu21kCJ zTbx55m?0mlaRB6#-DXfg1FP*GOlGoJdJyZ~ztst=L+$%GOY?w+$tIM9AE^0h z%WL>zPX~e@t9y(0j4Mp?|uH6jau?alF>9Gp@vkY4_$RZ6NGUm`;R{-$Hf9Cp!ndedG5f>yh9m!wzfqC((d=eX->U}w5+r~%}laRjmn7ZNBk#gI0J z892HtXf_}07{szCCXy3KqQ>1(7MQ)1!{Tbv z*SO)M3N}aSD~1<1k{QcxHJ&BM_x3hnY$M&W^mBCJf*1}y^PM$5Gx-@9dy>XIc6=MA zBlTF{4IgH*&#zjuzppP~FY4Lyh-52V@E1?`p?3}jm|C|aFN@bnusRn*9gO0R^uxHh zHpbcUZin6eBBdeJ@W+c0f+D&=PW*O(ZVF>xJah>bZtSn|Vcunvo6P{gHE1>{Kj_&W z>;Lp>%TTX8y`rzSr!Cnf@9Tgf+*;UWD2#PxQ?m+a1#B9J4=}MtUis&1Sv|q9V)B1#d?50}@xOA`d^`UyGDdzd zs-KQwx|LJd*wz#<}56itB_1@OiAUNV>xjI6*jJy##Npe1f~UeJ5IyXC2^qXJ;mvh70n z_s;S$CrDi4Y_9LHlaCDrhUz?s$68#E3(Yr)jhZ$To%J2JKs5&laYk~FsVv6h`H?KfWH<*A-BKh%Se zt9rBwddv+j6VXL=-ISDi07_Cr=1z-BkEc;1Y@5k_Hy>Z3v0B&^8ZR4UF1jVkn3S6M z?K>m1%O)RzAlkX(=-q>g`6uv|@F_lqbtv)GCdioD_UrZ@x5 zSy7S}UB8Re+7Eij0_#yy7Xi&_uDo1ceWy)vigC;=+#{?|UKjXCYEa z*ZtD(Nq6)BN2fjj-tT#9)#C1#n{;CSlM{*=_({Y_FHVVO*o;nb+unXMq8g&*NKFqPl$8W`@ZO`lw~-IX z`ELqq!|i0GF%obzD*#cOuf#B;lZ|m2d9!?%rGuN@*^iCxiGbSnHii`h8w(@EoNy#4 zhm(WB{1)4Bpk#L7;j3f*3qExSl^Zq~qj`b72HVd51^Y+LfBH;c4`TdUXbJ7XwpSk# z8EVToUsv#R0PDMkyzq~xnIr%oTo{IFK|L50Sz5-UP;qZ#$iwbDbD+sR6>L+z1^Q&{}f zyvN}5W-)$F%P1WkMs8Bzlr+Ws3N>u(8g!Nxn6>zFqQh1g`9Qz68w_vVJ!24e9`y)CpgZM>jK$?xvrBn!OG_M5T-$n1l#%A}oX~{s} zp7&1h_4ra8*9gtT3K=O5l>WiN+<@q2XjJRj%>-z;bh(i)~(pcN=? zVq^%JMA=m^y8^l_EqI?qR8whP95e>|ZE2mKR~wmB;29pnTWrI}I+z&C^c?M66$~KPG+5;}`OM?9uSbc~`(c&R1w9%= znPOqZI-hOTJJ$DF^7b9j?e<44=M=|NiQL07tax;|nQzVh<8$xu7Ae@;5o@>z zYJovb7l#jqE@4Igg%$UJ;+$S?q`^g}^oZ%HK+(gA2{fPGrt`7)iX9_DM zitiw{c={a>bB4dmUpo6pXWz+8>=Z?;9XR_n z_6w})^JA~U3ZOXqeOLMJ@_h)FUe_|q{1YuaXfF-!$}5rS!f0Bd;O9k}p8GUtR|iq0 zh59~91Sy0Z>hD^@G|}>)GB*eDR_GMQWoUjt^Ad_R6cdMGG}*7MWvt{^djg=~k6+ch zHr2WB(zcx}$1d0=`TZMPSZFY2Kf|y#kJWaHzdPjDvEvato%M{#N?{WE;M-Ik@lII& z?$0Io;2n+`S#vM1iRMqk>Aq16Z5o^f>L;`6im>(Vg&Zo7swTS?#2`4WJC1brTJg;{ zuUE+0G0`$tO>z=3CDVe0M_s^qoozm^M?R%lX~!nZDDO#^sMkq|men z3SJP)f3xxh`~_=DtR z?1?%h2V>A`52l`*|&a#vt6D%2qWcyxfE?kB+s^!|7=cIF$ZAhD9kK;H+o+Ir+qjL~=v zjzk%U~&>NvO(34wE()v z>B6zKBF6`Q+ug1*e%T*f5>d>gBoS%agvphUNDIM40w6!z0Zwa?b~aVd2d#6GcwPlwJDq`aNf*>( z;_9czUTBKww#=Ce8lc;@{AQXYhny!#7pN9u#E}%UfRiYtnKSNGL{ut(qaUJx7(7gBG(3} zxfXP@grzDfDyQLsN4%`Fr{ED+W0okqauh%rTq6F625>5vk(FrvZ8g*MjZ8pxo?&nTvv?$Z9P%r|x~ZYYCHQEI_YRG- z45xs+>uQ{FzFo-K?`OUZTcRq}_rr>2l z78(R@k8fmXc3$CrlXJ2eW^88q@E<}b9ln&69FR?8ZK3QU6?@?mjaEa}9*%dfN_MCd zQ=v72Vj$3v6gS#rN~Cedx43G)SO3SQ)Z%ePQN`do!;%G2HEZcXt;+!`IIfX_wzD?GVE2Md?y`DQmKfoG;c4%huL zRp^+C1uV$j^2zSpKGOv3qz89f>o=zSJsp=Vc%LRI26=2cid9IX^BF6Zz$_k29!<5DsWcE0is@G%t%?rBG^UJmz zI1y8o&{wNFU;byRCG-b4jH-xFj-BK+Y5Rp7X18?cKXPNXguaL$a~Qpl1%X`{oa;|n z>XmydMGXDdBo&}80fgs%}D&HpmN+qRb)_VxIvtv_q1B4h-%0Q!z!m!0`FJhPaImX z*UPA0i39oxQA7f-NmRB?dXSlH7zvq66T>h$twAI&RnD%?eEZy;%pZ;=hS>_7v`b&J6YVHu^5} zo$`GKH$kB)#9;?fW5sXnrWcDYt4Yly=q7_Dy`i%s+kZ7koiS0)i8qxH2l;0@XQQ zmX1xtfL2_Zu;c@(=);>}Cytm%^OM zrL@2~8vv3*!M6t(_x)@Pal9F`!CIzhmfx#0&XKEh13?7(FSOo#S&zelhPDgSjMLrtjKTjl)DmOf2+ z)A)B;G+7CW$sLnZVWRSW$!< zM1jZKU32!w>@mFE0Jwl{m<`_r&3lTyA)eFIua1^cfya4V4}I;04h9oMIqMY!p&$~t z(yg6iT6Y7D_U+rMpl0NrI#=~LQh7Vd$*LR)W+0p@vSLgmD!$<^DODaQWLqJ~=_v2;FaE!G0!s#=v zN5%{ZP1`W}S1)lxrdIL(kgb(np!@9LdTDxB`_*0l#}5$2*0sUz+$31uvF#y2vWAzo z=Km+4Vt6t_UZ|DI{d=XW`(^pdlkJU~u%S{q&Nvc%cRve)%%f->t%>D^BVQjz%I;QBAPJ(a-$E04 z+NH%~1*ppe**Dfbe`iPS?y96#-rB9?&nd0_0XI>f9o$N+kGy>)y;?bixrAuEg*jq_)t6AxK3BT|fe~y{dP^`!03!6d8AuLtpk1-v`RG6z?XAS&B;ls$W7B@Lm&L_QGrO9HAEO0K^D(%XmC1P@%px2wxZ}Ds8lR1Vz&XqCal?^s6i6F{${KOm!q+b zIaxR<2_cqq@|QvLkTvT+Ic!4oUYux2`bY+kwG5cJz|CCH!mU?#Twy?bQI@X&mM@uv z!{e7hOkvlwX+W&d5PT5ZSC!TE-kV0~bBpLu4-@Gtl_?&nz^8tV{h0gSqTr^jMD#n1 zH}^K*cd=c54xR+n7upPOXmLBiG>6FzBa($tB~njF(??GU00N5i$6oT{wb@-i7_w2* zoNG_`f9)C1wu|R;tv#XME{SzE_1Q}ELSW1gnc%Kf{ZiYm^^=Jx)4QjmD+^3`Mmr`* zyD<;f?JDO#^y>S850)^1Ofa)}0I*?lQ`%Cpi~1Q9i*$Fis$R5zQx1cNQg;1dOYpcT zm348z`Gb>QG5@Y~_EmK$;Cx+r_7Ch4Rz^16>bo2kH^0o7yu!fD+*l{i&4`R{gBKwy(uP`#z!@e8b`1pwLd5Q>wjX|00c!o%cTymJ}Ku16`bk(?q zTo%8_yZ{o$(m@wb!W<9PV35-`ibehiSO5pT_ zn!oS7BgNlxN#w`3tZys#WzCJYEh?@iKL1H2N8K_8h!O$9Ra zm{Z_LbJ2bkbyleiY)pZehI8@4sG>8Q&NJ*t;Pk!BehZvF3!If@$Rmy+$9vBA2QSZq z0JYZ$gFOy3pL@7K`^u$+BPLN!7zpu{_u_KG@Els|m?h2iy9NAN9*=`Q1D9b4n6sQT z=#Awfpz$6z0H#dE!RUAt_ysL)+3x;?jP#(N25Qp)N(B(RXJs&WN-Lk~Npr-I_TQsH zX9KP2v#uFLkQP^HqAejp~>K~cG_$+4leZfI!g8#EPC#Yq$<96?8(AJGdb zlpJ0Ng06ujE#RH3LBIAX_*)O3-2&FhO}_W|{sG>WCO7AR(9$VUpaHZyj_FrSkF=Gw{Fa}&pYPsnJ>aSG%&;m1>2cI(N6e8fPh4H|NhoiUdpswdV`ezd`$%SuZkvF z14M-A`gs(=E)Kl&?9W)pQUgn*vHd#V9keIosV=)6t(F`xML_n2XpYzk$5iTR6+!+| z{2w0iG62dSD43eZ9uVCO9!7^Q0)7vf1BxZ@YiMi-b^+t2-3KR_qkq=LG02P4%`Q0r zl~oJMJ9GP8mluKjf#Y4PL#+o!vT+y5&;N2yS{9a^>{*|rjsL-{$hWS_Iz5Rcc* zJ1#Adn$SKZBV#dtOJ-Fd!0-^+GDH)bRe;{-5DIDE-xfD1eSV3zGRU`D&VB{?R=dlg znE=9j2dduB`~J-L9hz^|=0w(h&h~t>HLRDU(rUA^wcH?ddT@F08<1QDF&957@<577 z8^A0tTce@0U^fHJ#f)umc+BO<7pgR7uoifjAFcGFpN(*TSC)Bwj%HrJrXU?upuw5OOuG_@6Zj3pt$>pW=Yw5-0X(V2&r#N(+k$G`e(_)!D z>qGaziE@&0$^db*tJ4Qb6A~+vvt@&2Fs`KKY+g%X@KcFfup|Ud zx86qHe)c8y8Rmb$xNgPd-j(R~z1R0CSkr$~d!o15QpBCh)8(?BO~_@k67+iSD^7x!wbbXP zo5coQV{uo5Vja9Th7qv(+1C03U#V|%a;U&p8lQ+*4XctaGxPh8v&x3;Hg;lL*!=KB zv8jYiIVvlUwMdv3WLnK02;1}i40ox5+YMlYsv%oq+e9xC4$(oE8&pHt366ogjA_@2 zW+P#Ohpzaqv$KyFTEYR4>X>EC=NMn)Q|>X9u|ti#h;Mnx{4+1VM{Bz4NLN2EWw)FH z({ppDWhK}3dQ4Am2IEugX_iOclXN><2Yt6wwk--k4e>g9lgDq}W(!xz1S@YxKwDGY4ldh?uKbvV1ED z-uG_23o^1)e~o;G2q86_MfcKVSXCvfgn-hNh4759ReZc2PVk-D_HlZtE})9?_Xbom zRRU}N%AZ?dFKkyVW!q&Tk_8<$uKCs8G_NYD{ZYtP;);i5DG5PDi}VAkJT;HZqqpw0 z0&m%V_OH=vrjs>GC||}#44XHvZ#E^2?m(w5i%KIG6N(X+YXe#W)%|fLBtQ^#0fa~A zPsbyV&$zVweQGkkJV-7R%qagLr6DLo4NZ9Rz3jBg_vmeLM-Z zHSCx!<1D+L*L9ibx6{TCf^KoT1Tzgpu;b%yP{3N*v-Bb5bT5kg~|CLGn*A4CU0k3 z6@|Po+ZnZnIiY#!n2j>hV(CyC$al<|16b@34f0NF^)VRgi}nC7xSx3uz9^2a&_-9q z1YIGB64Krq+)S>?bxJO24hLNv@1pYEUMdR@6dv+Q^a0wHaObR@k%G`8)B%KH`jv23 zIQ7DJbyMeovdh73%)K3od(jERJ!+uK)&*^gUy%i}A*pKEPo9uWJvLw%wqe{1Rk0-s zG+UZtx2O)J{!d{=l|%NU`owT6@pGM*yR%wX&#?TrPhXOuRk$UUM?Cv}8vECqSuS>k zEUn4;MSpk<{Bb4v2>okqWBYv@b!%9W%z*Z}Ktd&fWfDYoIDI|DH7&tp$xWTfWM|@c zr5ABU>8`M8V#Z~~A)x1=R^ffi&whh_mbJhg8-ynQ8+V@n?t9UAs{>WE8J9D#O=J+B3X6wO;Y+kt5fP@lc~4iAqo zSCwAM%-NW-8L|9C?U4&hswrmZU?eNCWA2F-V_kR#2FYN{H2j5k#A2- zu^1v`vCo>YixWB&zN1YOzb?I};$%(rCu0^^%T?w$$al(8iHf5ckqF@hd!4*NUTDCKs^s2K(XfTy8;7T#Ut*9Fj2GE$(AuG1knsiad!M$bVrX=;bEQKF_`Y z8!^GnyvBDLtcrj1{ACg2qjd>CKgMRco5gCW(qKqwE(B?BK`##jH*`*c!gd%tK(nCs z?AohlIpqCcz>G7oq{%x}9eQHJ(*<)icIcTmgOg>Gz`$CuXSkI}O6RD!VT{F+C!TTG z2iWXrs~(B3>cbitU6WH;exvVf%{mQh+|J#+?!i_e0C*jzp`{&7kc@a zUy+n>&>dfqq59}*^)T24rdXEYPOzBMb=sT}x&5!)%oN{+XJ25yIz1P*8FOLBkg48} zsfy2gOsv{di}jsPiq156{f!U}5`}U(ML_Owe;r;2IEhFj2QU=KLbC`YXjO+tsG~u& z#~xxND@u3 za_GTj{Bp=AhhC+rvrVW~8_FtGY*%54oVYg7(B~xjM?$o;wRKdsF@~pk0*877Gk&;} zG&Gqe(698>W-6C_wRd5&$N0|c*o8{i6=|FHqZOw2&h2!EYXVLXOLM1$=MM3m>v1Bc z?r`h3R=0jz4PSSIJ8SYvs<>%V2Z$ZWiDt1n+?u9H>M-EOrad53Eoq4L7;dkixO-um zb%7q(Owz^sr#TntK5}7+brCe0WN!@i{A#}DB3I%B0@h5f#K)T**kG!sJ zR|+$)s7DT-{Q)>?8Pp?<*oUwayoKu#b9^-dg6nOH5rLxWG$gPi?w}5mlptkZyXgx} zdT9nr(^OhMdgI0v5`>5eRo$c6E0AB>SERv(0X*;CLN{R`U{uk^3l1;2PTrqgOP%~z z0#az@7NRcb?DnhH;^wfxtzPbjEC;{zJzt2kjWTvP4G8kM%36KP$G$ zMyp}S@%ZT~&RYl)8;e zIhf>QF!Lofsrbtjio(mwl#J$A($tNh-uPF{SA7C;-gf=SS|6t6=hPeZs|~8<;xRJm zB@EAZUXnRhstBrRM&v&Qh_W!`*j#gf@u#I+zMcjIdAMvCv15LthQN6K_3W?QxoSEq zKY??qm;csmJ2C^)ezMYh*p5oRwML@wp<1re$KY*!ADnst_dE^-D9pGZe{$iY0@gHK z;(p>i;LQ~A7Vo`KRa$pyiGnTcLN*NI^5Dttwc)AmLtSkCno2N<;gq2y5UeyLT{?AP zYX>KHbzXc@B6rQjJR(8PA6%sr@eWr%WhZ9L(CnRafOx-xZzX!0$MAmX4jGyW zY(OX7H%yab;5V)>3t?9St>CAx+x3M2l-(fXH6&A{maNZwX8fNGgTko@!6fRF-!P1x zWk)d`9`g5&Zf<9e=~kFGLnyvwYgZK=AM#Y?!v5de9aGU4c40G%FpGN-y%(F9KwfrMhF1K8yDMT(^ z7)V)T&AWSrl=ZQqxJ#hHFL1$hA=!2NIDPDNA4e+h<9OLI?c<~$tIc&eRzlxSRyK{M z8G=sK4kY(Y-48qP-qaZU&L-5q$LUsjnwJ%yOtp74!wLd*|WK2_M7@YWe<)tyYg39^$m5Z?lN(#m&@+sh8b7&Z)MNMuJo?@O7D*R z+>^Un>z&nG%k;1oluq^H(pkG__#M2_5s#j6N zc1i9u-T<+$1JIw#>u|kd>H6rsAXLCLGAC!IA^GiO+lB2-GJnSd(9WgORVt+a=Ezxn zdd7iPwf)W@q^h_WCa_gytE#3Q^xK<$F*{jFo8N8O+}NqbPVKV=PW!10B8V8uR!1Yr z>e6{{D&0c4SlN;QHc@d|=sK$cH0-h#tIA>AjRTDNAAld)=vD1Ur~EG8M}6P-{d1MO zLEj_{8Vj}^(tacST-nII0sX52SE2!zNNb#G5@a50!-E7xC&f^JxWFG21K#5Z!Nt1x zVwp@CzU5rKI5b2(>u~4sLDlgjYiPFzZF?&J>BT!-aPj>ykIcN|GhbE=O=TN;`-_A8 z{hOx;`4?{OV5FY5i^ks0Ys_DS-CPMJa%(N!!Y{?7K3? zRyUXYr_+3pEQ~;RF7pWE@pH6K{6!H~$}XuaCK;HjtSArhC2T)O$c9(iI+jdVZJi)7r3M zqHsmPk=}_pffKl+9Q^binq5a;W~=%kli@ldaKqS&7YX>Nf#ap5lWr4 z3SCh0#dA8lX#++s!251G`}fdq{4cj3w*Zs zDMd~OSjmp$M-p#@ATL#!bqOFZ5XWmk{1-^Cb@+E!@{1tDZY!LMawayD9tDwRd zp&fy2gI`;-mc>fUfHs(6mCPlvFxgl*GVRJet1}v>lpUE*^99a+hy7=^5pOXAwgJu1 z-{rD!U-12_@7KOFw9iK&Rhp2~mK)$MO0#jfhm;(Gys%O-&v!#D8@hHtTd5?ZOCA}( zUQeWu@MHMXuJoDUcp6TWC_qzzmSSl|dwX@W{&V*eh1G}>z?*}D7U2$CuD@%{B4QL-cD2kSb)lSTnrF4P9miZ4TerOFY9%hC?{b6~Ot)?jO zT^HKAO-z0xArnnHBiO{g>rP2{0uvTOqDK{CQ_QV%bIbQua~LyFrZX>Ad%xrmwjd?J zw~K{aRj5U$Sy>-=ktA(Uh(u0i?dOxbb~=3axh^gZ8s!g4p5H@FUu4=jS$qIu^i+{v z3$a*f4&+#c2o==S+NF+ONXb#rH73OTtE>jONI(#Y)7Vfw_auB}pE4KZ+EMk16Mrnp zV#9FS(y?_IAFq$r>b&nLFxeXmn9Ue?fo|?%av4nMi3clSqUwI=mJ}yFEApQ z%;2rH5L_}|bm=pdck)wJZ|*;4i}KXKd&xH{s^*MWJ-|eno(_5~3(tO?{W{K#W`2FZ zH?9QQ^&HJ@Jc5TOnFtcroEPpUI~dTS-VgLdp-$PyyUSqc#24IY7*D$ZLl#?K0)R(2 zhPjH$)$@1!&80y~a_aqD1Jw>6Kdxa-8A+6$rk5qbVjFjw3V zs4nGub1UY_hj(GQsyA``mW$gvy)If0v<;uCD6{-XA@#4Wt?0g+$+O>LuVH?MD3F2X zz;a+TzNQkm6%K)NJQFeo*a+3KidQvT{F-?tVDX6@XIR{l_xKyw-izC&V$R7$k>DTB z)Hu@_D%Hw+E%v*K~1jEajit*<*S`^ZLDd!8KDK{HX{z87on#7*n96MlO=CK1NoWn*E;GC z&UGHY^gz;XQLo&b*e;p=re(5XHsr|HN>0FwwA(A5^##@AsAtdLgD(BgTs_PSz?42o zxgW1U6SJx<)5_DU#^{Ld>?eMn8kl*x)&XB%>`qXj`TS?fY?&zaH88(CgGF_+;^9sd zyA4!IJ~q>kW%!D}2Os)pZf)8N#kf_-C$9C~0xbX^RKmO@n3m$zNoj7rjDrJV&E==j zUO{L&rA*Dy$E;@Gvyb~bRVWD4G_&+N%0T#g)I^%5^J;=SE?9z=~3a;hAUV*Yq~&qk`^P0V!Ff^YTNzr@-92wgb3 zG4ulL(?lP0q^OHB9}3N!DT*q-!{fA1cX3K>fM3s(F!RZo=h-4EcxB zDP}63;!PD1e%j021O3a>Y(rtblB#-L;(HJiaeq3G{J3U+-G>1bI0V{Weig<8=F?iH zc?qq~YO^FBR?$^ zuyN6m@=+gDU>-%6BGWU*eHhH*)L0(#-e_-en1?6p%Z7RO%s%|ag(9qm!|&mQTR)$fjEi{0^Y`-TYMk( zJ&hBr1}3vLU1jwLgcfDZ(TS!)gXLUa%5cF=lH(O1&W_7@r@_S3}Du9`i^s&%JOQ%DL3Ccjl!@5Q3RdfbGz@e=%N*S#x=j%06=I9`JI6Mdd=(W@r zj(fQPxRnbAEQwAF20T|s!GM4Bb8;`_?XUD?U5>0z2Z#925ZmL>02;?umlMED zW^nJgV*3eoMmS`jYk#T!BY3`yiC4b9^4Ifg_xx3z)5c(F#eJ1z`FvY1JvC1n0h9Jv zpJzYJ`~n?=g}wve$NJng7Ij_3Okl4-_;J_i)%F{8sDGrVf=4Km5J9hIdLd=n84!iM zzs?~tSI zM;%93<}l`a@4$TTXQ7Pb2#G8^nFb_u|fqAXyeAAKG z+zRq#=$g=V^~C`&zA#xy2P=WW;t!yo>+&Hfn;GOh0!brRR0O|;SWqe>ir-;AcUXcE z0f>=1O}6n$DYqA^-ym4PUt(3Ndd73&BmD1ttg?-yBC6H+6p zY^@;IO=q)(iM0I2+LjPw28vK5HqNmu(H1ar*nqGxBXaE!IClJ0E?j(%C}Vvr)-Dl6 z3;YF_(c)>DmstB*a&*B!aB=Q}9>Ql;j%~a?(W7sZ_BYQ&I3i1v@r{E1d;BLWOy zX360WaaP}9fyP}q08FR-sgHDjZFm649Q~PVTg9Ky-$KU_nC3k4hUW?bFl5d2<#=N- z`CF@OvtX3sow$2Qf4k!Lu&f)26KfS&Ogv<8sy1{$wa6_)s-}ex^{IYlWd#ROmNFDW z|JPhvvgn3LZHq&;91XUE6Kd?hWSXXIpj0P>z*m`&2Mm+1mh;1%Cu z!`Sta0RwRgRoH$Y?)PC&@<)7M_Wdo*R%ut>>q>juLPwC9!upCM5Cu~#0J?ldv5W%d z>v8F5W0Rtnu3M`UWSXfA$+Y2Pu+ZJoIPDb&S}^Tg_r>W=*Q)MQ%VtTU)Nz8j<@2PysZ?+3N7uwaw+A36d2{h$$%gPVjSeAKRP#hoq3b3 zey9_YSBYzu>Afu*(+c&J%Ql)86AW0ZJG-Y%_DLV%otLiA;uVdaomliWJE8a}+mUWws@$=c~!~2fF7Cy=P+&Xm|dP=A9sw%qNIveS0({5+? z@_MF~ijZ``b5nbBHEH0{@weltmCx{0g~{@vO4mzzXMkj%<6IeM-BWSil=f7d1yNhk z6h9}Xvjvs~rDbTjn^@Zv-Gzr59y~x~ABVf)4ON~f~ym!9%@~ngX=Zr?k(9Tdu@e4{% zI>ez+F|k2bl|Rf$P-|+;rM0%yt%4qOetjO8aoKr(B@$s3?yCuKiFJ9SE<+WKIDB`e zUzL<}YfID|>~0G5Y+Q?)cFNrqpLgwf3e{E>+~pjX#n(Sm@ZjJ8i)T6?z=aqy%(Uid zMzoIQW=Fz3%i{9*)r-${d;B-69i!>j7A-P|JH116W9?2l8_)c=8zYv|jT7~7TURp` z(UZz`yBuI@g)VUzcIN)rEiVyA>P>^y#A+v>W>iOTCUhedf9T^WEV7RAV{hdAPs)le zk8zSp(32?#2JD2M!hgD8( zVKMG^W2)qP7Vr5JtmEn~Y(?kcTGaXP@_nL$H?7}Wts~7}Z6Y2uzPrtWid9qW098K) z0zl}Te;Qjpl~PT?fm9kCCU2Yre08juY>mGNfDAJ04Y z7SKVxm3A8&<7Lfz_;jxeH7I@wQGDxadKi2sUsY!@oUjqN~hlpVV*fX^sla@Du z&Hb?ozGRqoyK3{?>lDP4$5BYz%ZDWh7CldM#>24ZIZ0uCH^y9Ex{yMlD^}ymGdIsI zcJn{2F!?|pB7WJs@6c1XHl1lM{T)^ORfPscrYfsGQ=P*h&Y4&s(1C^;AT2;la|=vKmMMuyfr75DO5rw z%AZy*Rd-f41K^x*K+dy?`Hjm%n}kx$)wutf1e$ZrT#!`b&P%IcFk&`6aN>#68iZ6< zt-E|)>4>TNWvjTH<*rMB`j@t|G~QX86}D-9DMrV@Fo#tQ#Z4-ZtF@)FRVs{ubQ^g8 zBJ7O(5=-}+Cy-%`VSeKM)(gnTA3`Jc>WM z!n(1+fILHrVX5n&O(ZJw(9KX`c>FnD43D__zQ`4Es6*Z?4*fAeT4OEa9BDJ%>TxThO|VAx8#s!-wQEi zNHkzR$4uJ!qiw@cHQtf3I$C2&w7c8vOD@{b6zW{jJGi354nz}%s4%C$r~c_@SX@{Xty8M!QIYTYP}4be!ipTutr4sLyD=jf4b-MNj|t{*$PwVOE@${l>-@ansd zjE`M-+p4LncMZsYDZf=1YhWFYwRGqbD~@8mQ;SAe(PDoe3dpR$o7PnjyJF3ds58nT zoek) zYYye1Sl(_Z$T%CC6ndx03U~UI*TbFiR0RIJZ_-hIz* zHrN1<0V_VA7kxgr{fGYu1Q`t4MlM-8G}5zwvB3s3D;VloJ+N;3ipAG2P`kR!^pdEW zg*dxk&V`~&vze}r_-kzozs;H=ujpd`eanUq^>>dBcFBPzS?L;R-nV+m&Eq#f4julf z*$nXb0VhzuE<%HB?tB0G>%lgfy-487Gtp^qIQtaB1C1Ct_xc+UNnGLw4a3o*cQqnQSU z0UL6SubG+THNu9>20}i=VbiB*#RLrIk8kV>aS+UM3H7*SXbfoAaZeM{6oSK2!PHR5Zt zP1@H9pEE4R^DEg3c7xp#t^m?>dU#IR;;WQL6p~KTL8eUxY5VXXO(#QP8i_p1fJiSx z=kKX~7n$Zgg{s_Ztf5jCa1)HuJTft-XUQo7%E`(&n_puQZTlM9w5E)1+J2Eo1$yo*=^q zrj{IEG14mNft;KhBd9AXCNhCzdZfxM1?$51+t#Sy^B;l~cIhgIf}Ad}*uTC!Tpd+&=0J@1Eff zXz~fq0B1lPG3d~kg6%18`hl%muidSuq3L7WqAcn%D{}uSfr@Cv31qq1Oy6*?#m3UA z)7;b3<$tYjom@%SWRl1X+O+~n>&P>em@IC7!y`|u@rPM?-N`%ezP62GbMXrI{h;p& z-#1dVn{M zPR0h>#@D144mNE}OT2*XbbU>Ii#iXlS$p)tv8|nJ$A$;mHV(HgZm}`U9zgHcd9BUj zy54OA4T~G6Ci|Ck?MS%<{k6@`? zqeC|0Lac(FJnehw^p4uIdHNvc53?mzA zTD-b(bYjJX(PpW18kPE&+#I{P)Aum+X0hkDX6{MZVbzh!%DE>{&T|AIk$j?Br8WV7 z0ul5XfG7Nk*FB*{%XD>%YKGz;F4&bIUd=+2k0JKsfnZ#0>*mA$XyfH-I4rQi7#9o% z0>2N2gK8|n8*)$$+-C4P)00ezVlK4`$lpl=(M z6TR5|Zip&c#C{_KDdTg6Iji7dnl?`m^UFi~u36lF@s_^F-@W1PP0WEvX3wLC*1ze< z;(d=D-uw8lB#x=TKY}tASTy-rzZKEvB4!ltu4G3~`Tp{CVMTB|Xyg~rk8=BfNqJ!C zAnF;m{#U)GRundJK>}6rsFLUbW@?H`3Ol?5P|^RJ;_t}G^56OY#eT@)diFtnQSU3L z{j|>ULw8`N<`=J?{VXD0CxrG%&Z!Z?yM>F3q7~5Ak%n>Bb1}4tK`P@Wyspoo=}!)( z20;`r-=!W3mR0;I4QbW=lN}dZyZDel+Hgb(g`+jN$FUn6{^0QgCo;0hi1V?- zx%Ks(zOVXz`s&%?bVu&oqhWIqTPa|3)(>g*<#?HlMv}af?poxx{Xp7nYCY{%muo>@6_Ko%CsprFxmmYl`c)oX# zmbGJxE}v@d()|ruOQv&U&-h9#)6OPXhU1L&-9ufuHY29-O&!@yt0#7SxxV>T#CPsQ z6#f0{!v1)8Hk5VKArmk%JewO=-CP~>Ycyl`03HWPeE~+MzA{0uhPH?z)86FkDPj8~ zJ|r`io+LN061d37_c@uf8Z#>37mNkuaMxfA4gJF^9>RM+d z?D5W5H%7k#--u!WW)?=hSZI90G)*Q*gR_T6)3}eH&GebH9;GlfQiG9}`u>q-iLX@^ zapv2m=qT&`0fb)oLpiB!@+Tralc}+eWJg>Ns{pjqfoRss#;K+$($0k!P8LUH9+nRbd_ikP34~p@)$wJK75?`)s zhNxL9#pnm11B7YB*G0_=G%V+#;NZ`?28c-P#%wqJb6q4YP?0^p&WtXfNi?Nn!L&ak zOQE4fi^f}q6Co$iWFdj%O*@RDHPQNiDw-J00bg?y``X^ElRLXM^}mZ;F9=s1ICkfP z{fj1Un;N~QLkO~3P-Z!fUw-^#U8Ab(+n&MJo!5m$4F^Wx&fu!(er0su3eYzWmG%PRRQ%B}s1?Eh(b0w2`!vO-gEn2paEi45x#3tp8YB zB--B`ZN}1OQTu<)y$776b$vfR->1)0XP$Yc_rAT)%-;69-Mihr-g||+gS!Ln4h{qa z6a*9yu}~#q4XCjs3RXnz!(~K$YLx!V@nTw+ z5mhjP%=;P}N>;Wb8cqpBaH#m^pxGG=NJzD`c$H9ds=3qS^)OCO zDF4JLrDGn9V1z^l8x1V17tw;baA!v}=xInRX4K3moXG+SLloSqp7W&B=+^DE_oE!^~fG$Bem*fwV?Z+PdVTH4%Vf}D3+jF z!l0U~HT5zwAj={VW;YfWags;(J;dy1d7^{_3$WA9sjum!Ohqk9j4H`qR7vWMx`Iknf6B+ z6zu;OG-|!&6rot$o;=B|1Cguf%c{}X(CMvZ2GnZHDXR5{h;qFtgHAa+;IStXjq>Uev$%N)6e2|Gc852O)3KCxhY_jy`6`4j^oxSfkWby)&Nt0I!QdD1h)#Pmh$5(zrHdJ!!N5>mEcjSgw?fLY*n_>wwcxuon z|DN+T4rH62{_IhDiR2mo$k^b}rK3v&uGoNVXqS-h9m9`HpKBc~w_Hm8gJfbP$Y<9S z4qQ*TZ=F(H6q`2(T7Dk1q|?CTj&1g2b+!70`$b)ft}_KCK@?CjYjp$t6|-3sO)Dj2 z@pgqsnBS~WI#-|_cqm=@=_Rz`1Rtf1xa>oXdiF@~L-IiCs%mmmwVn0T4+1UN18WToRPRJJTY?gl2<%+gyY*9(!2nrD^J&hgBFcR~yT^L+)J}HdX%i6aHpp zfV}us)?>xV#Ay3u6N5+0lEaGDc5*@~>?mfu{OIlZgh0kux^+vpIC=EYK=~s}I9}69 z*16@0mKVw=zfEqxQZ>~5<-2!JcI{k34jjg_?dAJd9v_CBBygYK#Mvf+3A`FRzg|i2 zHCOOd|NI$truUytQ7}Gm_4F*~8Anf_nSPLs%8E@A2n&8< z&?ZlNufZcogb5%bzRd^J-Y8+*MvM`cj5G$_s=|wLXC#ytS+4x`zR~{BXlB@ziaNuV zk#>65HAUlVb=JRe^Pl4F0u#V=8{=|ES?_pr-$cjeu7=HdS6iV2{jIE`df+6GJ;!9X zoYuR15mnEq9#RBS;XSMWS;{jcq?c+0n3_e zr(gs2o?F~_oB{HgSsj!IfoK#VV!jxSqPaX!ud8TDkPoU%JkV5hR@U+{cZ?<)IhDeV z8Lkkc`C9sdyOv5aW)2%L$1j=c?v$h$x+g7`wbJ!1dxn<%{qZiV!Mnb5P218pA6fOf ze?9toEzbD}D>`LoVe&1{A0-1{j&!SGon;z=fxdM4$Je&z^;DeP*BBjq=m)!O4O}Mwt)0^4%lb<-ExVuf*@>6p6*wT&T6sOOX-iWRHJV?9)Sh_fU z$efK4P3wttrv>c@vQ;wG0aOL}Au6RVXuKSTR~z<}R_!0YVl2JF#b>gk9IF`N>#iyK z$5xF9ESokX(TQU{I|{8UcW>)W8Zi?oRV5l986L6nEB7CK;|}sfp&Mc@B+}|w=h08y zxhCb^3%N%r*VpjsuV!2=d)7CpW_N65=e}1TUEk(UgjrXN*F&N;I^m!|GoKoy!hKiRjUwQujKD-@Tu^OH-5N89)Jw%bqs z`-I~11@v@VcXM`0xa;I+@GALH4T=#&;T)7EKV<#^SnYd(26Q(pYl?Q#^bGN|c5DyJ zpuL*bT)^Dj*lZ0HK=$Pz+#A|l3`%vJoNx9iMoJK{A=T+`UfYxOO?V{P=jwiGC`Ffp$)sTYTH{7 z9%oRcJ9rQZxUz8I<)Hj)JHrC&#cZq3$n|Qxr3;a^34g3*k56_1&pPkZiLDFDNVzQA08*?@k=;>Coo zT=cZCx{MlsF6Q_ClS@stnK_pj%8~N#;-+Y*66f+OcahDKjLzJ!?6K4z_yXRb5quMq z-EHVj)=My{K`na zBYkvj%rZYmytf58PV~qRm?k5+lRBMIs}9+%;*pb7K78b9V1vrx+DUCyI>p(2>2A6lf@N8^pwzy$yVOB-xOK; zrr-W3zGN8iAoc-QUZHUsuD1CAXkR#U%DF;{PW(_6Y2*F&BmA=sHTAw|EMhwGJ1ZJ`sl;l~2bw~w+Nh?fGzZNWxzm2@*1ix#CEGSXH?`Xv<&}!kgIAR6E zuYFTL+sUfigylD~qT)|o2}mx6SiZnnV}G!qTV6@Py|TiOxgF5j79UIfIFiGRDdss<;kyYBI!E5i zKf{qhXZd+ymA{FU(gD+}V)ckn{(K0t>B$gS%f&y%J#UrQBo&DfhungpF$pjEVJg7M z9)Se^^xz5p#M?%kTKM+I2@}-~j+aTul=zMBO17?cp|HHic{|L5Q*G&TM%0L@G`@vl z1;OJ|7%?YYZaMwY9;>|3Pl{%S z4C3jYeK`FpW`L@CsC$@H*aH`PO6;tH$_nD_Y10AFs&@+F!Cj?zny{#V*`EcVEhkdP8>+GwXLpeWB~ghIr5&@rKIxs%|T4$sq%~Yqdbe$!+0wVbCEIQ{_(lg1_)VGZv6hsNbDxO#$45P&V`{ix8&2m;g3bxQwiDwMjH{jbh{DLepdd41 zc?vc#D5#83{tu3k%z&gZh<&VxTR;N;wca2r>9Slt#4+o55{xK9?9?6(>tpC`*+dO6+Ge*G8jWtjrX3H;cOhHw&_NK|SLutOnz`hDyd*bwA5gY8kh z*!s8%@$e?>hJMiTF2`448z7^I4XNzTNG`+|i|D9iLl_YY=0#ip-Am&E3iwqKT8ht$ zs0iYQbAzjxvS0R>3J`o-z~_9$toVEU^iumb0w2MSo@~*!L&@r2fmJos{i!-Yum)7R z5p2LMw?}ztkYx*VF0V6m>IKRILSJN0yj8jBQ-Tm~c$;JZy(<`0oF8IUUWu~g&jLw9 z@N9M}QoF7h<=2g{y?@%ua=YKxmc`abJ+NBUwDN%oX4%`w)~8w3=RXAGJvqWjPH$=( zXSsht6rbO{QdK>x$pKe?Ag?*MD!j(~y0o|oHC|5W&XLSmxHnwEHq8SSh)qUkF}gm(IRFn z_M5dn@{I6#;g2GM+R;@x&gFClwgz=9T#!B_KKH2jDCaA03V9jlQZ694LtG##lh5Y8 zZm;a0+8vHMEpv-cRDmNXUUQ!zT~%w@D@F&1Q4k-g`=amrJQqwVzj*-x=O;HTyAFWwDgxi2_=?)W2F zLhdCW2a+o!EZaqq`YAdyj%=1mpuY29E~;%Sf{1r3aybkD5BdolCw(hRqYkKjTZrK> zP$$Dq;LT`kxFyr#94Fn>N2U$~)c`ghQ&hRi1L|Q@pTP&kK{s&Zdh+(SQkL#Q^+&9* z643n{BJ`rRiUC78?Azv*^s%5-hgWX}BZ! z?0ArC9Se+etW(uFf#F2godIrG(xarES47rnYTL*LSpyiG{wp{dJj#&!NKy9j>|PJ% zgTo2US$;O)+H`{)Vxt>&Pu^i_qr2J!(jYDisX8WLSwFv5adN6#A8csRi?>*C{RD;- zoz6r|Qk;Q^cd7vbf!T>b3oIfp3#=eGJCL9c5f2um$dCt~5BV+p>v1v)1B6YENQ}k# zo&2ihs-jxj;lf(cQmme4r>=2ZE2B{u!doVI6;%REM&-yvLx?U zF|O;nt#dgclAAR$=HQU^Zq?8kBz7JLeBYI<&ifTc8gAmy&k_+uNyzRp;C{j_45C?` zLm#8WC zP|z?R_P9L4umL~_O6dL)*hd&s5#R4>kzCn?r1`y4V8c>DHeIdz_H1AUxnu9RXOnRW z!>b0%@thyVV;2$*@m5wl9;ZKXD@(u-04Ce!)EJomy@2GG zd5Pf?B%H!#m#q3tgC(GwMPjHfqakgGqcP>`0l+AEIz~R^MrhCJ?%P^MQgB2&E@9E( zjddeKY#_-v&G85}VM$H|48}G1=|c=qpsOPGczB5iWvR_Fc8pWa`mK11mAW8;@lenGI>F zhWti3CUGRvU}O_spHq~*Y418Nye7NK8{pMC*kI~ekIGn`Cc}k+S;lHTz9)VD?)32B z-*om$md0%Tjknw9l$CUJX*xObWH+Um>-2ADXm(k>G$VGZs)pn$SO=DLI=Na8y~_W} zSiJ?BTlJisTTM;4mEu-^4!PBOs8oKCrkxJ$KO@aLxR>T!XVR>Dk!5YWmS$Ngjh3AL z4P>jrSG(({QMKmirXH*96u=`w0gAdIG$4g*j0A~YdaOgXrrwDZbDH~z;phBXVn};q zWO7w3g56(!uOKx&+UDm|^-<-4?c|@SK|jv}9seX`TI z3zA5ELe9|-ee)D(qBtzXbNdm`q5cZlpr~AVHYLIV!DBL0a7BoY%o_FS1vq*1=4mmZ zjjP^zrr1@f$3nR1Tl6ZFZK<~dZ=d;m$BNYx5A>`~r<;~2H=TI#kI=|z=ENRAYsVT} z?-}EASFd&_xLkuF=f_uNLwJ|GNAbh@3FD6xTeb`aLoJi7!L}=x-(NJzVX^!rGkaZe zI6_Fq{iEI8mrlKQANjsrWqi+7SE5VBtMIw|SMkLsF87C69xY$~(&fi{LjQO|HazH` zUc*H!M9%rWt9o!{=elc~*7#jcP6u7o)4#WCNs7Y<1pceeq)VxXDp%V#bDVp_HcgWB zep%7$GktV7cseym+L%0plAO&bxjgF?@Y)bxLSn3fY2zuHl&_d?=2k4TiFES*cs#6J z-``$-qC$hod#g0aZYcj%Ptok-6%UV0Rh;mmcr}SPAsq3>n!L<7X4oZcFCk2m8_O%T z18wVgkGpj)#XfhbQ~FV{sT@Ct4#DEF?|=x*TUxj~%5U4ALI;C;Wy`gnzN>swhi16A z$kw|*TyCE5=!~a%^v*ZkH{whSipj9~^16=^Z-@tHaljV(58%T;2fp$-3U(L39KM>N z-w>IHesdTGJ9q5xp;(}FL%4`A0nx4%d|89e4M3env>{)E89F6?)#@FZt5Rf z?h-s%XRPHrZ~2Q>U*Cy!nuKI1BLbMgbceqoWinJwdFc0FvfpO@7I^!e_CAPLfg|l` z#5VI7fdCOoJQYg>Vsz-CLZjTAx4ZoKK=sXHJ_kA?BaTqd?jE3l4TwwoEbtv(%R@`b zxEjiiexxGuJ_@i@?4pRD%u0?C1+~TB?rHY-4YWz+|Mq)(?ifoCZ*JHWBJluG+5=1c z-MRI}y#J4Vt4n>UB`ea!5IhJ|PjXVwO)$SHdA`1EqnK>9+9oIR4T?LVwS}F@(u#?u zL;L&<{$9Tn;v)Uz)r>3N*|6hasb~1;(vJvPZ*kYX*B$8F!}ub~s`mW$!+nK#qwmTs zkKDMYr$ZG39-Sq@p$}Zq#<&u^_QN$l8ji$EUB@2|mw$a@iVOS1u)pxgqbCQ4J1&{p zcVt!_PhVtD zqfi4;86B>898ea>7~o^ncFMP+XwD<~ z<-}O1Xkq~Zj}n|*SVL}6U9N$)BOi#Ycwi(_{>rYA0YNG(*?QkAF6r-X;ynR9AL(w~ zxwm~7RTS^5ew@QDC7u1tyOoA^U;H;$zGZkk-P3vH!|9)FC*MDY*m6ht#Ve$k%cXFk zp->{0-UbdJ5r)7a&Z&!sOkb*$*2}rd~tj!RV13`L(~>Z)rne{bwJw z9}S1}C)sR{QCt!uImq1a=^n zMQ{*cwAA`dc5o0ydV@>yqYYjo*OlmE!fO{|fT`<7Zs{(HhzL4!M1@Zc~_z`#H08*@lkn<%M;7-}I?0<-$7^p(&dfeQeoy*Q#);{DE2trFEH7$d!v` z(n<22)&BDHlLvd+n|V)CZcI0<>>eHHzWn*W7%8A}kMbIezKad)m&i{+%cAWGo=eM8 z4fLo4S@pNDu(zL0$FcL%Wa^)4s`#Hpx;rjNg!06E_5HIVtvL-XHqG{eW`6;H#Y6`W z`gi8xzYEcf+E1H_W&)2IqOQw8r}$W6*xy}f*)`nCl6mPgzS3r%7oUW3&T>z?0$e~( zEz=&3u2~^eWK;iPkU zh$wZ@Q0YQ(vy%0;6YgEZOtqf0p5V5DVeKTe@-x)|2_?KpzHEEkLU$ zQ8t@lKJ0H-8Bt`jv^h36Kr#tbwoq_$B_F>_Awp93qEkv>V#w}_ANT;@wBuK-R54}$ zOtJ~`v~ATsEi!N%pR^yBWB38P)rq+LGvJkjK#{aNK3UuQfs-}cym;pM)4;P-DE4&Y z5@h;uj^xOFw1P7?5i;s_3rhL_1=IXGY=KXbl2s zcl$(0rzUv=nL!`NdCfSMjhS)wsN&*dO0Y#b;_DmpxEO}T5MP8*4Q}skbyZSS_9Ln|` zigWiIMf0T1S8ioTxqO8ZaEdD6p`2GGc3&Qn*ZhF6c_8tCws_yBEyq4?KcU3MYrkgb z88}6Wgt#O&TH&znQMt74L>gETGP1zqus*~RpvvPkEBa0YZ+;IsfhCUPj<+Ktn<~&S zbv8(~Lwj*fWJ_}eG_(bQdtef1Y)e~J>p!7-Po^nKfYu?Rgb>#SiE$2vikyN)F|4Su zqE8ERJi8oCpB|Rgy#sDxVtDMrD7~XMhq9@4~S~Oh^?qxMm zk_?u~A#HPy8N;EOnp=h#Mabnkpo^$vLA)$8Ml}CT4ECVaCRgrA-;W=EW&etCLy(*~ zIo|Z``+wPcay>fqrMzx9vDVb>3V&m#rb}Lzs4RJ@4~soLWy{v*kQ3SOcq7$zixCrI zt9*Kbv^ZL<>MH8tp$P*`>$cYG8Kz}=yUC^=UH86)MWlSaAraN9iRozK)7W)o+wAwwg%O<*-EHgSzZWrR*AJCCp6>dwd(bm=LQe2&<76mT-vte6?)^! z68c8Wh&w-cRqKjmNb0z1(;mS1mzUJ!AI}LMKCo>3#!p;+%hs#!J=|O>Ej4p1!-Lm% zq=ECfam~Nn{g3xHmVaKWQ#>bnpgwOFeEKBw0=%~o+e5J{1hqJ?P$&qur%MFcY08NU z5}CXo6@oytS|SLTbK9@yln4lG)2izN)T}gM_nui9II97BwleS!6-sSCFDYRZIY%Gz zLX;kcBLLaOEY6u8ZLxsiw+fH2boPX1->B=_p>vTH<#SL>)`{rJ{P8O-f#gLkeltXn$_=E`G(#7l{+MlpEY}P-N&MSI+HW? zy9?br>(~wd>_ObSgw7F~S(v`lIr%FZG~1D+*qjE!a zd!;p>P9s!YQTusiaG~;o?W@{7QurujhgxKM`Hxe1#vGS@@`l)GG@a@Wdv(!Q^rs_x ziDEeuCpqRDo*Wx$X$vM;mziav4efD@=L{>ya)y~<_-HYg@6~;!j`nDglXd-4(#=H- zot#)5)qN4&h&pqL*fBK0VnA)d~$zy>dzZK_nIky$x+-i3k#E;#M-c(vY4Q&4t&+Rj1I0yWJ+ zt(z`T(;O5xU85#{%~(s#Ae&AxOA;w)-4w&7l86p6;{{1+-WqIWGAWH#nyYyzRHiSO zhzil_v^O1%+aw&I>_q2aUVX!k3rxXKUUGq#z6tu-S(XX~>aIzjOE{#xP_I~!bPv>0 zpC*5*iNkCIRBL4#q&at7=mOalDs#VNp8=h+v|lE3zS*uyjlrJnLiC2ZPA%5k)<84X zn|ylyaj&7#$Wl)O#=Xej)kKd+=biV;#47_|H`Yl2Tkp_X{*%ZV*x9AU9(s1R0JD$* zW3Q~~PG9`${N_sz%%+v*oew+zforu|GSG!}7?2@4BUVD}{vd31%_o1B-&XVI&g2;P z!V_ua{@VP!Geqr7cd#;z7i>d&z(SK}_4$JH%Hmm`F(d-<2WnPcU>Fxis zlIp2}?k|xryB8w)BBXthB!S@O%GElyZilq%JwMKak-m2D$B!RVGEf2vY1x z(OA_q)IdSzXIw)|Y`B@vWVn?*Yn&`1P=O{%kt(*<9PD$3Ht$;+!?UgDAyFYworiR9 ze|liDWuocbG!MG{={t6BPLBHgo>03^pa1tM+3<(0#*W@>TYG6+JH-$nzdx7NGJs{F zqmR^f_CX&~xj*Q*XCbK{v?P2Q57RJPsIGbOzSdd;gd6cnQ0rVRm7|Mmd=M#ef#i2lt(yF z;XZq0`;0hwk@}XcbsF`h)w<$^$4=)}x@TFq*#LZArF+)Zb54W_UT7GV+Cb_b(KDfX z_s;)ey<0Q=Un1=*UzqYQsqS5f(p2wI{riy3|NE=lf42Um0gzZNRh>`w)=I@QHEbI@$M6uge#q+6NCUilDV{P4A(E$e?_tvNb&>A^yfo5`L^LwLq zlcn8*kZ-FL#!|VOhtS!p81IM|eUvH2A5G5jqLh|#KpJ!)x`Pu{^;$ngi=kR-srFEG z>%mx%mjjk4B~rzmEwmcSuD&o)=heZpVyt*0XTTnuqlt8K)qv`@| z$U0h2|2g|vb|pGkSJR2a5*V)1xmM}ruoqNkk*S#`w4IeXI~isrW-3MoQshp>fqcPlL^+L>WroGvc4gs1Y&qJ^!5?aW69e4@t^%oc0vvCvS) z-^c#g`n^C;}^_iY%(hq=EC8RZG(NBpM^TFP1Kf(#* zJt=LU2W@lku&!~u2YJsLJk9^j17L4l$XS>&7JEi)ZD&LtI@ed5fvGt8avmUO+rrPI zmP;|DMyrTQG%p1%MpXkcK_~3aharVo3Yzbv!bGp7?c88^uOB-V?G_@MRHy+vY70~} z(WB1C3&9^hv1x4-Q#L{@_SVkG#9PM~#*Hd}2RK>e_vrvhkoSBjZSvnZMUDY2+Z@Vv zbq-nR*-W4Q75gLP0z$C(wCnG+)w&vh!K!y@i)IxXjJ46l=$~G#jnTAgv8P0_ zC4abKEDCIT5BiziW}LfCH91XJLY>ZQgCnc`9!9WamvSljHCh}siX*nFu>TcA_}(ce zIVt!WhqArH#?%eF_U^f7Q>-BpL}gVbx9;2l*fPl(y7jdK11DGRm4&!Ide!9Zy;3N< z8=ct5Hv4LawcSrd@U!erh?WCb9I zp!A-v3t_eiVfFwT8y0l{Erra~<>BcbgP6uk#5ocva;6%$TMWEv5G@9i#FwHS1_6z{ zWXI5{o6TdixxP#CpbTEVOp- zjyUMg{Jb!6{n~h&6Ij@ok5d`H>J7!#6iSz=Y^c-UZ8bHPKit1!D7+#&;&r)Wa)fyG zv>e*hoJ?*6G>E_kcJBC37gUi8aB*t?vZjz}@lsNWM;axTGmjm(?qf0IVH>RtSbv)> zR!?5$8D2VY@1BE1Xl+QZdB@6ugL`h?)7;hDDq~mzuy4Jrnt0{l=sa!(V`scH^+k`T3mYPMEfg%i-{U5Kr3MA37GvC@@+#HKVNt{_4ebr?_FxA( zq}-||n#84s1ZVMIu$Gbd=3Hv0<0&7IHm>LS`381oQUg8uPx9kCo zM=+R8cdqO9sbY&6@VorHrbZIkpk%lQ!Z~li#i^p@jA1L6pv$d|k>(Kjf!Th-#q;rP zOE;EtfiUw;tEgo(mJ2pcHCV0A1iDB*JUXy`Fp?zL#_MnJ`BsdqxlRN`2`bf; zKb{AF4fX4oftjy#`zvbM)TeW^t; zlA|u>uE7!h|L5hIFV5wG^3!v8j9hs>mMA|{AEPnvtBu7;_5!Z-Q}gl^^S)Uwxo-{! zo}XdLe_Iz9mRHxt|KLN`u>tcakDw3V?Pv!VI;i@fktA9uz)DmjvoljCNK;cUo8o6c z>%h%MzIry=oKJt96`STCu+bWenRUg1+(^fe#Ev30VuLzvFRj`&cw{WSl!7`kD^7H6 zKDH5Ei@+?A6C3*m!j1cvu4>j`^(=4EkSkgRTlh}Uq6w2#^~|UE zXF~2;h(LbKp0`8buWP&US~>(uwcRz%k8s+e}8@ zc`kJhfGxJoWs7B*l54sVp!R_j!qi4BiY@g^_9@i1+kk;r?aZUOmRb&eu~ly7$#d^} zK5;02fU%a%;DgS|rOn`j25Cj|H#Obrm+CHhS1@2HzAeRo1vW&T7_fDq;nsu&%eZRB z#hn4lI7fHPMTL#mjFW52)HK#Fru^za`faxXWNk+*I6YHUN3NhC%8t9=iZ~X%EdB?D0C+(2U z(tx?0FnIyU>b4sT)C+r1fyU$}CPi%s_DcGL;)i%BP>-Emrped3MzGmaY&h7#@>##q zfh}MvtZyjl)&t%=-5VVM1S}hiaM%#wMZ+jz5;-x9xoo04J(0$5O@KND&p zyYAakUf-)IsqlaQ*ClN%ro5WfljT2ze5DkLzYuDXoxlSO$zLshX(jPpKU5I0M2fp- zYS*;Cyztz19QnEm4Ub+t1#54?y%_qH)$<#?iy>Qia~(FYLbDq$o@R?Tix)$~sg4ys zw}3|7oXif%3=>T=PS($;$<^zfQ2n@(oi!-GcMdP2|cS-D=2~l z-$u~;fb^%G3kR=Jgs@6cv{Eol;Z4I{ISM3GK*?edcuw^d$t~cqVoTBdO)m5D>l`}*eNo(2q~Q^4L^Fcd*Hykj*Ok!9)zk+ z*2SmO^t<&XqaRvX=L}xg2r0%cnv&&2o!q)evN9iCG^g^8Abj?JB>#@8kl~0!D`1yW zhIRk}?2)Vfq|C<*;-m)n={o# zpSmFm)iw%zj{KW17xMz~;6E!rr*_g!WC=4q*bto@ZQ&-IE@xQxyJK5h2S*1ZD-z3t zgrt&W!WzqZGsqw@hF_E$37WQu?hglf`VtWCl8oLpj0KBLp@wGrUowhSWUi6pZy6Xo zvGVcPd}Z8pN48qVioQ$s(M4QCW>7*xp>JoP>88)wGdQ1FIxOeDlyaN&+2D7V4&NNR zy2F0QaawC>@wrsuQU(z(Q<|=~aRE(Wk8eY7H4yMJcHSD@!zg%EMc+r=ecBI&v^6?O z@X)TE(G?xlDP-u05_c{}-W-fKdu)<|=?!6l#Vqz9v$dE%C1c+4 zoauE;EOuA^lKl#5*b!j#ZbSv`LIbYs8Q?g@LM^MO0E9BVS8mR9>)07{u;n8Q+#&P6 z^Gvsvzc@Pyd&33ql=8g<+?sE)YaM=Ay{jCzIv&UU!q~yYA#=vkY@qI;T+bpx`eMPKX;0uJ=j9wHlSu)dWboq zu&ONk^Ws)lORMSDdBJtEsoO<7A>ihR!p$LuA5M1nZaJa2jR2D$Ew-nCBhT@?E2SsZ zSaj;v$qg;*3#&V^FAk^+`B1o!EHo|ca_WkdonQwQ;MS@fX^p-3+jxVMVg0(uXZ)Hnewj<;6d8}Xfs=pqp{0`-llbXJ7-Z*Xntny6Y5vbM|t)iD@wk6 zc-e5vc*8({+v?^m-Kp+%tvv@ucV609(4FEV16Qs*ddv7tD^ILq1K4R39PcddD(qZ$ z?G{-fB&FWItx7lmCJ5eef^Q*HEW&a@9$aj&|{}Qdwe%17%8bS zyZfD&zWNon=rRRs;q9GZUxbgQIKrrPFLNBQE8H^(uGtxJtZaImqilKbT{Uz#XI}ln zsGQ6{Ol#BfLvwdPlJ8Ed+o?~VyCaQEoL5cH0dn=vZi6JAii%C1XFpoG^8)&r`mdqW zyV<`5F3M87mUPijqJBs#6=4V*#ff8ScOe3rkUh6 zzI9w!4Zy4y&IZhSk^*K4QnmH<)?<4gUu)fX%GqgSdw+%%>b0{IU^;W0*{%Qq#k`zzN!-PTO0nM`$no0yBRakrHq! z(|P8h^Pkhy+vA}oAbo8o;xVV8?kNB9sM9!DL#B?rsi85PZ;Vj-(_Z91us=brnEL5- zH}E-L`b<56+E6=}0ec%`ZCeU^Se;j!2_Y|d6*(PfP6e9M-)~hAd zt~bO-ha1zlZ|8>VD7qQ>>HKd?D{rB1yMm3)p~e>b--dnUyMX`FcKrD>8+k!<@oLXs zcU^sKvWeq>9`wkw4PU{QN9}?a-ShYMMc_$_zx_Gr?-mD8+h>|1Y3P3iB){-~eMv z!?w(TDX+Ng=kRxannkewiLn!u)R8YhU(n%>d>cby_^DqLBXNwiK&!ys&@TmF8F&d* zO4`+nR%wQ;0bZBO4ZyDAJk0-5n`@Zc*923HI*J5ObeWql5}M%j<5|Iom=0_JU zzHhBJi)Brc7)yEM)*7MF8Stjkl9LU$C>8v!%sj9}|6IlN`pt$@qUW%cP)wgJ_&JyL z!CNgC=NDAV1UCJebsGYP?le4s$PL~%NBqRnSSc2O>?CYV{%2u*S5R)MjKs}uiME$e zrE2JO0EN(5K0Qt6q1&~nF1B65yj}LKj+8S8&Rie;F~+m3y*i6{#(w42iT0Uc#HTrX z1@*GyWBEZh%fvJ{)wr}5ae9691GM6D*_r#5X9o>u_bYc?LpOgg@7eK#>L%*2DSIoM z#O)mg%TE7{{UmIPhS;#(juB>((^OZO!0@8+xd+K?>W{#>z}tfxGVO9%m5#F-Cz~c6>Y!jXPm@feW@oLi^(asn^1(+^6^>XnK1h!o9^(&l!1_P~y)FH91w z9q<=m!aF*tV1Y99%V@5ZPa~mHp)PVE6b#9>>CB!D8#6}S$EG{l)1$4OTN0&IdspY` z;uG!|LK}A@n~4zJ^aA#u%>aZj1;b0YNNmu_H0QFOC_{cK#lnQi#0eIlDFJW5yz#Pi zTQhB3CO+`sq19UlyZ5}|!0R?`+*ow0J3raK{NMp#7SV^xhL*J-=ml@#3D5zs#~$c| zg4fKBO8wE+Q(!NdZ)>F_#%lVqBNxJcjAVxDsio-q{Y&8cy5X@@`+j4%Pv|1-5};Y6 z*}mU%(B@&_Yl%pz!8>KuJ0NFgtg*5if6r`hU^6qIzMKu}8#x!$_YgB@bJc9i@0l$O zAFdJD7xtP*Kj+bz+g5C!%NM<%ftn z%Vy>b&=q>v>UtU$0KS}^ z(meM|Ju_cwf9EBM2;p_hc{1yM?3%u!k7YIMP{PyR+0{yz%lhc#oFK4Rl7(R@RvHWy zI7>@#lEw;NJ*18ie+F-g5lbuoAQ54>*>3`tG(*42>bRtpC@vKfpcx;73D6H_O!E9G za)NaaVui_|r#ywJmAlWv;ywQ0EEezTHj2gjG?^?vPLX(dPv<9YEbp7}FYuj+SG}P1 z-`lyh0CGs!7)5BppfdZ~<1b>>3>-2$ivVE;htdOJkVb@B#NZIz2&y8HGlEj9w(}!& zoyjB6H|Jy$$kD_t;~0y5EFXBUu6bTrilxTiRT@K)!y`&o_n!Cd$_09FZgQPm+u&92X~rKXhFfcgHl-D9TLJlz^vl|1uj;TDy`owSuK)p5 zmLpUyuP8S#3JU1#wX7T9jF$R3T@p>%60zt}dbJoJAy;HEFTSwk@Y1$jeVQid zd}2+aZK9#Gcx?CP-mCuXSQj5wMXq^gU;DcDfu&c!nf#+{BHf;emj5Oh_G6c(DtX8@ zvzx)Zqu!~NpC$c)107onsV+-t)P%-lrZ16fGh(4b8wU23mh_2|J0mV{$&TcPCOpX| z*e#{%Bhr4pi|WPZ9>j^pX8%)QMxE;q)}kk|mTa3r_o#u+Q_nu!ZMDu&6k(^w-Hjv085`l$Xx;o2UI&d_}8k<(vhA7j1%N{g7M z*#x~NdP*MNU&5YHC^Z`P7O>zOs;knWi{;N%mSUm(F#ig&Qte0miVEHFDi*)^EV_`` zj<*Z!n%@dPah0d|Km3xo^*qBmCG0MfCHMtihG)!(Kmrldn7UeT^P(jbQCg!iH^oli7J_g%#LnkKt2MR}5xL8wl`GVc+OPlwu9lK*l@9J`= zR)n>zQ@7mC=0IdJo9^z2)km&G(hvn%6T9viEgtM@>n*g%?u=7z>&ot#9KLDj`l~VW zOGwr*L0LEjpk3q)+MTSMc9n_qN-CggEoaQ1O&+`GO}lLv?CirZExmLGr-PEVM*2;C zwrQ-P`{sg-sX4>wMv87!Q3YjSq);M#q|^^uQU z(XzH@=!#`c4U!5&+0fd)u5D`0yCCdU~ zdk9C!<#REzA%-W{=f6Q1)#<`jdRErms@(3ckCv2R_ z*^Jnuf`T!(ENg!GhId@r>ykV{)cxH-z30w*hLq>zrWS=0aBVW9K1+D4CIQ4Dcr$O) z^_ePNPt{LXgx{20sNPMJlb)2U;{<(XVx zlI_`b7-O2fvch^T%cgMz# zo)y`qBp3DYO61py->C__0RhNp0G44~-*ZI}&5jKpUtVkwH8UsW`U#E8D?0MaRay?% zeFODrNmz?HEueq|n(d&V97#zbA~c5LVx+d_6}iPFCQkrQKm2bL_m`+dh5gOdbS zRnT+Pv43N@Vm~|1;5TG(>Xc*H&x&rV8VUgCgQ*~+2!0{FfZVDGIajKcGK`>09o&LhH&BAIuCyE-=y z_b+MP*55qZ>_aL-Qat&-{yJ3~S>3g%J2j9QTiP|$wktLs=R_&hFw&aqS(3YS?XD}j zxAlx%(Uu*OOhjpsgo;XtfnXZ*}=(F@5uOw-r!QaQ=ii% zUgkERm~7<}rIwAo-TT(G$MS-nAeu{L!$}-AxjTQJg+-lI@_Ioe<W;J=~=vr7#6o8sz2y#*&#BHP?jt_lEc!%pV{9nEI zEHC?ObDZps;_g#}U6*d?dix`5?^w_54aIl9?a-RLjtuO6`{CVhFG)hL21m~$OPo%< zXTv|odp0!cJ^xNB|AA+U>aU#T+vf4bQ4X8qi`%?AV$*m!#@OPxsj3a?Aj^WZ9>8KM zv{o%}BKB2lY9Ld&+tX~pLaJeBh0dbWrn8)d5Ra$}+aMjb7Cu*7kJZeKU4~eL9Rmy~_?`uotg_+(q@s!lZ|>M<7@O z>8JkE9Baw?qmgjZ$rzG?tb;4omgw)#!~>m8dLZL#F?b(qxCPZ`xZ49Q%_ia#pkHIb z`N=ub;o+!sB>1Xy)OR@jOI|OLS67V+Yf+7c8@s3!}(qj3Um+FMbF_K%| zH*jn;)26voN|a(eU=b}qqAu0+V} zx7Q|9<p(P1%8oaDY<(sE< zog6P%oMwnrp4?D^^-0t1q>n~+upczCQwT=_Ca($z?S}8Xu{oDAC9jWT8L5BsCHpso zq_4!1^~EC+hrUbjq|E!1M3gJ0CDu0 zGDrQUiVuZuvX@T2roN|3K6RT1o7l0` z$EI=95{TDCX0NGV%4lSnquC;_<*6PJWM$erqXJXOcToF< zLoo<4Z#bkgB?->mOctd^N$O1HQ}I9=s|`$F-?Gus#{NVgU^SQ+732*o$S`c8XWvmnjSmJNH?h#jfc z?t^8EW(nc|DxzzWHn&$IRjQG|^LC`#gjOktP_gtE<=(!%kCR zl@MWc&dg@wUV-s5JnO;Y)`#a%{i2Njy1R&cQ-E-Z1R*|#Ct(c}WJ*9O+CQHXYZj7f~34b~-r-q>!cu!*_?e`~&*SCfMNgQejX%epn(rLB3w0wDEU##(w zX#;YO8(p6&BCDErccl8W6C?4yzF%?XiqWQQr#CnF#@y1Pmr~gmYVFBB&)SGYRxzsZm#eBF{4ds`xEU1HQ&WESga;cc8{3-9UL5YibNn z`QNDou>`?oyod^nYtimAGmJk0$=clDM0&KlVOg$mVCqR-=0wilr1f0d)3k3(*Zwta z5sxH$RJLKy%^$el)#H~jt8=7=GfN?!{75xsKZczu*ErrxuWLzEE$dL-cyW+;ig7E%dbN zrQ?EL6;)w;J*NvhHXX)RWV2X67+Xt#THbwLrB*&^j<#e2(P%gU-wcpH9AekjWZyt0 z;qPkF0%>2X&igpsjeX=ac=glZl_9c@*tI@|xN!EstoK~)T*9<$P3ICmR`*$5bS~kd zzkoeZgJ4X3ZSj&<{?6h*Ti-F)ore(XK6P5>sp!+nnP27jNZn_CaXE=aYUQffLZR1m z;N^pXwpi@K>58b|+hFi>(iN%MgJyN)qRvv!X@83@csbyM%;utyw?{6Dlj=L@y3-lu z&)0p@m73n%v%bmkxw_A~w)_Sh(S`CGFM+x~P(wbk{3LQ8jB8HrgAt~4A1s+peni4& zBtM+GH1(^OOP61CdSr4DtYFu7=kmhUj{0@|#pg?29@(Gz$>LHeyv+G5xJJ~3yV2-;VgYm^DpnLUklWef2r$`XB{(Iu*?+( zsLNiPhVQQ-H&cGa9I-aAHVZD=yhl3YqE*hceiyB_FVFPyNOAeLi%CIEyj-Ha=oHmui(ml3kvt>D8b+VVgNv}aHMFI$(;bP6Di7Ca zXBWt7eX{<-aDCpAwNMvJ`S*2JhiPooj5B&K@`{x>c+2AQ^tBY4>OCTwNS-eLvfFBj zc_!x6Le8oI{56>{DGD-m{(Had{sQg($cdR3hqJ4d5vv6h# z{f+gN?r}EJ)9cu0Fe{sIbUG$6)sLA*y9M9QrC)e8;|vnv%sU_onCKO;@Esf2~^1Yj7a3)x?|@Awla$0+wZ2Jj{6ibYAicGdHXQm+JZ4- z$xtw6+z?hZW9+exX2GeXl*UwkWmj)k&yg2C(34kP8d+U_YURPg?^krL3UB1a#%z4q zV!S-adRy*8Qf#JNjb6B7HIVARxZEpuD$5*StsuYFkJq}RIh z+_Ia)dpPB)NlwBvjl2J<*I}GBuZk?o2;EnI(ba84x>c1`IM({|u9C$08q`bf?&Q2^ z?)XOe4pI8-735SnAmJVu$J*1MVDDjmjvXkMqAuWpBQK(z(H=5HD`{n%0CR`KNJ7)f zzTHcTYbpaRY2NR(@|0)j0+60Oo#^YqU85&}2N5Wzp#JIWhB2*VIDkdROhwDTAd3tp@KDvH+wns<|bi#hc}#Lb0V zjm)DMx&pWb$Nh#0FaLjBy$hTa<&`!*RrPk&+g(*%)m>FxeVe||)XX$94b#)zFat9% z0|N{&-~fXTH$^}|K@o36QBg5!G-|xzE%6#j#Au>1#!C|8CdRnt=5E~GBxVy6vzvVD z_g^>3?k1u6o>Pta_W$V*=;^7x)OpW)&U2n~c{2se!4yIzv*gSLo_JcwaB%=#b2%>G zb?WlV(}`%&{*f4)==Lu~f#rG22gG&UNV*|qB%(Wt@>M|-V`h&aEO*M$&O~#AU<_Wp zT8pW&7SVe)J+eOggx%h*3Gxq^sBh1p2}NNc_n;**ITMq_>?aeLBpa%&+&a=}hPr37 zTS9`AN(q4oEE#89C=+C+#V;+0 zdWU96J=5HAxz5ey9TTx^XTk{g%{e0?Mmj`=)=+G_{p8Kkq|}3%WhlR<0|}K5NR0VH zmC#&_jC{?4U!r9t=~C^H=nDVKbT{1~{8#6)@BxoDtWdgUCH=VZ5h z*%4oj7q2_$_HW$!$C#{1;pVbLJSunOg)>wkkVU1}i^s#=(X)S?B-?TI$w#HfqykM) zL{k{eKO+p%PeuY8iFAqz+Y!yT=0XGZ19dhkNhBFP$^gZ}u=m|GV= zAC;_PCYU(8{>!kGF*Rk+h0$Bzm`McNUp-tae*CRq;36eb~UoAk8 z;5qoG(10EU9Jiz2EhtV5#w~!&*5NuL0=Z7ixIr}&EVt}nkPQS54Z%WrwfkFKMTVE9es&=%~RlVQSBSMVI5-ljnld z!j0gr1{(Ou#V|1RG!8UcP3Q_9~hYp*eFC63t zQ*p+vV@wlPR)YO|>RHWH^vv0YG^c|_lGB_qyWd`Y4|j`3TKHeZ@R{h%W_;&hmB+_Z zwq@C=!`axtmedeGIC|!?06K3EUsGdz1cyI|%Fv4rfB$DszA9ZK4dBEtj5CF-$O71I zu2zSiauio_$#bKfGv8T*pgN$FxPAYQ>k>8?{( z!reFu9$yvz8DDRH|3*nTKPo6}`@eQL%=&zLdD^-5cZTPV|4m<4^S%w7zxn1iTv8mI zd)Zh*GLLiy;!2#$L8hguiC!bJ`U!AYdY?LLE!!siXDktcv&7v6Yfb)rLI3yycCzi(iml7y&V1yL z$+-q&l~2s!cVosh`_}f^QErKQh3nhHEV(dOE1ki;KrC^UsdX(%pJfHL8NT+iu1+nj zW6+=QyqV2OT&pD>XqeS8h1L7+t#ECeF@M&f*mel_c#Sj{w&X0jM{G;19Q4&68-azu z&V-i3jzX(Dy@GGNfwAZ|YIVt%2_P75GT=aUX=meCwR+W8;_6=)8AB|q*cG1mb?@DK zCx?xF_rB}mfOT8q(!JShn}36lc*jPR!*kFbQDN&T=? z)aciCk;j47|Mf={I1+sF3sX^M?AI8d`X%c3{jxpu{T_r7Cxlo6aa zNKQyx&zDSDQ3Gfe3I+m+YGuI{3ubTW>mDjn4Ui(HtN@Rs zjKjb)EHXzIK8q3wH1(KTG$MQxQ_yhewGa%V2+|7ceSHNUh<4SHbk((2L^I&Jn5`&Y z7H{i3+mFs zTB48-sob*I>>K-yYV%hPH^Tv@B%|n2z2)R9(u-08{@O&xJ@&puZW)%UrNRZT-qA7! z{SNF|%Phb&!24kzv^Im&GU=E@fPo(`Qmtr?GGiZm!2NGbnJ}2tykSWc{DZ$>df{uwQYl(WnUP1x}N6I=B^1| z5FQ<080LDc`GWbpvseS2yok+knj6eKmvtlTC&($l$`HTckN zmf4p}g`G)F4Mh4Z+e+6bdgi)u=3(%7+1zoLv87}P22 z-LG!guOtfrBFN)7ZxzVwSsAVsC#E$LW-m@ zGig^oADC>^f?9xy1H%KEfo)Myzyx$*w7u%~FAnsr%#|C6FMzSaTBcFX^_Sw9J*Kkk zM0M^&KwuaqfP!Jc9O2PWfI;K+)516!9Mpv^Yt%jsEID!V4dAW-B8di_=t7>C#0)=F z1)!+)H>xC6{3(*VCAF)O(p5_sMUSfhwd&+~fW4r7Fvn17S0>H%pP`t5=mYY@u_};; zkKJIWV9ZZpu#$N5_;B&PnbfrKmMh25T%&xY`HflEUHruAWb;Q=CS36`B?x2r9+xDx z=u2t0`KGmdXCj)`l?$zf@-v9V< z(|StW%vD@sRf=I}f<+co#>ZSU?sQteW2fa6Yn%F>p5d5GvKAeU+$~)UrXm^900`_V zXXbd71v?eC(*zo$W?+Wc;4SHKX+Cs!2$KS-$_-Lz3D2$dJFU&zsM_S!35h~*q=dNl zmai@m#R4=AnHDkQ1}}AR3_Tf!P7lH@5;j|a>4JC(j4%f5HHxxR09M0K5H%s*)~+Cq zruCA}BCXK~jU+7MwRpNl?zGk^j<1dFd~$63Hg@Q+cgVC=jq&5=+~%)ucGxpL*K@Yy zKe}aMS+fbZZpQoAPBET}>{@oYR(v)POtMd`Wx}o%XJJ<;mjsmMI(t7azsFO>GHXnD z>e7f@t*hy|1!>=HfcB%8IhuSaAVi-tSlL@_f{KvEbT_Z(=5Ek z&=Q-*S*ltTx;LU))ZW3_;W$72Ce}X-K6-K3ith@s;ApSI&dV!u_z2rSB^_uMI`(aW%ewP-3zCns!s}*7m@+^9pk(iv+x+wk^P2cLR6L2g?N>Q2;u`v=~2H z2oU}n5}@iNYN(};CG#X?Uyc-Fbo$EM0RoJ|CxYAfmyk5hb#S4}H9VpX<6`k>9k3FF z>{?4vqieBV1k$j2vQiru5&DHi3-gUPqGgqxT%$4*3}rS6N80cNGVMlhqIuE1F$JUd(R9wJIB1TS%vXxTg^!XdhFU{OSFW z^NqkTpO(_-ter!1SfSeFk^ZlMVM^p8j0Y?;jCw5q3sA4RB@bGRpS@y%pghidK~^8X z04&Cpj6{pQf#rf7B4AetoC@l&M~V=xZ<6quT2*|_G@Dz>rS(PO3yNtfrhRAg_Gg&T z-Cb)Q=XO?5S-h5LsN^4 zU1%}kGiOJWkJ~O2+T%R?P_&(?eS!VT%9WFvzC>Gb1~!u|^#cIDR7;p!&t^4t@mOcE$#A}OtyjaO$Lbok7*Z)zV!x*9VEY^ zU5q&E1s`;H4-m9Jax6h6% zr56Ofd5phivh%;By6Jn)wO!M&x_=5B%gdT8PW3aWY}L-s3n&9o*a2Of+Z>2F+%|+I z!fatRw^CWNuy93`N7Q)cTqT-#IBKRw+iNE=!KzyjdK&iCb2@*sZFMv61x(;z}jimKJc z2ijv@zj!)QT>AT~c5_=rkf5d$=e0j^r1&KEU-Jh;oGC)|w&uNa1GYyC%=#tPxyqBtplal;P8j z`or9W1~L09Ebrt@HmMC(&eFmOlNsruW13{Rzmc|LbU{hNP+c6D})7Q!7XAgux|F6WN_qtO zLGLKq!jlkCqef8}X`_R!Oai07fKN#qt3Kfbf9An4-8fh>wykge(R)s~33O9@p}wQ z$yo?dhr2(|RAHHCW~&DiGD`(YhbvoPsg0`qc+0ieif_QOlL~6%dUxeO;kf~Zt z+!gMgJaZi`o!qlR+YV5PxQYRsY@jh9rrs^JNKe0L6_`O6Fb8O80B*vtR2o+z$;+2@b=O^Wn$NP?oac>CVV&HamTR z;WfWA4OnE{r7`Pck>uBzAuME5+>R&0H&mFGH6J=;Knq`EAM50YdyJ`|T7KHoY*x`@ z_Ukju=3;TGVtKBS!z{S!uxC{iBf?lT&uzt9t$94H9Q0D@$f7;@W^9_t`5l_rJ~F%1E(ItLO3aYPUaN1IgdUgN;Zx${j`622)!R z=*g840;}OfYC@0kY&EVV7TBC?^{f-hNpaTRC#6|=h;e6~(+N**!89;iRmiAntjv_S z3tF4Cjlg!^2R^wE=ZB29T@Tv{hM05|)C2pNix)_Vr-l*BdvCGvTv*{M-AgT)DlRUzaVc4GAxW^QFx!9*_qU zncuQe$QAQB0kSa}e zoLNjc5>KC&PFsgO#;3FucVH0^!3u9}(e!9~m1YCP%wGTz4uiEFPEW!ffb|V#)G)dt z_ob5g)U0_!WtKnS4c8KBt3HxBn`yOAd(4crGwWD@Y7e%+20sG6(`&1P#XZ<|19_}= zgb0XvTjD1DCJ94W5I3P5>}1>icZs+Yh9yJ6nEH6r43fc zL&$s-GQStO?h;tXQFLZrO`1+1!Jrs`a1$^V%&na#^E-f%+Z4GX)P<#}_IKd41Rqxs zs6_~X#K}idZDXKVa43SqZ{Y4AaT$>TB#Q;aJJ70;&B3-HC_-yQAK<54#Me^6d%8Yw z+3=WcI8otpD{VC2n|b11OE|!?%{M*G6;^x?ps_4C+>1su&YM>Tjie4}sd)C5e`3ma z3}I`z`Hb;w+_p@9#4G53bemGHz$-rsnoBdb!fppnn4ZZ*pr;QC!7LBYO{|Ack-E+b zpFhdAjPBu*RdtWBirJ&rv-W60)>Rz36%8ntW+Y=%8lEb{|%fOR6m)y^VQ7uU!}7=?o?VMfLN%x7ES_qmMVS5Of?i48Cv=p}2SU!VlHv_F@(Xup znvfrlr&9KerWt8XbCFR{Q{|mmQ^7i#xyMeA@vGJ$Mk_JqF7Gy4wGg8xzaV`Fb|u*M z8J}l_C@2E8-~>UL*9Iy?-jek|HVsNmPe2dJ*yC9Y@`U9lltJkuk|N?@5?VtkI;KKa zC{m~qgrj49A$7bJ#b*@wV)0;7iiCGu`cTl6F&97&YEKc14`(P|fyKdQfRhDsKD=m_ zlIEF}(a(=6hq^}NvumuZt;TJ#Nw%#cs92RDPhtI)w6V&;?yh8i&Sxg1bXF;8(F4-* z*+x;jV%=gENQO@bx6Y(cyE12y%TkamjPepn#dZvDp$Mn7($r=wOvgPI!+t!8JhG&e?D+V@S$rCjeZ(edV zbmg4_ipU#`KU7#M%X%iao+r;v>FSXc_srMNP%^{kG3<|dxiKBu@Z^D4do zGiEQbizfNpy|6`30Sk5dvGPmUBV>3e)@wnR7SJQU7vldb`fgNVu*g;c8Op6JS&N~= zYQU%?PYPbh2Cl)9WAgxn1M&FY7>i2W5#dW(Ph8LnWAG~9)wl8`erlLLu`DQ_0=tLf zwdms-HGoxELHvymz*Yly4cFsq1R^N$hSAEQH!lWPBYe#Tz^*nMZwunEt@Q!cb5&S` z0*Bl`gI{?{kOZduA;eS!(U! zWsk$q^BI>HtynnK5TQnQfTLf1I}>1*i08x++1*=)ZBSE7L4K!GcL{Go0qg6sMNhHj}vH>JlP*);5FO z_7|mvI8(&Nob*p)tpRmD^5359IdI;M8bl4wuu2HX2f9iOYXhAq3-IvJ3R+;2e~~H= zU>AvKLck9zcgp3E@?awqQ2^lOr%JKSC{0eA6MM1-p0VY4Q1v6wV*(Ta%~){Y05~m# z%GjF1$6*Uu>7N+8{3^!&^hXB3_)>*&yMe@bJy#xgD|F) z>}Tyv_yLev*-(Nm*S1(`W>%-T2hHHF`*1RW^o41qG1724^>NvBak35HJu8ujh6XkV zdQ$-&Tgl86kMw}W53qz1pDA6(nK}|Ibf1s3PTz#~fQB85Pxv{vfc`=uX`lyaIG-^1 z2&!tNwrM{f*#!i=$FJ1&O=1iG!Zl%iU>6~@Dz*UaFgx#fK1e_ghgPgeZoUEIX<-}V zwPumltwwbRBxA6*{$2!@!`dL$C}J-nh=B$5{GAS;2>D|d({nyzI|U%}m@QQFa2fV& zu`bTsFuv?$+mO2B!9iBpJEKQ!E`)#+#<(s&SZ1kPKDmmy&Ew$?mg4i)s3Qz{&NOuN zXaj|Gv%k%hgK-z3N$#Gz^C`pv(p|&u>wNf;9;uZ22T71Gn;Xcy!@id94LAR2dr)*r ziwU!N7UP-coSRJ}rG){0xxJ{&L4xZ#yfYhP%E|>JDbCVWYy{B=w}+in^A-SuiU#sQ zGQ6~;C^>FSsH_UlrHyrJ!NLT__T`lbA4a%RQmo)O4~5KZTp7`dTRsry)ZxqmZ5M-2 zq?!?mFjDvlqVu5_Oejnto2udPk8f<+@zlR+kJt&6QngnOX+p|YUV7{zQ z;aG9``D__n-1bko0YSSRp^J7f8OW(zJC^Zjfd+~k0cswRPneRaEZnb5=Vw!xZ<{SuQT+TWp>C?CtMe3z5LIUXTYPt%Hzss-`9OK6du;i-Wa$LT99g=M z>k|~MJj9jFALCi9aw(X~mU~anTFPAPrSi!iNIDhsGzuy$rtKT7)MOhAl8f3_9 zB}#tGtu}nC=NGS3v9Ta>tZ!!xzyUrEj3us=33*_teHcNz3~7C=StlWGyg50w8UYa* zjt@v2YXZ=xfHU?@nr}6sBILn{iIEXzwu~QbY8XF!962)rE^wCk?9Lh#sa^&DOsatb zk`JKTAo21ij!(1PdO3{E1c&sJ%XVB@je-bam3N-;8oS9O4BFUk>*tS5No?FrB$*Y@ zFGQkWyt({Z`kIu1Jz%Hf{ot4pvh*{cf(z!m@!vgKQsYm_S44{1E%9u%%B%aC$Lv1r z4*aW1S$ilI@7r2V3G>C#eHF=FG0I-kij&?X7YwuBoWTb(UYI>$e0gea{$`^D zT=);WpAMb#YPb3@t5DI4*6Zx=3nr3Q9krBtjQuh!N?nW>QOL#iSHkTnL{>ZtNY4&d zmGoT)ZFa{j$?RmdVVcoMAa*euoc0pZg$ouIxycqKG@}F>whd@=;^cFPtp@!QmFJ^| z=WJvEj`Xu5w-Hg=mJ06idCN~R%kP)g>+(+}k@OAJNpewCgsK$`7#uvsd zz!#ui!$vGkRU0@R?w^@rdpPLEg7ib#jBA9iVZk8IXp7P?CpQ~arh{UBh+!&>60|2f znr|k1p1vJv+)yws1RwknPAccUh;eqWutJLH%sDeXDX;2KF~WVMu60SWTAd#b$)cR? z=}w8}#(~W>uV>j4e@fb>5=Z64*>=fn?%(%Muf)T4BB*ODy5?R-?VerED2f)y_IMbC z&H}$f0vZ1VjnFT}5RV4}C^4TUeABX2T@ppZ5#oAm)!54kM+x9KIpx0t0YQz4n`A4i zN(g%J7v}#5Ic5O`1w9iAKx-xDcriu5WD+IWz+f3sj_Sso_~}A0ET=jJg9}>H2}(t^ zxrE`M_kbQfusa?S&5q57Is|9TRl;_6chJ5>_R;~It|e$lVabA=JfTWnD%lv&jLqC0U^-3tLx)k8wYigaPFxFs`G-3A$|zc7%h0Xuka-+m@-<(O*4$P~($JyX59o=2wJ4axr4uMBO9T~?M#bfN9 zvbp6bKA`hI+4#t)Hg`^!sf`PX3E@*&`dkMG+)E`k4g`WCc*elgBL%>OWj%8_TpX&? zoEQ&*^l%v~GQpa(Iyx~4DJ$_TiXvdSVcLNv@F3xuv>JU*pjmWBhJzz~d+k9^|f8qT;r_Rf9zM~i{C~G5OmdF5%YJp6a z*|7-ozFaE+9cfuLXp|N0&)3<=hH|3F%FHOjM@}K;zYn&gj9RA=u)b^BPDjnnt8M=d zn*xwZE)Uz)FA4Qa637Ao#i9TK@COC26l7F8>R5X`jrAGula^T8qU<3yr?Pt)i4+72 zFiBWY>}c7;diFaOZFX1)D)nvs@%p_M3q+raw}Vw7b!g>FV2Myy3b;mQ3#JOLmtIU| zC3P4!C>MNSl@b7Dq)zbVzySb@`4{6$J1*vCLw)d#=*xk@1Cc;p7RC*sEwY4QPQ{76 zn(5j6V^{yUoetDV7z)=$PW;=oi3e_(0MYWBe8?0A+~&Dz*-i)N3FojGwoJ4@B=q$* zf5fJhJ{0=MxtcUGIiyZjc%^?|3i(j_|GBcs#$iT~^6M#uqH$F0D5#I%%(K_-H>7P0 zXH6(|z0B+xNve*+yiH+#*-u9#)OD+6RQhz}*(}M(<-9u$o2^R6p+H%i&+3E);8ecl zBqe;pX$ej7cJEk_FuA_Ko~dSesaj-@*>ycU$pUkvhKB%XkX2Qab zigQO!eo1;6b*6o7?+LxsQ`Z7!SR6V{im(x&kqf10mES>|XyNMqnVX6XcS5QN<%1ak z-l{hImjl6}afn_8HFN!9Iy^Q6dPu+skEqzxe+ku{B2)tYqo?pD)K1~QgkEom4IfF- zRi{2BR>eaCb@*;WI3@{p%!grBw=qaj8s6;G%kepD47aBShj4NDgdc-@=6*ZihoMZ`=-xHJ;e zQ6~6STE@x;D5$w)*M3IB8{Yw9V2@O?&(!{~#&3|3Lr`!;Zr^mdYx(Gwf z{|1w90+^Nkj(D+|RB_N^;E=qiE1P-2Rumo%S(C~(BdYPJ{Ymi~*pqSR-AO^rC*$Be z1|v!=7z#uHq4c1a9F0d+DWolmC1%a;2&lr5AXmp$%Zger5vij9Efhp0^9IUSi*2(`-YY$d{wB3;6@Hen zRYbIiW`qFrup>hHOgeAD`D-)bs6zua+nqlcZmud>9d5zPfxNeb`=2_e z-WO8z$oMmdG2hotfQg3dLA?G2>09C-{7em@QiRIDq<{t#DZ{6bVMpc{jAN~eu$bx* z;SR9JutR|%fiFI~23PcFg$UtJJgT{Xr65WGONeP{QDNAI}jXhvad6P7xgE}aJNI?5Y^TQVD5 zlqHPX-2Hj{I+h5X7UYB3{^M0<{chNH4}q$x=R-hq%IC5-^0*RRto5#B`CrtZ6aNs% zEV+;+Bf82oCw80-tVas+_8s1HYR_7wU$J+zhm(n8K#FVD@jYNi79MzR%G?d7E}Sj( zL*D{M{0tOOH@epu4q63l?j`SI6(xxDIj%;Z=U&awtH}+xlu#teTxu&Ku6PT%NQx?6 z$08KT?Zc;1C4yg#(DLoDPBocGFJQe^{Gb$F2Xdp(fKoURb!RPt&>3}euu_yJgSiIy z!!PiTM1cVY;yU=?k7y5KU4RZ~MQM?cq#^Hq3Q_uzM61$*;3c3&>y5C2NH0)<4{R~k zY>2W5#GUw6F2D{|f}BDEID81aEr7JJk}ZULniyRQRwSLRj$V~3;G1nB1LmA z0+Q|!hASdcP*Lu}$9m9s_5Y!mqQXJtGu0kKCdmmOWjyr|7h>VwRQEbA^)X$l91R7T zr-nnRp*t4#MdLz@;`1jSv=p{)YPzC_*%h zbyebOX|S+N3iixK0c?p)jcZyqF34j$loYGnhN26`2fdNb0E^c4rvN*T6iW(h%E-yj zO3x!ESk|^1tKO11R312~L2MOv2GBToj26#WLjDc>B>1mZ1%RI-C0yrI{Pcn!gU?luXxXmjiOWsC=u`Lhu*1*w zA1ymE9AKu5<2IksDvLI1H>^f1@Qmv2JF)@k!K`ZRqiAtLx&l zn};dBd|j%DBA4>A&ztn1=0nTQj0Q|O>d2Wj$TyvS z@-gX~=1!KiwQG*G=16-qm5+SzZP&2`1>L7w`Eu#UfdCGfQw2R1->?Fc@y%Xw<=|4BE+QJU z3V6G?^RnR>J)%-LbG8|k)F|w@x9Y;7l=xu4TAWN8c0^rk0)@pQs+fp|1LEjXoO0s? zQG0qY1LE5342vbf)@1SWIwr^GMdgH|gv_}Q-fW{dK;rpVb`GcK~@4$ez6;b zTJ*p{l82=*AEg~s6GgF^2TR=w&7qL34P1+or7qMwz|z06LbyIpg$)SX!0%8kAZ!BO z5Mfz=%NU>@$mgCYfn2AsAFC&P37m=yBWSXpw--haCy@qEB|m3L0l8Y#wh%*{jU<^P z37w~Ry&O~|lUH;(W$qaWl~ZE-i9e84D z96KgF9{7UrWOEwY{3)EQmFQB1XF#HMgrps@gyBZq=f$P5BogHAebQTaMs0VSdm6KM zhjEHjGE}c$^o$jZhne`+~G9_Ea z$NW;-in8ldKb$WfrXMgM|A&&TcC5TVrt4DIWqZSVb0n$;r>|QRagQu1D&pI!l~IMD z1zd~sx5T6Y18PSdCmj=(Am8_Bu+FEMV#L!DfUMfX3+-r?3I*kQ024JVwATe2lE6(X z5EPY>kidD=Km%VjKdTF8p2vc6F1D~62V`pnVeb0MXor4%xS(jmLsB@8m_omBC?2%< z${A36IGQ0DW-2iK z7E=f33-W`3^Z+G?x{TIFg*T@{J>+wX#2mxvlgD?fLowK@a)1t3J(vbW@rcE#Ckv1opF)@b)&d}n63@VH z9x9?J>rB7KXH$C@bd#_n+@Y|DxIE9A8NP-8=M_j!oX?tH{5pq)D6)`@(v3-BU^UyR zC{ahfuJJmEQ($$RcPPz+s0e)JYSixV_k>U)j1OpjOyhre7#%K7bE6A{XsT$23lH_q<=gn*FVmCF z=DkBjg^&M)*A*2837@T{_!xO*%eWbfEzNdw9RW6BA?bpGR{+un;jmS(KdOV;@p34V z;>j~%PtXigIf%Sic?%dnaFpl^Pl0>k**)cGuv<($#5!bC0$8<1y^c`Pu!G@HBo^7` zYOdNC10D=2(+#FQR!OOvlmmjzFfAmOFczeR@pqoQTly^OYIWod5JAH5C@5rhpcah? zRvuW7Rv&-zF$V^|GXf(H8&CZ(PzwA4hw%~7NA{gaK3qd3S9OXI>NwvKOC-$F{`t@7 zXk)J1Jj9eS)8N|RV?)~@eG|e{l<5j@Uo<)i)>GWgF2CdQYpL}2xNxpH6(|G?M@Mh{ z{OnLXA2nt^E4WAE!z_L{)*&stA2o6P&EGFu>d0On+`&L;+XV?eaz<$TB@48a28>aH zfBT8lXv?=%fz;OFhGC^*0Sbi4&=EF(*0yLBAEH+K1(Z|Zca3_9tS^jUj4tzu7~B)t z-i%1k_}9=+IBzQz{fZ!3iFBy%{d;%axb3vG7<9gR?wS<%c%kp-%|GmzD+r}~1nu0Y z8rH!qTz=j6E2%W5Gv&Uwf3|{M9}y26KG>TU#PqL2g4rcYJ+_nn-5wrxqlv3uf8?Cy z?I9e7eA$uh6X`%mJ8|uo7P?AEt6n){6tq-2c*Ug)!j$1sm!I8}3|Z~*1LtN@Kpj4R zBGi>5jv#yTN$JND`S?v(VPpXPTf^!Fh>8O2A*+wOB03GX=HI99F*=cnAp#B0oPw$y zbx04D`MAChzzZ&c2BQp1o#H10`rpM8s$LM{&EHpUzYGvM8I|u??PbHEp6hln!8V2X z7YB9uPB#@oeS(-VzKNOChG<#U=H1%dfzDt6M?R>rn2aJ1QCd*JsV4HF)8;VoKW;yy z;}GAtjkze-zdHlWWy*&?GdZ&;ap9MjqMzGxAA1MPrWi`Z(Y<}co_tmc@PIBVorw#E z3R`}3|4~?X1;Nk<-x2@C zS;ev&a3&{$ft#8 z2fD_?)+L`=v`NQ+3fK~s-?Zt({f@3_*9plFgEr1356D>&S=tEBUOMr9uT#8!?V<}# zn-z&v=ZGEh2RB`_VKept#rChDFSG>4zXv@b&c8fd${-wPQK~RJT zA|0j|UI6~VPviLa*jcQ#(awj#N77DKyeQZaajLp?NyjEOzuBJYV%X{cPH78PP`?PH zYj)oy^e&dU_b1niuODh0zj&CEVGISs`SW>pOI&(0i}UI?qby`=^k3eoa_czT+WeI* zvC1AG(W7IHXFGSY(B1#Js+dX*oQ89F65rdh_Mz>lHx>rcs+HeV*@d$U++XlG?p5@8 zj8}kjEB>9f!cdqtWRNi82jH}VoP*!|NMu8?4ldU(&VaLNpyLc0K+FWrkN4Yyh&CFq zP@v(}=tk>OM=t#IFH`B}du6TTR#klv)pbu3VmyIAXAU&Ld#!Kw@JuPQ40TBs%S1T4 zvcP@=TP}Vt+^!nI@5i+w8vUE^+{rG6hz6gW+QWl=n!%5Pf-pTKEH{^x?UyGL&6hFg z#vYk2eoh!TiagQ0J5f62^W~qH-azku6cPJ&TFWv>VN|S84h)94K2}tj68biP!v|{x2Q@F=AEb;vH5HRd{q-w)~NP= zEL|_1_r;2nloP+DgjOXV8-KZ+ve3>kbIuYJ=b74OwXp@zy`rf)-(P)tmdC{F_|sJu z329-cxDPizVX1QdLnEieWs$`K?(Pu}B+G;L!kW_6ud90lu^+gp+mGft*LGw&9}gSOmfHPUYnMPC{yZcAW$a;^=yYnA`@6X|fMFh( zk)%i<_UK=-g0z1Da*2Q|e=ao6Xz}qnte1iVR3T=voW6;ah_&}|@hQ-bc8!YY z17%;VisR z)UzY}o4vn4U}*~cV$EEagj$NT42wJB8)_Y zpxdsgk&*LRZSI3C}fAVB~J?IhS(0m zMw*sRKE{)@Sz6o>VFQAEey5Gk^+7kuy@K`w9Rk2mnba2U8kts;@n6s9Y~$C=sTZW+ z2So!6n_?qPc}iur!2@~WjOm)rSq?esYy zL^oc**gb-3FuM0D?)?o(hdvGaQ*jVv!Cauw5<ZQ9QhdF-Q;L`I>0L%vo4>Nce!L>to)Tis z=eU!XPv=~WXw+Vz-E6X{>fwjXS#s*Fm3rp*)46op$&g;S?U09L<&4cXwa10KFaFsg z=Q3mwz9PpCJ#-0*$g4_RIJ?N5FR@i)kA25&XztmA)A#GhMq%{f<%>TUv)z0*H?%TJ z#wTLIzBqPm9DLX#; zD$oi@iWVG`op8UP-)UL^DoV(tka>dGd|XL*A8#T=%4{0lfKFEm}vY zap?O8kN#%oArK^FUq---e*!u3P+J!&K7GvW!bgJO#?3+uu(M885q;jf1&eX#ph$dI z(rRMB-fDO|4#ch8zAdc$i?PY-xG?SW#hnifrq0G#JOb&nzCC4k$0V5eyH!>)y0ghO=ON-hs{_ z$x&?cz%zdf`Gw`+o}fp#Pl(YkD6%O#58Hu+43q-9h%AerKfrTf`6>Rxv(OTX4xxUK zitp{&{a2wLh393U7;=as|rcLkLp77Pb*5sP%Z_>b`$ zU!M$K^xcgV+rWEksDAVk%apU@MSVH@Za~xa_jWk7F4Xx{dmmBsxA%RU4|`ns_W3tY zjt8GOG3M)65%%Wq#1r6`m(#RRs0wrnMgHOw`vD|QZ8sp>WEntUHv-v)blXu5^#BxP$syL3Bv_he+wC(b>Gr=5$+ z9nahE##wMtcXu(i+TOnMh#HTDnzN<@?qe((`Q?s7!t)rx#(@7fe^=veCxxXuRdo8! z7`f=<&98rDX_2S43)3{h@4o-G!DF{@>ldXTz!q0gozk*C1mVcn;Op}LRgJM@X;GohBU4@kAlYLr*_AQAHR3^Y(v?0x%Z9IrY*Qyui3>#Bk zpULiI=AgW5W<7hTqNXO|s9QcR=w3D4e12Fe|G7C{NzF(Ad3cfOra9Z!$G$xvDv3;r z&6b+~y^?!(iu#2`W5o)=K3frW``kB>^ZV=@$u*!DHSaf=6^jb6_HJ%hD#i=qCT1Pp zcO+99DhcOZa2w9BfAOz-CIQVjv$bci$e4%=vp`0eoC+S`AuTc0(m_E`)lc_Da0a`> zPpM(W@e_;)xgAo0mK-D`H6b=%F(84?n&ec2cd&=*KW5K#CVIXW;ueJlbn_aVh zVKi;+y#+Tr`&v((cOPRnj~PAB9ciw}w{PJ%Ae*X+u0~enFDOM0#@e~r(?|^k)l@nv zUkApdDp{y)`_qotMkR>aryo@mIG8!X5sgPOC@SIJq$>wvy|e0-*-yWVGwEcrC>%sz z@okc@`GQyob~u0Xxbz!8uHB3iEZZm%fNBlgGOap(paKahi-@95f{-ndp4b%lzm;7% zl{aY7urUb$j;@y#LXtkc~8 zT3Ag-mFn`YEpsyCi`Fi475mn)4(Ag%eIE5Qrsi&AX`{3h(}p`m##Z&l4SnbMeBpjK zf(c*->Ms&{dr9~g^}<7LM#Isq(u3P|5ffa~YE(3uZ{4Y>LASDD__llQym>Y^zIHj61njcm!G(#WE!FP@~Ub$=~*rN zn>+ag@>pr)Uv36BSRWB#Ccp}V)Ih^O0Iolw%?EXmE40b~6$)W2KXC=T?4W*#UehX? zq2vlx%qY_TnA+yhvxKK%z=5RciUI14!V@$&{?uy;fcvke%Pip{06C6cL^+qy9uzLo zQy5Yv$^!fso=OL<5j6CA)Ywr0%%?er8G5H6eu{kq)7XHUXEP&^bOgD1aX5<-j9+gs z6n*f*Wul-(DkFVD6lxUUnDVTD79Di~Xj@qNDJC(1#f<_{#nXo)m<$B6EfRhqB+Mz8 zvWQ}l=JV?9!FC<8cI@V#?W6`Gruh~9b5R^&jUIKg`L$Tc(q$=TvKaI~LC{Cxm=i#-b>wzYMUA%{gGx|19j9#VTq ztO{67W*YhH0`&|~D#j1Vft_eWyEZsXDCdCHS;R|eqvKM*Ektz^fl%yf-vc6M#rq)F zx-8jYOTz67llkKMOPO*NAXA1s8|^jC;+!jbNs5F6+7or3`nn*iA5MiI^ou zSa-hTJt!~YOB9kt;=13W;#HUyw5?Gm z20Tt$*MgMes>lhXPPorckN5QqJaC>lck%l>;Y~p z_uc)3-F$nu)-I~K9pF*DThvb;oLNvplVC8^y=>0uG8!&Y=8v1d?GX2xL&BUVrRMQ% z?3LcDfsbJ2(P#=@!kQdJBL&TF@vuIx0@npmIg&sp`?j`^LCgKH2l5%9St1S0OtM;P zTnDmnFr)^n0Vf5oL^YpSJbynRd8m-osfC6VoIE`7RNhDMutwn*UIlqqe4s?en~V}% z8v)OC1&aSC{SjDyGPwXRGSj5maN0zCQ5T&ob_QdF-e=bqw?F8n9++y^@ zeIH_z4w4FX@8XcI{HK{3M44qOT`-HwWPsQk$F`Ubl8owEUHqG~z&jlXpQ%Upa`_Sd zekA;?@VTL)E3D6EjYDIg@NdnJyPAYzyLWg%MSHrj{VJ`9!i6^va-|U0u3$_r^nRN9 zb<@3XWb9mfQv3RepH+&DD>l9k#~=A3j5K_I1)SY0(ECQ)df#e*!$Bksb%(?AXWT$f zA`ZuD62z3H{ zSpn6pXo<9y4^>vwTx|>`*TTQVAHhI{wp)R{J7Oy?Q}prPjxYY3oh@w=#*5b&Oc*&J z>mcrkqx`ZTAE=l#MZ6Y82tI7_0CU^X4Mn#biTMl`X04!#kBq0!%Cv9}`Fg)8-)E{p ze30Lv?d}6%PYRVRhU`a_u-&1Wv5D?xpPJHnMP5VbH?gViA)ma+|^qD z0`SfqQVgqq5!L4-3EMyoJP0jXWIU$3A$a1iISQfSOi>vDMxP!Gqms2s_(o~?wSZ#l zUpQW(z3@KtN~sRY`~N{f|AP}3cB36u_=MJcG2WgHNfmUiYz;N9NBLMJ9~R_UK_ejF zCpeAqU$0w})3wzjshk*y*1LXzDhZ={W%5^Yv=kJuEA-~dbU1Aa69Fk^dW&-r9i+{& zL5yV99}X$n=S(4qDZb(R;zFb(0@{Q`H9qewefPYWsjoS^yu^vgRy?#wZ#T6ido&|w znQHUV?ifx)7chc8B)(<1T1bgT!k?&}cpQB{0V$amo;qtbu+R_5MzK>iY5+DWi5%(G zw4Z1)9`J}CKT^~QWg+_yikIR^#1(+Y*f3GjEpS%EbEBdhaT(DSgu4Nz;d)@f{1h4X zAt@~FHS*S{s@VKOH((&(9N&bYwhdtmabHrvz0%W)hbzJ6hf1o;ylr8WKZ81_WLE&O zCuTNd;uA87!K*Fy)*LQ$^znZ7Z?kgA_gBpy>2W?`@-gW^t^AH8cdtB+Urd>|*NW4{ zw3A26Er!IPvHb90wyt!pejBXdZ4#2p=zJ zZHr$NG*N7x)$WM0;D+y9xRMCE)i9p#0FeVelcWxrw zH{WH2OHA{UVtK^{8;u{%U6_w#N=fck<8>}f4CbSQ%=%LT!w1Kjzrm!Pff(0oMXqML z{;qk$2paKy3&$3%;QZRNk?K?w;rU?fwC3|a?GQf;Mz;LPq|1)1JXb)6>~NWJJ$1pp zJJamd7tvNZ;dbZUuHf#=8f5x0rBLc>Bx)9ax^&KWA7j%GTzdJ>y18i&#Z!kz)^)!# zZ&qZ9=kemEUSZw#`O1rbaVfn4T~3~SK>C6d$K>8D5YgqZ4gQw*gFJu7%Zv(xCBKv` z(4Z7oE2aSwq}@&tFvhJCH*S#)p&~3;l!kYT?*`=eGlmpPwJL|{0#~BQv$anM%!9A1 zYBBkIUHLzRy$gIC<#{fynfZ3++nL#&*`3*)yjS4`@X?5ej|DDYCIf|yA95xUn zw+sVu<^etaKe!Tr{DBq^D{o0=yi*e%O(%kZLi_=rkZB%Tk{Ia{*R>>-3;Mf_}eS`!m+un+kp&bH|s=_+N{JCrPG$+Y4AE`pBg{p5`R@uJ8wL}fw(r#vY&U=TRrRooavEmd^5fY#K9KJN;Z zS6~UM*tv+zi*Q!>ECNwTUSQAS_G2?+gS&uC$-DsM>9t^e0G~pi3I+yF3x)>Qh~V!M zDUTCNg0?+{o4BTDsk{Jy2$skITrjB^18)`o-gbsnl5FA@a9nj|N$tyvojR$h47)H0 zrHhdQ<4bG9!&?D&1)4%YFto~~{$`mA7m|3`s#bI;0lX``{R5^~k(nVh7_6d~9>LQn zlmZBXNs zzBV!dktNPLS33Ey9!xji#I%>AurvkawBNycrGSq>CCsT>=ed-gEkEzvPzfqDH{pI` z1!%8(+@}Bj!}(Kie(V1~_*lfzLR-V1%nP9awO(EFPzWjxdvEeRC$q{*!xuO)J0yid z9b;*T8)t=^gXsKIqZwt7&}QNxPsRPi!Xi8Cw^gMOQU1n>>o^Fv3E4zgOGXoy3L8`L zKr)i3%@l-tvT@aLV>Ky^flYoEN|%M}VqraQIkmo?dBXDx)|%GeKH0A7lFsE&Sbwnl z-2IWv+j7D|W&EV5B+*(X9bk$H>bfqXr6Z_H{D3GbMnus?`6Ls$0ZS(eZMDCU{M}L zC@-94G2#JN5G5e3B}#g64@o?vUR837WMNQPLv)WLLA3D^Y8IuTAi(a3c3x*0QT$J; zj_?^oDJG-~1t%fj7LP1~J|k378Jj)N{x!r=$Ji5wD}PU6$Eq*lS@veB$lk587Y^u9 zs(nymF{A-ZKl}y$&RjU%$njBDymmDMYrn#*_#R+3Gnvwaju2}nSjv9AICk<=%b$NK zyP@OP`8gok2+NNb`EVpy_N!%xfkKlhn%5I@!<`hh;11P| z5V9v|fYj{>-HB%0d=y%yi%;lu^O6MeoWOpOjL`=v1H&1h{)cJQoDyAn3Yiw98>yN@ z*%#J<`b20caE$8a2|gDF%&Td@braT3(UJESac@;$1=pp@6OLnRZw;e*=O0(aY!MZ; zajxt`X8F2q#ddz7QBC_74K3vxYClrq;il#67Jo+G;y|BA}{_tiFT&R5>XI&SFT8J0KVauN`T#H!4hwK*WZ# zWsA(amO>y3FRcx$!ZGH2=x`?j z@jKl~nMz+gcFA)&=Li~DR&RR2)XUZw#8-X|v7zLvL#%ei`f|~HO$hJqXWx0H%Yx#g z6Ic^NhohcltGh7DKw(onbwJnG6{VC*vJFyffePTW9^VYXw%iTOUxjrk1Fa{-Br*yC`GEf6P`w(YbH27zBwkCD5F)^w^(YhD+glmbNU$=!E}SdO z2m$2H;``-eP)*VwMOe;bh5qCGN(|RII=mG)ToSy&$zlFMP+-PfcFf7Wf*UR<*=Kr^ zoj=K`3M;kj<(nL)KheXIui+)XbqZQrFEAUT2KQZ{-F~Tw&XI4LMHc+C9ng%83AasM z+JWgE)lxBK?1T_=nV~%g4GxwO%>fkBi`U(4jkSgj=;a>{Y*9>%bifLf=eOTTeC`73 zY1c~?#3?P9QPb71gIG;)YB;HNS`!^#`~Vs@pxpB#=0bKAxuZM%$0G7DY(`(Ngzeg?R_^19!rW-|3Yjsh^)_oO+%V6SdCf?Y*Xu1CE_3fn*E&1yiN7oAqK zs@D);;F{e!QSN*cT_rs^Di~0NQKpFxJ+yUP4_=|@1KX&v+E4s$oN~|)=>@tBG8LKw zSZ#7}i-qgX=8{%C9N+Vpq{uq*smU1YC3Jt1*8>NE`_`87J~IMhR-!AX_o}%=P`7L&E4;QTsyePVFf4 zGO~U_M-vA+E2mIb(kw;wZBIsgSI;aqs;Zo3tCpAN>9&9C><(1@0_!hq%}d-DG|{H$ zhY4Pj$m?MxZ#0Qw%e+0Z-}uU#5!Z->QmxG{hpOT(dX121m7qY(YdcKYR zJ#QY^IBGTM_h_OsheWWCh6XiK{R%z{(JjuUmu?eW2cL5Ntf!=G6KD|2ZRq*YkD@cJ%yYuB$CU!dFT9U&m+D@LHr4?)dU zv$_4OEq=)nWHOyCO5c(npfzjbdy@#S!!Y682#TXfl{2aP(3U3gfk>?s7ft7&P|QcJ zx3yO*x3n}{yzI{;?o}`@wQSgPclZ=ggvw>B{i5r<95MNadtpO!z&o6VNz#D!kPGrt zXv~6cBC&B`8jv+aDKem?APb3AA&UxU(+5>WuY^!{WT-VrSOMf7v>0^9o2ZCLI0;&O zV46j&3?=oh2dnjnZ{FJ%kKWxmF;;^(`rfmIF%&{I4O2FoA4b%ZL;&npKEKp)6{8I? zDIe)gl=y9JS>a#xb6;(0DvlvF;gHT|^72^`_8$?>kL{`&xyyT4LYUOLYPVhpRlm_| z&-JB+F1^4lU;k{(x-r?ofs>V(Fpt)0*(bN9#fmGaZ8o!{%ejeZa9Lk>|N1MHmfGyi zVKFN#Tsbc&?3e}IPq^T!kU{=OL=N}}ba2~|0TP>xkRFgbJe-k|H5}vf9gVmFbhl8B zMxB`wvU=|vwq9?Mkt89&A>3ZNE%n=tfQfc70Z6#HRNOBpeJDRrC4s_bY|lRO?r4kY zix;Mlgthv?C%-((_@}xDg#i+BVxPghO@@}ZBCq{#D;=EsGanJzx988?Gzt9s`x{ScvPdU%zWCnLIQGY6P0y zI?DdGVBYFp$Q@Hdm)v}?PT|tu9luCo>P54oJ56aLpS=q2oxd-}gg!*Pyjsk61IX|d z52%rM;FD5+xJP%Sm~FA8;ac25uLmYl_2J%~HJlI#y7$QIrpt=;qzoVM%D&@A}_SRwn~LO%(zm^?tZmzvPd-tO@CmD!-WmK&jTAjK&O~n2*(6&?x} z=NdLd8hxmNe^Gi->HvG6Jbo`#TCDSvxu6nNcj45W2fv`V+z7K<(c|RcM`U_#I(gT8#uOb)ZuX>yh91E-j#^HtN5rQv17RGat$1 z<7(FbUpLJv`nBNBhnt#wsT2{E80szLvs%FVTa5a{!`Qj?^IX?CLKH=jw*nK0oXPV) zFsI~@NPd=_Jl&u^m`3W6!_&<~4V<>7i0eJ92IW}_JP|HWGgQpHkL9E)sU6 z{t)~g$lhWfQ>s-j=wcxebP)Bfit#VB@rB!^xm1=#=gBYtN3{}}#j;^PD0H^FKjC)~ zo$%&HOdnVY!C*+sw60>cxyl$M^z&$R(ac?T^=Y(0jYHdG_;UBXoCB3$HoVRm({&fo zs0F1gHgAiP+f`*}z!0ou!P7S_;SGO=w+|zxUois#YZ;|@a`BxQ)%T9_PWf_naxpVbJcBe6=^l&cOM6K_JL|1k2)TlVjBL=GrPE9; zOVpK8fPN`#1FE+0Y!om|w?biEpv;Jcpmd5t1lRF^J4hIL$!{Y*i=e@4GKaSWw-u2I zt)#bf2qWsL7#t!>@gDEbBh6dWNM5)q{HqcW<)r~un9abCHqQ35N+gWRlA!}a^C?G; z!j3DyV~ZMDruJJ_bdv)6XS<~R5>qzdlSjgPCd(6Z0LX2hp-`x^T{ z0zwNr#$#G$*;!?u;vaQS81}i(@5Z%v>ak(BSDBje)vwHnsLX*HAK!bb6l&IKQgJws zw(nBy`@uBkCMGA`ZOA~~PwJy(g`3-j1HQtMvD*AO*_NmMTnzm<NibpxP8s6Q$^^+gZ4v zxFal+T+rBlur|-hn9Y(KO2?r=ZbX^yRfJ{1gp%{OnCTrxN5zyPd}n5Pt}BpP-k9e) zWW`ze^+m~9`{p8@e z|EaVE)Kqx;?m#*;Z=ZfHWW__uKwg|Z9)kd`5{K%GZeO@g38eCqTYHDLVT_g7xe=*- z41?S(^syVhnYC><s} zl8zUtX`a*TQ=v0wOLxsnShalGd@p1BcP3jR9mB>6)31~J>BEG89S0z(N03UD^ z;j7A4id+O?kZ;d(ZLl_xiKj7)HYPcwc!@mSV()TGNRO~~(!iH=JuY2((C0V!<#rUM0 zRlg3PQ;3@YM)9B(a$Yzsu7@OjP$WyM*e((U`W|W<$!>nm?8F zuRtUphFPjEFh}n{o5``}T)Ysxf`wLSJtM5VU=xcTWIVbsoFAL;{$tTn?JMY;OfLS; z%2}0FtyVIT4*G^5&)XR=HQ{G0`MrO^XhQ$5Un&DO>GBnr`#JsKKmm&wwa*j^O&l!yjF?|t!;#qK1 zZXgYMk6D1d7ICI)wz$FdlCDBVAG?%lyx8dE-@T1W_YHRtHy=WY_?vt(bPhqeqENs~ zJIV7?>_j$;d+To&*lFS{4PrxRHEH$J&I&RDdCr9-XwrxO3goe?&KGYJ` z0?Lah=Rheu#g_M;ftRM?PXdcK?b4VS${D!A^)2k8t4reVl9-KwW_C%3CQV50jG$B1 z)iAq7kSn8@X#tm!7X#fKle1kt0NI-4JzN=I{SxHtZeHUUQ|m^#)`H(=(w2-+Cpx%Q z7X@VW5NJjTW#VE9<7sBh(jPuoao-T^h{PV%ZusGYF;PkmjYKaQDw}86!dAm4m74F_JLd4It*5qXV0`+p=U)a^?e}7%943&} zQ2??igc5=ngh#klpb{$Hrp13bNLrrVj(}-IK`3}c=MilgBYqHBoiDghwE(6EW@36P z<4n+E(d~!vp%OeATh88@DJe1krzUrY61`8cyW+-v3-(lBxhm5WN}4AY*hR5} zUA3A&G$F``Fu<5!G+$DLnK>)g&$MN(t)~;Fu^w_vT0D)&N}}otd+0;Nv-Ub(&OgNy zf4IMSC`mxTk3Pru{nZNcfj;B`UxJPU@fZTg11@hk35yY_c+DQre!C6ak7_QAwpZ=$ar`95?>$d6HI3giX%+>7?H?_e&l8(pFu!oUN2 z!4AYv3PK|aTPeiB?xm`jXCY{R{u8fJYm8ZD!q38SYo~Dwyh=#h(>?2gk|iBc^AAfx zyhV~yi;;LXLD?H+U-aWmx@VZVIp|qIYH`@w^ct3g=v*6%-cP_jW^|yma z$S!H(r&fdE@Fm9HR};1OwKJo054qdUq^67{pE0Yy%CvR};$zaD6-y48SshbB$_O*j z!sr$OR2EIS+co%toE)$(n~^OXzF`2Pq1t)XXkR|N_7lxl6|Mm9BvyPU`%ufPONYvk zWw;AI`A+`&IoVG@=V>eUzP`-YzxLHpc9JWDA>VULUruoF#(R3jE zm#evmLK8E=AZ1Ain`BFT6d{NC=+2B!r?KYjl~I3N*gsXZ412WP=}QRLy;VpW{xG^% zAnd$X4}OiE_Ms)ZPxOc65slt7llm) zw%UasrS%9SIy~&nmjw-qY^u{+bt+Y4Q$-oJmukD{QNaS>fZ!NJ>U7S2i@HoQHKi5C-KOd>Dq>+xC=lfzwl@vjTcFP6MXIko~4o4t1)Z*j?1`qPUAVT5h9a1xYYsCGC4*~b zjwb=+a@Y9xt1%QJ#dTj4{Vy@D54kq`w;5UYZ-no&p-q?MqJBIudRtublbXT@)x*MZSrIEsguS-@bl>?AyRHU1P;fRtdgrb~hJO3WW~?-ThK zp(hnoQj9oQ5%j{oCp9P0^ zmPIg2!XKofKwlE6_iNCH@J0200P)wNA?QUK6y1utmyy}gYljO-T?g+|88))*!c3Gp z^3j{;pH!PH?zMhc8bp!I`UZag zfTci)ykAzgL=3+aDYw+RVo43y>J-l5BdC#@$jIB!qq_>TS)agq<4i?h>^y!1{6(Qk zC25bPBm+Qd2HV1kcxFzndTa~46+CQDnxzpo5_AlS+!|zA5F|i9Z25WcPJ;K0fD9UW29M?`b=_rQ0In^$82Zlso zWENjq!DO`~!liSifsU$qQnFlNC3_h}6JkcTV#UId)i%0}lp#) zypkML<}Bxr;uSz9x&hWRWoHw76)G}=hEZhY+IuCav0&@CW?$SAQoX{4M4W$(Js9}u z4Q!A*LiwiZ&Q7j<|FtpQ% zZC>t2aquHp(>PdYcEe_`_JGJDN9luWL$w{p6!#4zYNA5JEBSE86}tB#gPz1Ii78dc&w6$#vpzFhd7{)m!oQTUJ`4hCTqZ&ZYR zn<<8qycq5&@WNC~vjQ&+&Kvt4RoQC6ql}(!{(yuL^+h5sfQ1|p2&64g$N;ZT_ZXw| z9BMPHYf3r_xVz+$=s3`ua7ZGK7a)+Sr2L_tz@d17dkj-c3Az`5kVgbuCj1EZQy`)D zKrKDSr#OZcL(KNB{N@>G*6fTG*S712WQfr4@|mGS;#!)77cjcoJ{?NL|IDoIURO!W zkdIj^tgzV95rsYQTW5S`YmP&aXg|i%UW+@QG|N!H(yiSfrC2vu`p=9G>5Cssq(M^H{}*{KuxszRXcmsKvYUgwLA}!VN`Hac*&OWPyD8JgMkP~^ z?M75DDx<+H5Mv#<19mk2;P%5t;F%so#l*wtuTJN9xc5y2q9ZyWgF;pYAMUk0$b$I$ z&E5{iIjAogc`lDiMiB>vYtzh17%Gas2~L8kqtFdeg%nH~7=DAs=UQ6n0$2K< z{#UJurl>ajd5DGrAn=Czhu{v__57OwNU= z=rJ)F0BJf86qp`v!mlCRU4AJI=WsE!qip$|B#D}?1ClEKKuWw3;n{z`%?Qx& zym#X%qC>n@Uo(QNnql-|m7ty;L<8)wh)d)A{E@KIEW{WHBjr9JBm|meHaIVA1a>UU zWY2BzuRpPwPd&I>+*IrKC3}R&l|Wh+F-Ra-`$dx|q$4#HUQFNZ&u%=ZnfB^Wk4e%< zFFIB=!@0VDQSIO6UnL3Y3pUE))@Cddh$nx*fR~7UU;|oInVOUvW5Q#TLq;fc&F4ne z4*DKsPvFl}%0s@F{eClHnSS}9D4HpQp`hHG4-}Dd{PjgMTtrw}`^Gt=E z{=jYX(rq9_=NYT8MwGgIY~Epc5d3(&6g(t?Uq9Y~q|jqUc-9Eu9vOOA6k@EPY*2sL zN3;~+f%LWT8Str>+{oDDa~TH44n6YE&9T;2!zO+M+t#N4o1f{w-rlP7iA_C6;%k04 zlSR`X4SixJ$g1ySEO0llKfS{4zNl+In=C6irmtl`Z`^I=1_fo))72UBM=aWhq&pzT z3=MW>YAe?s_ZL1j+en@JQMa$(7A`EzKI-Vg`XivnW{YznLuW%`YQ`D{z{p)tS_XcL z5>wT(q1odL%I}rXyp_L0yJ(A~-Ldk)qzx}2UzCuSLrtQjpw*<*qS4rPL@Fg?nz_K}5Y0*}3J=Hd7#`-aoRHb+qD0z9Pz58Jc}<{D)KJa);7tQ&rNAReV9%Y7J^o#LHNdy*`KUVAQ6YG zsDH=eQ;wZPywKYWB_@FH!UMe*qcw6rCi1?^+Ls!!@fiP~N&PAV4U0Xg*WE$6^`@qt z*)|^=D=W5wL(+I*?oIPuh=yzH_f?YT9hE1wJ&;0g>6e$?x^H`Fg_VC1c`w9)@SeF&lvlMqhQNK>Ri`@>NU{B6EdB zkQFjmRM?xQ8G0-X2O%YlPz4+aO6@6EuYU`Y{eJM2J-NAz=%tJx)Opm5x-UV-h$X}C z<|!&uJ|Yc&J@{blFd7MSZPBoq6VjV7KQUG5jDK2zY>brdPR0p8uT6H(74V2-T#*SjnN~+n!0l=SoofgCn7#Bzh|c~S}OW0 z1?2b$ynyzYPl$z8`E5vt3v!DTNvTlt^!;@ay`vyB{?aS^@$kTrdf|_UKd$^$XE#TY zkjkPj;%7TAQ54f3GF2#2TbTXgNAUH~VwM?;rnK(X@r#0?shu;mVA!W!Fk3JAF`w@$ zh+{M$FC-4b8{mDlV20~Vpa3-W?0SmB2%{mO21f`ciDF(arlLcFgGiM(%J&)}@g8yD z-Q_Bk#HnKirwQ-$omB|Bg(;8`-M9(o24lUV#@1+6|; zy0`d*?obVyUvEm$8NF9(M=|u&oN7XUmhktrha`UJ^2gY!ydSvZqv{dN8bKqL&ThU0viEip8e)OJ9v{|XqzK3ZoNX0O8#~tE}HN|&?%?Ap)7QBhUl;V??VKQa!kCHj7bO@uw zm|l#=Gf$ovuAL}n7v+U~a_EBj0r7drrgwTh&kw-IM4+V{ftqr6!`X({aF=oGD)pe2 zrf$_spM6b;d+BCYT}rgQrzzN_)&>+(BQ&a!j27-IrX|2Tk;gzyk8(RNXht4F9tXJw z7LmX*2&>`~Hs$D8OhU;JH6uVEArFF|ag@9Sn*wdGj)&kE-wlHcOc`J%;4+fx!omVX z1rOzX(&Az=+^lYjfdi;(1i^}NJx!~6Xot$<%BN zV-1A$NxNuRJd1)Ki!>|2G@|Tc7THo#5yJM`L{-CDaqjzh2)Wo`S56j+Sq?Ru=+*82mDhO4^ z8QZ(S3_{LpZ$4~czC{EV{4knghCe>X(450p+8rI8JeH)PXh)&W&6c8lJP}vpgM*9i zPy*+E5Yiy5@j&^eAQvvX%$CTuk;~d6B;AP*!bSeKACzKSeKaKbGBjFKDm1NR0hqx|Xh34|qp-nu2ag1=P^(WweMF(hscNP5BF1;%#DzZc$Q-1%adV!** z)>QY&><_6-y!C66+oDxeaJgjD#!w^&{4i)`^ScF*+MKe^uaCw&T^FZL1 z0BLWNP^zKy24iM`w@q~Vh*=1Js81CGfvDO!Bh*&1)OectN7~!xRMSwFA|00IWz|@# zjmFyEKt_B{59dv4P9{yt5P|m&V5|YD@W`B;F_}^Da*6`!qht!Z+YRqqnzHdOqf4 zfBeZ4P$Rhd4nRq=O6FBRa6GPZ>8`B{^AMpPmx|_lINW?dMr%;a13JFG2sP%fRda0> z7E!lvZp(3}(8Y&h-|2^lr_fO=HZ}QIv7pL9IRYfP?LHU;c!2K56YR5LDSN>UAw#ybE8t(;Yhy~3fiW1+g3fHsu}xXz}LPFMlh)hVyWL09xXI$ zG5>bI&_p&%U6%~|BpLHSE`gY_N&Tb2?T~TyvsxnKBsW+}lP3JyuK99~G|#|T*-&ov zh3D?J1k5jiNL4H$JUi8oa?9z@j;v5>mHtN6=jXl+j(!CC=qwWk7B-3)Kv6+FU1mz6 z&lBKR79#|Zq5B{z$6_fX=5{ePBl$4Pmw^*~k91ZlBL`i9Zuq~gBP+BT9-ExX6w?bf zEQGNuyw&f={^;)^JK&Y4Xg$a`V5iWY@$5aVDyctHdh@3mu^zBO`vmJq`^v*`;EnN( z7$qY0co%|=P@uH!T?5D&u3LrH2EKvnW0X*lsqv&}@tL|}7IqK-PYV;`ruQdlbJc%A z+Q34T729XD6UW-ewzR8S;IgZ4QjKO^+iYlJ42+XKK20~=fPgsxXi7QQx}$YS>*n5d(b?IgD=$^n9)lidv>4T$x_KEEvC_ zxj)D;tIVe?vcw*Dn`X4N2y-GaAJ24{D;N2cvAJ?iuboLJvzodBbvtE9(sxK`!2eCc zpG_ZH=n0c)@vQLl(WGpK@BZDne+UOuRb8p{jf8a3v}bnm0SvlDihDa)#sMy?Yf9mO z?ANP{^s;CI~bN za5@*a1(UB&;jv&%n_8~*P6kSVwiv$LyzO0 zu1~H<2kNK!V6c}9d1wrnw?r{LjrqHolq#WtIMP3%qoh}y9Ei)Z(E_-;h-z7*-l8dq zK2|P;v`s~bM}l04AY60_|vPcnl zzrQf!5mO)A^OgIYC`xN(;h=khJ^#{(3GP4PhctWc4b(7ZHHX{=b3X0)Lxmq zKXcZcFSN}I7twQ#!G{>Ik=8MnmoLoo+KCtRj^ezM`ql#5a9MkHk%y6K~fJeVrc6q5D#xgH^~9ti^b`WPVj zIJyH=g7v|1r!_o8uVnXXy`#@v}BHcxIVYxXaDhTNX9 znC8UzoRqdMRxI(34d_r#Vo<1ZS#N4Mm{uNEs^hM?c1Vffw1KpTKlX8Vlid1l^(?b) z()z#7(53h4a}g6f9ARNRpL=bf_S+-v^A&$F>z=y*_U$wE3k1HSF(#qI65`FaQA#Pg z*@%%jm>iF(*~ORGrT}!enfP{e6w^5Bu*x3Y-Nn-2Go+}94L~Pt+TPPe5TGL}FOvKq z5_Z~7SbiX0UcipC>4oxGaI)GEXvBA67Zq`Iafb0C_6H6(el-|tT#X1CmAaG}_9^WX zSNIfk0A(-~+V34?)(t=n>T;1C`X$*6kXFXE1LbJI9`5#0NT8h zX8E|OpLXqAdd9I+_&FN2r6Y$l-QE7qNN_T=qOq?_O07$!_9pFdzCfML*1n}RCS5LjPMYq44oNJ_n{J_Cuw)xUO zh59fY4FgF$LU=F+*BJ#Fsi zV5G0c(wI*a*MH z%CN{9c)IYPbqCw`M8e=j*bj162${m)Cenv;#;MdgV4zrw+JCyA#9`%$FTqituKj#C zvV_?gjKyF)e3<`znp2;{E8+Be29v_u6Fxgud)I~;9@)jt#EMGbO_RNY6@d<!4CA+v2vu4x$6-Zp`pz@o)h*{&O-oO5( z4ghq6sh;b;hX$0^OAVp?DaTb?Q|MA;jagc7Ex^|16kP`Z+k7C)ZH~4Itx@u86IdfZ zdP?q=o|C$Ol}5o;qAsoZu+Y03?(t%)2vR3V76TH4;Gok(pcv6EQTj)fjhv&;Cj6@z zOr{=kkyZKt0cThuI;+ru!BYf{QJCf-YxDxlC?IXDYP5nXHq5!@MN-bRFUDeljexU> zwe?Ofny~N@3dry|FL0{%2+P^m)`>`QUptI@-Xa9R?mo&p`5J*|e3l`m7uC-712t2m z7umgke|2MQb?u8tQwm^YKK9QWI$#(v2^d2kYK4NY`w^}W7e6v|p>L9Zdf87f7vzF3 zJgKB&uMKJXk-PfJAa2Cf+GR^o##H0Ff4klNjr$GWDRl#DBWnIO$fD>$P|SJ^Lk+yv zXuu!X(%E;FGN?{^m8kX;7%ESyan!&Ekx=lL0LDS+Jqty4e{$-!9-%``2SdxZuWoS; zlx%KPFkNb7M*y@YgZNT+YNM=j^1c~3S4GSa`XbgW-EdpOXBs}+@P&q7Q(ooOfT+3D zD*?eyA-6($Lsl(pQ8r81SU;g{)$?7clr!Tfc*s~CC__)Gi)ZTC7*Z_{Jj0?=I*yze zhxYsrK+ryZj2DoIO~0a`EFP&wPy@#yt~%#aD7a)qVlrxf=aD`9fV_SB`=SKM5M2S~ zsCxO_t4oPqx>rXFX+jdLF{ON+f}M^vEr7&eB!epXDqG)t_Jx4rSU{pAEtQGOEHJRB zD1>sca3Z)n76xfg%;%Vv4Bn!n|5sEZVe8G6F+-Gi#0-5wSQQQl80Q-Zf9|uIsY(c4 zO}qB5pO?;@5zd@BbC(+Niy;(=qF1kbL68L7ucpT?Nv~1@vaIL8lMqB9gc|M`3l%iU zSgS@cG8+3KpxJ(-x3^LDclD-3=y^qa5`=k?#s$Ye5jyvboeKD*LR`3S@&d(gUMuw1 zmKbq0#6|dgC-|D4%|&F@UUtJ8QxQc$HEj-2->A~kOXKPYz6(L|(`vN=)u9;zP2-}D zv3Rl&3`0+X5BV^N4L(T~{j)KokBNrORZ-~lvmSqkf5_kIA6EW1h7=j-X7tUJzdu_L zWC*UcU$+PXg~5!-K=m|3h(LG?!#tRvIx><8X#Q-e#V@Dg&=||ihu~D**T0PToNCOY zsBa#G8WNBmEG8Ld^7TlyfWHB@BX?hb;w|}T*j!j-0PIziUwW&s0pxL!a}f)S!ZY-% zRp1&BYvU3WBT(@Qxs_KCQ}7hLJeZ3pV8VW*N)01I#@vg~;rPS!?FipPRB@@JeBmn0 zm*-rtS1CQfw~>Gk{WhNO_Z*WKvQMCme|!v3*r;iOl>OX1GcUv24uGPXoB-kb`^8;Z z|J7pc-rS5H2=lb`2<^PsdRJ2VmW1KX?94bzH8b^MxjlFXIQwXK(vIuL^`U~CUMJUn z)>w!xLuQ=>iH@;AdX=mf25evv6Sh=Co;)OpLW%*U+ z@-EY4Rv>!-Ar~y476^CW3My>)G_F)kO$fd0fG&rQEB+qSdQ|T`RVq&GjwPJd^sX{1 z*+V)rE?~;_jcjepm7!8O*I!(tnr}9;MWPC)>Id`ijTRFELx>h-{Y>UrJ`Z8}w{d7uH8c}L2DE<{sc=_+!iHmI z-K}-&t%*=1Kir5pkq~zoJtBu8gaMWY%cL8<>K$`{1}nV-@pbzJP{_kE`R1gaSiW|# zqn}|Hr`g<&8$+teV&8&%7tiK;Hlt0k7b`!nAV9|GGXX1p4LiLnhAF_7=)Vqiv)TDd zRfVdzhzwJTzv_X|zFg;i#}ChlFL9 zD~(5r>~!xnDVNv2y*!0+0gDACb6{dzz3Y3ICe=9e&j-RhagQ>v#xRddyErqDRI%5J zDGbhXL5L6)ot@JBD|7?gw@*HUu~_g~_1bp?5ywiPH+!Hu^XG*1Q}7RN+32_La23FYhL|2w#Ro1tzkJbRO792R`9QU5T)8sBAwxEZ0iK>HGDhAKrKuv zm=HE6&0Qv+sI?%#Zl8nCIw@c}zWIYD`@j}jM_I6sjjWD2{i(26RBCT;S5RNaUPz+R zkIV91DByySc$;gZq-rl1)D*O^TnJ5ZlXvD!49uP%vcZZZU0#eg-v-r3R*`@(VT@(R zY|OnBf}7+3ceuXGkoypiG;ikY!uh(pbN|HnRBV3Nqr*x}T0bN#UN^`k*n?QL@oELU z*Wwj2M^FO;Hsuz_)?!QG_tCA(1DWZZ|6Wh0MS=Qbt7j3mH2fLGl(y645q+_YxYK~| zu-deEfB=vj5^_T+z_bLv53dk^p+2IR9s{PKDlC>5E~K6p(9y4_cCak<6|eK`vH0XY zsLW1>n`WGo8-f|3Rv|k4lgjIdHb0BDeGJ}w4K2XFLj1nq~aSmi0?%WX3v(?s2oA$IgiRS=wl9W}T~78GjOO8EF{Ay*pEwu{MN`*ZiCP;9<@||b?D&CC zw8EcTub|IpmlP?YJEW+$NOsSp8s8y+GJnfhsRA}cC<+~k^&OmTMCQ+h4VF3H(->=9 zl=@-TH!zM_=NqH*gu@ah2mnz+FIYGJp2E-?KsLBC8ZcoXfFN*w2;hXxC*d{DH%3E7aZHL~qJsfJAF`h(Q)IRMvFxVpc{_X<5MuneW z-|!MnH4HG$GclF0EM5%dohL8nhLrHw~gtJ468t=d#iB5kn zRfK@Ig$b#p=6N}CzTX%tNO>UvKjmX>}L^TN0SIS$XRQy-uO`;G2-4}68;#_xiw z(=P~5#Q?S!XZ&NIHnnp zAv0EcZ)g*qFpU)E1FdagUv1Gb(Dbr^@=A;GW?CFsy(EsA1de$`@l9~ly}$Yu9I-S4 zY}9GB-bq(j5s0$(f{w&W>6Y2Zufc2kfQ`pgBH?uA|EYvvOO0weQbx3&>?{NfW{2bU z`dQu#A4b0nzPKoCF_VQSyVbe1Wo~hX@TAbZ51h77n&y#dD zzXbkb2>9)%8@|}^CQdzJtXO6;of2HCw;nxk*wp<8xPX?H>SI+;zyJ$RIq1tu_n?*>2bnJn4mS;HP3=!88!w13!&y0ngNF zQ2omi81o{wSyhW=+p~wC#(jkful+#U^V}o3boRZK0vw$VcfwTk$|5Qplfa5tnS)643Qlp|KN}E`=+QY2S zOC}pRiSmcueC}BqrA*US6}B4E0N>HYlbB3^6|kDbb3ZewjS%z^I>W;Eg=d-EnuXn& zk6b|HlXom#up>;drer(ISP5wbPCcVOer*KxlM0axB0-zU?U>cn#44^2bp#$3l~@w+ z2No|h0AR`C*^~~YF#D3Af(q~LUnw?bzF!T-v(JFGjLBXR%;mxNO>D^?$zmw?v78iN z_geDZSMz}|crj`@Ew){|ZSwmwwz!Jx^{o?EqaAn=`Stk?->Lg_99|qW3gkR=&ml4* z!FJ5T$p_p=tJhcXxE)vsi;lS`xb3Y}6-zd)!G!pVPAjc90<-=;&$%GhhYQF5E40-0 z7!9Dj62T!SOMnOhX#njx8uU_B;Khj?iH!rtk9~#>SOMLjuS$<77N7vm`+&#z08j;o zf-k{_rM)5iJMQ_3Va&wn{vK@$_HK#QJ_One%bV}c*DfAup2@fz1m=U4Tr$%fd0r6i zP=@r_x^x5^2~;ZOBs@3ryUw|Wu2*(q_G}?i`^cDhQOg-t`NJf}NrAn`&lnS9QAGhu zbhY{8T$Po7$k=5$v~B6;X_UXBzBQ4ub7?Ti01=;%hIuf;G5LJ%4!{#6xDBE>ay4pu zK~KX37SGP^-N|n51}m}@JGVPFzNbswEHutdfk9^^N0^4raWGEGy51htTBK?OP)V^8 z2nO~_ie5IAaloBlYgFSFDTog3{& zd1wmQBifuK8yjjG9qu{-XT1| z$`QucOzRG#LM}me>tO>t1;BoU{nJoBa6=qqC4v^>BF#U>W}zSvs|{8x9(lHhumFHh zo?*h)BuhiPgLWGYe?)wO!h_dbOJElT1Vk^wWs0Ul3hyvv$U7WGjua$(vOxO4^ot>; z5MDOzflshIuIqqLI1uz*-Kakb)c8}l2P}`O+36N`LqE1k_+JNK=?aMqvD+w1S<9S~ zFD!J2)DK2b1lsjNve@!q%-R8+6>=vG#c`tk%$lKdPylZmLpbdg(L>3G^qob%FR2$% zQG=Z>#dN|5ZAByWg4<-03n;_GWx$@GH6{$eZZ%-LETu;{h3KnCkjmS1bXAcVy;iH7N>o zEqs;o$Nc`wvaXXJfacefUcWXi!kJXRSpY~eaR(~BX2{LGs9Op7b5x+tL69^#|i zFow)~hNy^(nZF^F4Yc(=*UYX+kH!nBqjJ=Sf2E}dG~~?e)E#XeBm?$WpBbO(hn>7? zfl{@P@SUR_aXq(m#!}Z|Cu6L)*I7G;?R&9 z{h{>?H=Zu>raNMZ(9U(3-F{ugvZY?0{7J}&eU;7I^K$0kKq)6YtN4SF6YR;;mkjV0 zeG%FxCh%(}86;}rg$m1bC#RBI;T)8sIv1lVV+saG=a&Kn(f zGUl+@2qGy}M~Z;8_9jTdtb1!cU4zBmQR)^XkF&2(fKcyaq;rfTi?TA@2n7pB_nmr! z5VhH2)6iE=7jxkc>is~72=E%L0ABOf-+ND0R%mBYH3GNl%pp`dS zspzxTzgvulpyJ5tysV8qoQY*?H3t(IBf;q9eEto| z>Njn5Hb#=dpc>incTf&)dzA^#TXGPDh*KKhy{j7eg8hDDcFs)g>D276viNgZ-pM8^ z(Z}FcPG>O}fbm~$3LFbxRBTLl7-)y<`FU$JxKQV3wZ>owbo%$p;d`~-4ehmuwzlS z`n|{>B*DVoj#}`g4WDdyz^evhRj?=s<|vu-)+diuBWvy9Fi7$}%T4AVP8ti-3(^-z`1?7_- zrU1EISfXBu90iu2plIwZk7P=CA`Aw$13V2m3vbsn04Mm(u`b7)b=&EdaXF|Jme{r0 zBzItR4gL-+Yl)9gCm4)7<_E22HtNbS@;Er}y(2Mg9fq!7(Te`}k+*&V zu(x)(qC8PnUL4CAx>@@`VLm70x7b;!F}V$arCCz+i84DeAqdMF!`kX=kHp5F&kZv9 z0;ThC?(6QZiX}w&5q?n4np6ANnaW14_p~JECSv2si)UWGH9VUsMIQB)az`weH)?hu z_=%snld>F3NmD7dy0Me_G6=yzF!D-O?9`BM!H1;KUe93sgA19B#Fr(l@&1;Yn|i!6 zlB})i$m-cG!qy|4fAb%U(2+@jAu_&rEB9IX>jU$Jt?1m3+1V-dC-|0OZ}>fK=wD)H zDCyB^4JR6&ZFr;MZyNr&;XjaJpakUByDY0Bt^0Yh=N~6|$O;kl9Fz{=Mv!gbG0_F- zA)W_y(1UNipOAuLC=NZ1Bmh_b__5EM5`q^H3%H)ksehb)gjoP1a9EsHTPJ(;|HU(H zm=@${h7m!f3~>W?CIuGMEb4u&+*p?y$5rcZnr^mg2GJ2slVfc3*K^sN!*hk<@5G?L zYG9PYh!fG{cA~Z-no7n3Y#usm^=Pv&AKDmpD##Yfs-!_;m^b^;1+M38=_)O!+95o& z*tx%t#6m%z614SpUvng;#8ChKVYZgWB!Yv|8R;YxXwFJskc6KM>xRfRCFE{E zN&+?cfEdlB0!m<$v^5t5Zf@z zHIP^+rJ=@kw`~?VA1h{~L?-_x}Wo=xoq%Ybm3?);8T0j>+OWSa#_$KwP~fH8G0@6f^W!Ry>v_+T7`p?LiP?>)XhqDtr65$xE^6;WU@_e7gm9U067G`HTVK3gL3$$}7K`X>yHo ze`V`Og-VE8w*9sqV!qqM5eTc8yTsXMB3uk(6a~`|ca2`=SiwwwwxEGcoHqpVU~FXW z&VcT!(Jk4IYb~X^JYR`nBvIH`?L}9e*_lY+m`rKd0O!Uq1J)?bkB6b+$=WKRkT!Yw zPvuKO&R@wZ<#&}yW%o_nKXH|xaSZ251W-B^bWs6G8S_bZV8~4|EK29T>-f+?Ff%pW zs1)){+%MX9<nYI-^CZJwPJW`rZLUPvH;geDM5 zfKbws@;-%@v~*fJrLWM^7N)d7Uxs#QOMBW5?JzA&xidXyW?JU7(|KX$w5R9J?LDaX zySBo)^T$1(3oTJ>X=_W*de*bnZ~fM95nSRIZ>^I5} z1T)0Lyo;nGl0g#*g<$O|^%LPP6x+Wu8+V;)*jDD_{Oq1<{9mh^KHxU4{xrP&hPr8H z?F#eWTUF)uCS!MY-nk_nuDj;wOw3!c`t+na>=dDFt1Ian{vC;cGo#$RvDxf1_GUj; zGpkjOruIkHRHOMM>T;1tLtom?YJ5TYo@p7A&NPe%BOBjYVcWeH7Sv1aFZ{A(F@e`#`H#Vv7j5=8xbN!eOE2d@DQi zdz>9XO$n+>AYLU2AkcmjXd=LSgYRCxM80j7Gth+(ON0m?T121d^Ghsxo~+_y9NbE$s>mxDv&sk`F$FG2 zOj1LR3t4)0E@mX@t;Cx_r9&W1&=NUIc-@<3Gp2os`lGR;ya(!71OZ5M?e~+6umVSN z>Hx(sdso4~9Zq(~PdWY`KhphgH&SPbGO>V89NMYr%*G~!uG&H1Tm zlj1-lv;5+X`ccNFn|1gN#S&g7zjAM;o9 ze)*R4kV)`xG$(+*k;Tqx+IrWODDI`Z)O>rTlIu%^_+!NUhJDwX(zV|!H**G z50ur1n6u}RoZ2>pZe#E8emCRhH=S^{Rx5*hdl+gG$(GNAqjOuju42^W^KM-|aEVIa zcESFlxwx?Ha%Ed1r;BRvILt&Ky=43Mdy@U|ZeG`n_U}+6n!55iT^~`?^})~A%fL1Iq8ZMM3g+khLAiwb8LX;G32(2!0__Iv7O99*&EbTC8vAc83S$$Sw*sa?R+j=_Ou^K;@NC}?W{6y)xK~)*+=4_ zRft}J(xvWg!Sb{vZvz~$Hm&`(Frc^&zC{{A#k<|*N zA%H#c7h(b}arvSIOaRMsn-U!& zp`I~cd}@1IZEF9-sj0Bckm8hAek)T_?GMf_xyY#L`UMlF`BOvtOX||yw6e_B5{tl5 zHC~rj|MO4!M!W?$<1(f&o3Ly1iP52=J*$Rt-A^f1sMP)LmE#C*JG~GQhmVvJ)}OxdmyOcD-m>JMSZ-@O z?pDS4*xuMCRq^&jEMvUnr5%Pn?>EQo(tgMXk39V_R@d|!vEgFR#w;Pf3Xbu-%UO|Z{-N5sm0HSAh9(c4+({e268f%#!w1ySoc8(1I;N>M7HyZow3GCe zD1W!>pr5&GNG~LJ^%MC%WS7i!H*ar$cVzIO4~OaLl|%J27i~48|M|+b`)jeDo*g6$ z$wa!kp2;+Kr<9+Vbf|U2j5WHNPZmljI&zlziM1;du}DwGd~~KYfPZ09m(SaQm%w>i zNJOe?GxaUEFHss?jC7glyiqz3%tNZdfqe|j+F!O8=Gs%87~*^=z{c!gP55;DG$$@zU@&eSlr6=LeCOzl{>ppXRSG4==9zkiJG3tFl zzih{$9GY2XW}d=8-dvhPj(NKE`9~FfWT*FC`dsb$FeE9lr6&4XeLUhcNmMAnwY1Cg z1v_jXaGhiintYK9eBa%A83eQgW4?Q`1=T|8+3C$CsFqm!SY!Re^{=#ldtoK+_MF?g z@}-9i?Z{g*0q4j=|KCD9Z|<|zeHRYnoDRf3N`*5Q z)phDg>X=AKm{VF$q3hPobF-5dz30(})^kPlcXOt{XU5UizK`s~C@sOAGBc^vWGz8A zGl1k;v}BD;WC-2{+=W^?L@#q9&nRJ`=w$|!dV7z`d)8;2O#TDw66W={(gxoRx+M+S zHU+I&J|Xz##zSVgaJPI*MvlzIxV+QD6b8rq5VkbPnUX<8>6CP;LuU*edCSKK$gT}) zSUC00=EmpiozjEkhXOrHTEOxO=_#;?^E{>iEfPZ#t{G1a=q>OWn3;}U1inQk20(lf zbNgS$Cgm<^h8L3qn3U`KJU~b?xJakOCW$O|8$=fLuWGNTTy)pC`DMK#bkY2CW{>5n zC;xiD;(K6N*%@?&lMm84#%R<(`sZ}X@Lno1jMM#%GO9VUuaTC0w)_M_9eJkNxMqRl zVCz=%J3ZPr&93h9WqZqq(sMF+YSLGWrF7})XnymgdGQw0>+_A9?E0Avsy_eRJe5m( z!%0V2?M9yeyzXTb2Zoa|``XtP56#gIis)zJmB%(wC+|)dGk0WL zb@R#V)yw{dTf#>B-+#YsE(>sy(LQn28k|t|!|BM%nv+^V%@P-+R(B`k>sB6Tp3c$m zv8H(nMk10U^9roJs4}|Qpokh5y)6ItUk9rW2YqUynKew8>D|k|AuAnhX2h~kYT+Od zcu*=9&50H(#9~S%O7b-piv=V}Y{?hT0#VSUE(wazJilBSwlq(anW1vT)20-}baJNf z*P2w&B`yeRkPu~vA!Q-u#33?}Dr)1gTxpk7HlDJszHQ+L_mjae38Y@r9!>t*yk+`*E89nGR~vlte&cPw8ea|P z#qac(ek8z$63g<;IHhh)P%it7RU{T~vGPH=;(R0-kxnHkJF^BsMo5T;(+~WyMDMcw zg5VufA+wO>l>JD0h7;hSU`b>XVl4d0a$z(47cbHY4aEbD3sfP+hYqMbA!{y~pJ-;{ zG&+ZZ#G8@?a+4UUFF%&&NDj!O&@#RfD{3B8{-=*rM#!|M-RQMyz%>qK7>o!*#i;Gc zoIULQE1^I+gMDPSMpS(>+Fs+)u7^~kaCPyom3lnd{qpNQ*{5a)f0FSZJwt<{fjY#HKq6<4uJ|lvvm{)+j=pQ?)1|9Q3M0^`XaFOXH~rRJ2nH#{&|&#qmZ?Hae)*Z zRV+oK^rX(l;&zD?b>t93y$joqYsO1G;fC6~BA<+F}{rzL;bazp$x*Qh!P0OCpLfC*uXx9;vh{#+dU{_D^M}Xjt zgnA>r!P+!~ln{`AvL%2pyEX_Fk(2afv89H<+R0v*?wpV)RrGaq*LcZTkLpws_~}^A zr|9KNL4%hf)b=K#9Q4|Bt`=swWmud%@l7Pz>r_g3um6u%NMlZL9qHxaMz{|Q%Ron= zqdmthbL*YQ*j*qon#0>oYUS>nyDyT-*wMLcvOdaKytyB_VLFWU>)&{Mq?KkEdUd`( zrgCL}CKvAxH#|KT_wCYZEfsP|NDA94iP%|fwvbEsqfRbf?yr3}U39a(5yy;m?e9wL z)?%@QKhWda`E>5o-by?gt9b3Fvq_u%lTJLK&5}#@#Y4XgSw^gPI+xESy?FlqVlp4I zyK`4wTTBCI=fC7yweU^le7GwYy-r)<_QwnauZ$_}!dQ1fh4gCNx@QdDe8}_?(fE~a z(jBgD@M(V0;!f2Wti(NPr;*FhbhSeXe`LK8Dk7=)53^2OcdSUtRXvMWzVXqtZWqFO zrAvEyaH-o(e|=@Dt84v;6Hj&Z58YFUdjtJu%#LKft|Cn@&pO?(p% z;Mib@avX7R^g^(fj7E4jxE@XyY1rVo;;SUQ>T%d6ezlGPsQ;D)HSp}IjNogw*0X{77Nnfk6` z_Nt+xQ>VYvfDm#(@_H-7?>KS|r)R#0!Wn>*06{r*AP7fxWS5a8gswi?H zl#}s`HKMC%#N5o>ZDrZw1!dq{CY{-sEIJ67L*Q0RP54*x3->_5tcm_G_2FWXuD}CF z+fPLzrEMm@OM9EQozCtWdR<0-rW$o~>y(unG)_~`-K;8WXNuEBadNGpYZqMe@6a}+ z&oS{_{(<4^T8r&Rsv)X8@zobZFY<4)OZcdKotG}^5Mkf79>pyy}*eCLLBBOkgsgFUYgpPRmrWOpBs#sq?4apa{aXX z)oSAm2|~Pdl8*kIUF+VZ<~F3dPfxuYVvk&ce^6Fx`R%WmHLt=d0X?QfPElPr4r+F& ze>D%a2qph>zR_nt_@#H4s=j8(T%u8?nAg93ZHhQ)Wn|>qM)BO)>CuCGiVMv>>WZ%( zx}OX0=Cz4dQQfy+?N<-((|156ePXz~HlH?2l5-w)H-8 zy-cy_Mzf#3ypbRfR#uPNS33Gif)Tiga*e&QW0 zO6VOhI`}Ofgd&VAGL|HIwLF0go9Jplw8{;fN-(>Q*hv~nRVrF8Hw2uE3``%&D z)TObj(h2`5Qk)Bh)%3lpeV1zf>aBh$^8$>EZx2|eW!vQ&YUXjq2irfVE7%A}sIyOW zB_Pb1o1Pu3MyFuvLn9PEp~?^{Icok-R|RAiSs8i=41W*9CJo}CmJ#zZ4cCn( zKTe2_Xa&Da(k0@wzu}lG*Z7mM+Q%$=XI#m#6Fp^?$COv2bp-#{fMH0fz|TxgUK+K| zPWidf*z$Bb;bxF+?0RbMJ)bzP_}+t^U==N!EI1N+9^NxL$fOk~oS zYz47FS|VC@U&8QBZx3mJ#Tl8&h?Zv&Ecct_-Ip&rj8A$S3IhKXtHJ=J{6SVFO1N2RNRTZ1lHP>{P=GEP~x(^Gdi3I+}U(;F|vOc{^EGGG_=lYJF;?c_XTU%j9 zZvm@{<2#r))%`o@`F(XIBMN9YDb*{`MVUp_S9V55p#?6r_l)+&BO9pWCg&nC)QK8~ z(#9JYHbVk*YL@7IrTBq8Jp*p^^2r9t@ilgD>t$VZapR}JYqh0d+u}5x8as?fxg<3JFE%+kWgY5{2NST zpqdRtiRBPfN#`7Q7G#vbMPkMhB1sa+O$Wm$0|8l*3AoZ`|In*F`E+G2(|1HYSIa;C zJC@-14B*a->WCel+~NS}?lG*)TVCm>aJu*AdgKGTz8#qonFO5r5J$Jv~Q%C91lYjU13FFpWZ0qVvw8Rd-cr-a_ zGtNE|1^g)43(%Ao43EK1$|?X94n?~nZ<`!JN55{TvfAH0WW2q7ywl$+3fPStPO@_N0z3pUj!;@*xl68q0xLBaV^~9E&7K78imyp}5e?vx4{Mtt3aZVgt7!9T@WzFDmaL@*>hv?3eh+o2M+~Awnk)uxTenPu|BHRE{Gbi(2B$5h2 z@Q!UVy;jATjs7T(28J1ZH#4Q7y+5V;-*`z?YFk(=y@GZClyK@UJ1)6i6Rd3n4dc>-}pOQ*Oe<7CI`W~;|Y*y@}J5?V~9oqJmKELmHbkM9> z>l-)TZK!0lPcI(^&O|B;jK!r>_ce;sl~!4Nk;Pf}Y|uNAfhjFD>D}mqe`MKLmwg*s z!>R%bEGr^GFPYKASP*Rorx5GQ2_ryo(DN*DGww$g9J4|1M(lA+M(ju$hYj^{a0bq5 zqM63tFkS3GI0oYC*JE^@g(fxx&LU*!-jQ#lg3k|i!bT1rq+@xM$$f}rD&`y%+3gEFFUpU zXnERkwe6O!|LL_2&0DYQyMCJ~=!4@0CJS#cwVTUxjcoJnG1imj4Une-r($;oN6oiPi2M{ym1UqoACw2vLs{%5)UFCTD=k3DtzNLo*=PR#T5 zP9L+=k?hBc)>vvImcJ@x?XqIMi(`YkCd}eLm8K7)en3Etk_8d7g{vVRs_`GKD>-a> zt8$4Z3ycI4I%tSID928wz{Fxb_(3^Wc|321K$;X|7h7WaGri9Sz0HcW_g1egg71^ zOK#TdLrLkJ9a0G}IM!Hj$(p<%JMSRWVgr(AliABJ@lQeJLb51&oM1lnYpfj3O%q{Sl;{U{A0}_lJ zY~mVu{ePJJ5vtK_zj7;Q!!#kYdOupa?|GPQzvp<Od+1D_?+&YScA_OX{!m6KA zWj`8SPHeuv<+wVQsP3&4h?7l+3RB)j21uhRZwlQGj3ZIX8wn;xq>V6StIFZb9yPx9 zx}3Ur=NU6;8{Kg`veE_4R0pFu*E9O2k62Ubu(6pM_LyxLnFX^eUMcsDIG5;@Y_1>e zU+F)-1y5xSD2lMfUpF6s%3)~Bm7mM*IqNgZ`MV!>%Gx`M)AxDwum5pg@a_&DQncCx z@9rqK2;?YC2bP8WMwU=mX(Fk{A)?DUloG=b?Svp7m#{mqEF&x7cTg3<3CM9m1{DM? z^ic(8Qd4X*IG%7d52X9P^}L$B(&kGPfe*-7)&YVvcdJmb4BsmUTTd38rE)z$YSDgu~GB(uMeyl;{c ziYiflC>hOxa1jXyV^Ab9Yg2lwki`s|wX}*EYSgQ>-{1blm_uKca#uTBO&YiQI3(!r z+3q3z0YpHz_bP*u3Vng<%w5TZ9xmQcO4xdLtGQS--w}GnsqH;*=^Jk(OkkY%t$k+% zL4}Ejm~Otkh-`S?NkvX#v2OhbW`oOYZ;z^_}F38dO-tqH7Od$D@L`YbaklhGJKo6UO2#IfC+yrrn92rs~ z0f^yr5FbKiQmlrjQ5i+Whs<1dI&dnRgC|$Vq9S*I=41&#?4KGp`>#SHNXCqrL;F(-MoF$y!{5h zrK1)~$Y?snd+Xulur2?U5FMrOP=5*dfLjYk%Wkn`HqJSdf71R>NYs>#2X6e1cYB6% zigx40&~|HIw=sf-yC7 zKHN+586GoXj_S%B38QuJ4JSYJncnQ}TiSoSqoVz@5!Q#^u8hYXoZO%5N?9wmfj{;4 zfYv4u=*T1C2KfM4D&$7-j$EJb3A!U-0e1?BWTJnVa8E5S88P>e^Yo&^J9;B{`)?sl={WXbeSSU%a5WJV<}OH!+z!jaX_eg%`jF!uw{x5W^Efw`Egt+ewMSgV%ww=7O$# z?uGHBl0k^0ls%>WbANv^yK;nsP^GTqKWb`opl*6&Ub%4V_G)#@`wCH%W}HN9w0IP_ zNd76E%%ra`;>Im?^6!jX@`LC4=q_s-D zNxD9eu?cC^>6Qv_+p@H;=!T6QiFB)4%^b0ln$eZgX8Pawts2ciTObce^B&i%P&=l^ z*K5gr1ZOYUrAm91n@|5v&1*Y1gSPYk+lcDH#)aOdHscmObH0YnZ73B(QTaIri}?wL zVAIG(uYw3qm)|zbZ{ywNDRs=L%^Vp>_E_`kkfE>WB4!#Gb?1#pY&h%pZR$G#f7a;V zgz#CT0Z%rZDqOi3qio)Ul(JZ%D4GoS;=uP5LRV1;A@0Us@hnp%DuxSN9# zOAoR+IYH$h@8B8}p@8rPz5!q#ctv3};m0w0EE+E^Sd|dhh``tbyd^)C#DSee$WPcH zrcC%C2HR0!fRpgh2y(gCMr#i#wYsmA>Yr2UMk|{;$Id=I67Rlusqp*7*$g@d>&A+f z5wS7|R$o#sy$qer9E#6!PObs=_m@O<*Q6|=>(c^U;)+ya5y;eUx164Ce+O_f)I)`C zts?X{=gu^r8ID0X%fKsnp>OfdeC_Q66T_;fy1^-}_u|>^=-$U3svSq)b%WmJjG<|HXoDLcD^BdH=;*27QE;0bY6w2g{ zUk|i;a={e9NqVriF$?K;awdz*k`SME2Juk15tgrC%t%P3QXn>TS>eE0$SH_Z!ENEd z@GWDc6da)9lmbi~Gr(f-K7a{`F20%n2Xg5c3D*Y;<=Ju>3xGH%*vik&SUIP(8x3A7 zzG@`23ov^_kCwd6^f|WB7gB(m>pw-;w4s)VSED+rjyIe&J#{k-6=}ozjCYjVS(~+b z4%JWOtnv_;6R6pUmjXO8!U(RL(85-J(5n~0NcPKiL0m<@~6}hu?pfp5#p;wSYQlMOq2+-*+HJfmB|q|DUL9J zw=fh*UBrUZKv+DHtVV=s0Kz0j6K+Hctd`3Q-Jd6l0BVFBQS;t+fYQMC?Y05N&4+bhb)*&sgT80UFG$jJWC z{%X6im3a%(BN+fIDApz+mbq~w8uE_YKbq0IBm8}RmG*1L9LaiN8#6os9FYfHGFOIw z@ETS#Q#{9Di2G3dq(i^`a+zkm{PgOuh474a#ZatKWqNL-QR4J+!e3hvo#;v$u5&z@ z(-XhjyaX*)3Vd^feb-#gxqm^b-~IHC)zN5f*VWNoHS_RG-m7KC4wW-Uo=#`fk2NYo zO`FtsLHRycOye9|wXf}|&0^Yy@C-4Ml?&b;57kj4+Zs{D#cmE5Ek}F1`2aO^{+UjU zpl?}MlaR($Sud6Au+Xgg88cye$;ohUI*&}7nd)bDML51clZYpEXC!NPZPKc_|Yk|M)Nj@-vHqTr~xo_IN97lw7He@HU_=VEHP&)E4w)$RS;gjX7hTIp(fxcyyb zAX%PWPLM#DDT)AhUwAqh&pO57LStij>xNX>(zR3}>}db4CiLa{gj%lF>e|mj-SBn7 zrS|lf8pE2K`Sj~IoUV)|^2(~%!(O*-rR+H~H2(kBeSVjfMUWX8<{md>HFXA5CLJq8 z%|o*U-`30-&?vT0I2=oLQK2_-#V?L0*VnJ;jXKrrl*%$GMRQQ5cbA7E*1%kCN1qip zv$5p<_6y0JYj~-@_bcmGz=rJVLkBut=;|#S(PB1H%O6Nx$rvC^Z}4Sn3m z$f~gIHK#H~#GS2?;>uCJWIMe=e6i$2Uk$ip8MMWvV`4QGh_}FYKeOz|k{$Jn<0VLt z7*US!4&z;-JxW`VmYlZuPk8Zxdj@^>D1A%7$3$mPBFq(|#FFvbqT?Z`&Us(T6+7|i zSc@dP^Ac&n3mPkf2fhq8m0&nPamb_IM1wkysJI92ovQ@*hf9_8hB!u|3@LX&Hw)gc z18Pe=jK^;RPfMgLN5Pw+t|ai}aYR6AVwBM1Br4#%?Y}80V7`eVXAp31{lBZqqEl_Z zOXk}5)YZs))~YY7kLT>Awe+o?dT*-F*sjbK>GDw>X*~ke8)}q0YfiJCR_(_)Q+(bUm3sqW_%jpPrqn`( zh=y}h088rX>lr%XG6qu;;c4g}Ama_r2VI{u$F|ck2mhMMp~#sc-9UNM{U`FDdieyc zG}UcQKc$(+D8C)1zYi|#$IW`mGMG@f)F%aX#K(NcCxoz*`pX%YF>kh+TEQ$snAGN` zhZ8k7UEG;2##d}jQRr}rP)M?uopW{g_A@n~r0Yz2i1`!5RaI)i@vb|rj&F1tA3XGo z*KFMXOi$nK`hD84?*bBaIQWv6i(XWoICT-;$Cb;@Y7L22A)p0`jL(VMNvx%GF6nrS z0HCvc;5LMp0fQ*OXM`b(&z{6qrDlpP_|zvbVu+50j}1w5S;Jxjf$H2j(5>F#C5is0!0&>0$O1)ydR)dV6B}rO+fAd#U}UiWZD$jdhmDb zImpD3yOn()7AjeLKrIUTeYko}NZj17guY_Z*mqNIknciVbvB?A1c3;DlP3|A$nAud z!MN@%sliV(=m0BO1x4v^Zf)KF1q9q!GY;IQ`s8S%N4vSb&Ri+J=hz~Xq@Wch^qY(= zs#rfOvU3gXGPR^nzwtNII{^EvxXPp*IyH8KKCN}M%N&Nqz+zH>MVV$3`U#O=rQ9HE z?~59Cz7R%M&Muqzc`Y)Vk)bJ#w9&2izaX6EkCx2$kU|_tZNNPc7zq>=-y><8xLzEy7`1`v zQ`JHIoE^Y(rD>J4ioXz@vq9Nm0R+Ia21HgNcqN-MK0a!fR+MOAQKBGP`igY#il!UTO0=3t6gMahtzxFF3QiEwh;M()JlI^PB~S1cHrgq|J5@u73WQ#KBu%xZ$jWby3B>U@J;-74Bq(w zGfAFB9r_2${#9G0%}LxK=j)rTjD8kC95n&fpZsy$PZzIgz+o zPqI1z1LQ%_Wh`T$pRbT%y1}OG{7}+OK5FpsxbyQ+%=u9ka=)Qx0!c?Rl}(tl9I-Rz zBt1@q1YqK?IxuO_jC0hQNj(D>v{0Qusn)zIR%XylIARvMGgO;L3!Ytpp-$f&?sYOV zsqF09JYKh@x`z$R&OjlKn1&r*)fKIw6jRq*u=T8{?E;ZTvd8P~Z!<_#89)9%Rb|X; z&KJVDqQ11rUbqkr}^oPw*iM&DA*H<^6OLaB=aR?30VmRmEXTt-{pN37o#i-l$Ug;{5{iU^x~Tz=a zunM%1%CJByoxRoB^5-{Y9ikc7xqUrdh5b#nfAP6eJ{+NKk59V5poT~Uo@`xhwsM=f z{tcVG!ElO`s2X=|A1 zEdv{=7vAh6s4sm(V?t+;`vp0jKyxHzlKeG^hVid0`OA0>7z`3(5FS%t5I2Yo2&ydZ zwPTCH^Jx5$lb?EmtUk>Ga{LG7G_iQ`%|Uc6)-N;>Y!s^-2qF>j*b>-x>LL<6;Lo{$ zE_o^qEy2mc#t323INc`FDa%5f`s5x*?98E!J!%rdYQV7=Gsvw~%d-ZjyHTLCNx?vn`<=c-i zZ?`a9ka%5X7Kw$^)!`!T9anAbx9&C5KJraIB%rIAcI0G73BzMD(5hZnlXT0Q}Q(t?%TyysEY~9UcY%6wIhJwrcQ=^))|2Rug z;f7M~r2|nlGqjDK_$5dG1e&i`UIAsTci1U5X{vtzZR#TzmCR3VdQmfv4~BD*KTxVN z(K3xTcd@lv%um&xYf&cCu~n&rm>U2F7q#$GI9jSN=Aq8nv07K5I|>+CRL2Uj$a-Ky zD-Ol0v?IC^jY(n=M%PO+(!VwyQ#e%9BjjovbW+i2)g&4svn-X26ZQ&&PGCbrY}U-5 zXkPdS)zr?(_Rp>?RlcZ=sU5)L+JQr{v{&>EFL|RLi*}cEb8(Jh5JNciol!GykL-Y} zOlxVwu;Z~}=1|dGn9l3wg-LayXqYn`c-uiSBVDwjM_HEq{Xw2OOTF$-hbF)x6Vz`XF<^mw2U-uzl$B!Q)F zIFq&?Zv852x{sDmtFTe7JM_aGQHcY^7ZwLOJR*Z?>9P-LYnc6@Tg&T<71{&uMYZ6B z>3nQ?$;^Z=o4v6&_FjZmU$Gam@GI?X`7m4?$ z5MVc1; zU)~ryrRuNat_a)c&fW6iRF^G9r75?jW5z*>?Tuo-86OXd!v?HoiM`fbrB0iD%Xg5+KTZ#VTT8z0ZCxr9PR@Ly zx(~_AZV285>bO9>+Ib+yI99{DH zWWy3NHC>oSosGm_*|)(7C52PjFz5Lr;wv}?gPcJkG9I&8$$R($#Zh#w#a}oN`_8jZ zNIjtEe>Ppw4k~wv%KB^qzklytyFPHIU0!afr%h*Wi(?tvGAcAW@iSk$u2g*oyBWoTXPL+Sbx$J0*wHSM2$C#?$ciS`@b zAR)_wuX0-(Ke>tTvi;gqupb`#Bw_d@N04$}3J*B3&=V$29ZBP_?WXR!uqBlFxj{44 z7mb$5T-_Zi%a;oR<%RW@ZpQ-t!*}$rhkj0U;n9in=(1(fJ%OQ-_XVB|up*Gdhy;f> zen!qw%#W`qpviRVZv+#VSun3nzO4Yoldn$B=oSD1Q3noA+!|N!WN&yi{3?-(By<=& z)$5K)%IjHFvDv^^w}dCrF>@{;8SA+J-B@Vvik5ksHpqyFe8TnI7oR|hlx`&C_6+6i zX9mwFtQ6`+H*G7ayV7o1XqpMVYvj)lsqK3{nW%UwG0OZcd_YD{PDel{^R#~)xOPlm zn7srp@BUR21eo9f7Dc(xo$7hj`x(c)Z4HxU^`rZ7g5}|_M^j)8&4c?hv$)EewA}Fe zhZd**@V(pBgEwqQFQhY#=eAEK(ivj8Q1`EEM4V(_u}FQ-bCyn1nSphq+G5PctLNUc zQO4Gw<8#?(j{;e#rRhHp_x8fd;WJrw*Vc`Xh6mUKW$Lz1vR*AF`<`PDNMo?L3i*1o zjB&1TG!6K&0OZW#7i2vo9tv9PfWDH@gGvpSJJhU z=B;Bur^Eli1XwqK_{qh~`_1Te?z`9Lw^Y99l*YCV*?IH2Wb^Q~7b^3f3libp*M8n~ zFVcK}dQuS^fv(qAal*B9_#A?jH-7(z`bVhOh`v=19Nt@~n}1WYSPcPIf{J$UMCSqD zbVlf3I1~c$j$&mmu?V3BKVy4{8|obT^1Vyw5Uh-(d59JBO-qQvvQY97zZR4`Srmd8 z(#vsNltjkjl>#({A9HEePPQleO~R7k0p)bzuLaJmMGF&qi_?K9g=?~gqF*K3tfR&% z3835sp9abZQ9L$g$QcAMBpD{#F~~SEvQANl&mnM60FZZU@-Va?2)G6NW;!!AV^YVV zrPgbFuqgcM?TByI5P>V-h2tNN{8D|gG+>eV$>^>3#e3buC-mI*C7x;eh+mlVfa8OP zR)6C-N+u;EG5WwH*IltIV^g{@%Pn(aYcfjY!UY!2f@zd$)3fAHlJjZ*HtoiY`Mh@y zmXhRyK4XVWCr#w7meLg!=>`sgZt3Lm5Af@0brM`n`D4Z`8R3$%sNHRYVXFOFct6^Q zYR1dB`w~9Y%>8C20raDKvji5PK-q{Y9`nm1*#*-o&{^v%y9DWT(O?2CicH9=hIRWa znt@*I1F4(sz75MCCXS{J)4cP6<$&iGtygX_N%be5y=tq`Nc{7c9Q8H(%n5^VglJ-R z{TJiOVzfW9N`-0X1gK7TMI%;#{Q$Ig=_Mmv)-RpCwAf!ImWQ8SSFxUK!lO+SWu0Zq zLh#O?4KVHH;MhbNL4pONJlOq;XdyTm#SrKgm|&Z84hFO~p31*SS7L^NOavwngcA4@ zen7qBnNs3he^vEpJw4sW-uR1Ne?di7{`ajcfbj>D*`B_iEG?GWuiidbU;LB1oy6cR zJ1=^#`O2O8xSerd`mnYw7tyY^X_{~RIuY%L>XjJfmCFAg{I`kll=k(;C9}<=8`5Qe zbN3zJU#nN}*bhB20LL}OtOpN2{dr9jG-snkLcR&Wcxi#nrvU9xjjwoUNcb+YhH=ghikP|q82(nU{zzE z`ol|(F?1pw8GZSwEgygXl$ZJJRcmMB>1z0~_g}@S$qNj=%ln<^#!GR$s;^JNEmJ>Nkac!MvKS z%l0E0?jg)Gq9cvtrNbQwADN}ChxH5u3#5Elq<{s|`S4l&{NU{t-^gU}p?SSO(9d%N zN`?Hz@5ukO^kaH=ZgghK@7g!7xBo7j3dd@ZQ2SR3@DD&Xl${BkwXy}{#m1&oYG^23 zL(pd9LZqvxmbd2(FBiFRd%-jNyvpkSXlmHUqjzVkeLZe(I`oYD#?GXX${VBg&|uS+ZOj=$ASMTT zD`4XhCKD>~bPf=ynPL+psll^ASFnGecrgrqBC(cs^UMZo?PXuz$?;sdINgV+pbbp7 z>FsP#H`(JPyf)}rHtJOpw!J~U_G*8K8A|5Wn_$z6*xBA;#%RsXe|hmr5$Jeo?WOaJ z`e4@u!z(Wy?d>i$99274(o3JiTkq;8S&GNq+oUO#QOk@{O`NorlZu=%pSGLb_o&KS zkWx_3_^N)03ZYuZG1$LmgX%!01__87K@fOKJS#kmvC!Q3pOpwZ z)uGQT^O<`3^1m|gI%3Z#?P6}L{azFewfD9^diiwym9-QOyt~?;(Iep&uDnn^OdneE zr?p0Z{OwP>fP~E_&Zz7ix2^l`R4HSQ43-`+7rNpL<(KmGIU&sVosD%WMV3+Y^7mYD z+)sPk#_@UKWpl)V-vbAcXf=dS1*b7v#uxF&ol22_nRx8yLZQ9+MiCNlJ{KWu6dX5XNm9Uo1x!QAA;_GEMclD9llRbB-m0k4>mt@?;UH+!M_T%4Hd`#{8d|#@4wf2Ox{F|zB zVNZ8>@_75U-_C>mk~xEE#An}q@7eO8ehQh`SgNsN)2w=UPWuP9oH(T7SED7LjlMg2 zeA;>S=!UG`{%J<<=`XtFYU=v`JGnEOOoi{c;A^QBMzjw(nM^P5L9O%S(<XlL`*+!mp~7t|lcnX20n5=! z_8lKxw<}%!+-En2%=ncbJL1(pbziY`!?gpUc-J4_{BIc=FpXL|_AgfdTr}%cmJb%( z(C!P@I0bL9_R(TbEMCjBzp{FGU^?~ADMnnR?9z?}ZTa4*N;#aACe*jI%2)ltg`|vg^B}b|Sj@Xe=}w56A7%mX-3d)&4SC z1>tSGQz1K5%wJZvG~3Ljkrx~9DXt$|JBD&twBV(NSF&BHERBNk{Fz>cQLr49r!l5+ zUkbbHD=;0qwW_vRo7Wb!L)s~^5xS*M9L+QbY~ndDj!6gX6I3bN=~7KR1VGr8s% zvvWehi&27-dl7?xvDLYHa5n~bCO6l)HB2PIJp_hOd-JKC>kBD!MwcfB>(7(2NcfHY z$2Z|#5l!If=WmU-k=K$?bnYyn$(_lE$hJ7#Tzrt?JFs9TXtJS6vo zm3BT48(3^rtdvX0w-|c1d2LG%hZ1h%l3di(BaH6+Mc9s1w)DjrS`+ri$9tpkYaahp z_(|<^?SD5d#=A26Gpwg*Mp4>Os#}j3U0K^a`Ly=b6K=Fy*AxSXX{zeV{Ylg==+RiT z@cYb=Ki>yHNVAm@329cbYjM}WXin3@fm;tP-(Cg=)gx9SYOOig{HRu`E^aYm#_~Q* z4{IUjH{?Ri*|LMWzmqhbl~=E@vqs!3Z>+}SRwNR&BB583(P%0Z;?^{k_dm`kq6oDK zD;6Di_yfN&?Xc6O=_;Z!*^J5T7boTLq0L+>7QN!3_i5*}ci&x^TvxG(#Bx(Bt&kDT z{-O&ms4+0&WSmH;R>`^kL`-wd-0FgpH8bQM$Dn9VbVXe=m1C}HB%DrZXQN~0gLWg@ z{ykcHR?5+s%4fq&rI9Drem$W@wPenqhfzCYH>}@8@@ZR-=<%%e!$CJ<@kNrC#lxoh z2PYM_LfRLT#KC&Bx4)FD6vN#NUUH0TpSsG-5h+9>adRowgQ9^$ORo{ry@Hh`PPesC zsi6*;SxZGikx0zC*vTdn5exOfs7^MNipMl9649+vsoY(&+gl2{?nXlqov7=%6yg^V zJ>pooXxIw>H1393EiG=2TJ0}tdL97_Vp%&MivUqa;<5JMJAA2{mdqLv{>WLw){xaS zf9Q1g*J6B}sM}~2joe|YWSf(|ZG^kBm0EXjcHc&fdo1+d;A`wfP2dsuM4}Ve0~h{3 z?5h8;?>d{wvoLI`x^T}kHjF5g|uhi_>4-iTQp4%A*@LIVQeOJrNq}^y*pt@93Bfh#`0~0 z?QSa`&UrdiGdpL;zOsV8epp6&etF@lgcrqj$!>D(-zL(LFybBqS&Mp6t)9|1J22Gy zM@wY)aDJ*GBNn-EGG4Zw?Als>t*@>5gA4AXZG{azk~O0#?V8#f z%geDWiMvWxlcDS5#e_)#d}9KEw}UO;PO`tt%D_>!Il6+L~i0veB?! z8!xd3v4>bX>V$Sgk})G(ST4N)>F$xd-hQu&TJhSJwZ(`@c;%RJxZAlNyS_Z?#Its0 zBu7&OHexK>jGp9UU`z%PUKuv#HDKQFkvi{KzrZo{i#PiNju~@-5R?f{ih-JhDIjyC z1O{Xb2?aR^gh>FzCs3Y5`Q*KCMs`vTV@Uz>&X>*3^CDY_90zex01*l(L@Xrz7XuSv z!ouu%U|i6?aYMU>koR;bqP>kjKTt(I@xM@);n~=e?!n$Wau%h5T!AM>j92z5g z_MZe7DrP_TU|o&hcvp)Kn~KwCd++*=U1|ToXP}X%t)l=FZy)rH3$(Mdj#DbzZ~U`~ zc#-MOX^;PSxefF|mmzf`{-V>u*mVpL)1A)@VEr4%@wBWDZH>3@RX^ z)o?NA_q&*IY;|b%PA94k@Xs|;Wq9=RK$o5T=<+Itz-pG%GwgVDm zgpH)_U`FfY$|G9nBU3A4N^@%iv0^c7*>5XYQ2X1>u$JtvnU!4D${i>&v@74Wbj$w{ z{`oIC3-wT9uC46~<*HH5_)uodfjnBOekz;`xviM~4|J->XU*HZVyckL{%5P2dXLss z=rO`tt$fK=J2t;wZ~rEhtmvFZ?ceKhqZ)s48}w%#smuk6AJO>7ifL0{wM`3$y^+o4 zXu^q=?<}~c-db_B_Cxb>>y0ZieI~$avtKAVBn7PiI3*1bq@Bg%zvNhwB8$8@KY?Z- zxD%z506DEf)S;?Gt*$TlJJ2pU6e$$Mnh7aWXM$R;p!Dp}Tr<#-qNvi@vTVIEuIdL8 zpz;a{#Dk=L;(Q;o?IZ-NV+C_lmxIZbib4N&F;RN|1uSAzu2204b$#e zYQPO=tF-^=MK@EPcFoBuU}bH2da0$n<+1zvqwU`<6={ppJ~3kbeBy4^Xueo0eQ{0N zK7FoUuGptSv7L|`PE&WiXU(C8?XXB6%&X$ex1*YS!)&?A=q{%6B*5v^+4T2+jGhsUkl;4 zJQuzq-$TiTY0m!-WA6eVM|GZ!Yi7>OoVo1I?9S}Y-gl*4X(g?swX~AfmMqDZY}uA= z*~qqR3tzCoH-T-8F<`(L0}hzjTmmGJ0EL7ANhlCPLK1SJ2`Ncw<2EEsTT1g!(l+^$ zHce>1q~RYe-}BB2O8WiUulR>8t!78koO9mG^FHtM4o^jt6?op;Z7zcQjshB(<`C9EVFt^K z>-@qYXaWbS(~39VjABk~R*@xAo(k-;ZdK>Lg! zAu1t*R8Rzi7nk63B(F~n^6=&|hKxg}U9F;zf2(Gqr*f(J>JiOBP5Z@jkZ{tL=GHb- z$~wFjN(aesq*Q`dhb^Qd3Cwtk$6wx=U=!8sebD&6;~wvCMo{pN2v=9ZHwRY?552Y; ze@*DkHhL^hF-$X9k?EFD$X^qVhGtAf8{Uv)i=bSV*VN`lK|NkvsPv+9fiyhQ5*}#k2olkBY$qqfa$^)TbpI}| z0zB}j`7@xF5i0WIl6>r5hWL>k@u-{JEVA1|iTyW#jNjLdZZ$|E4lT*%dJ~pa;vTdO z*y`HPS$~IF>VJgC!BtDWFcmXbey(sm`ZQ=i_2-f)=_M2xVxXgtcGMJb>Bmts(&067 z$ou}F;dP-$7#3CwK=RPYLyfZ3GcHq0PZ2arF!@xp@nGrkYEkW{q#7$mdAaI?P}nDG z3;sg)0T?Cl`q*-4J_X;q52gdS4cQhlV5(2)^8Iw)CwmcL3Vt3N3m3wi`5OWI(Pz5= z5bDAHCX<4%Vt>Kh0V0WzWx-Dg%__2Ho{Aq39hVd}_;CeQe8AYnU2r!lB>7S`*m8(M z{NfgD7#$P3E&WfS!+dKnodEy!`IdTonT`YQ1Jj6q{79url_tLeg)U=Mr*H=slVg1% zvTvqAn4yzWz$Y-NgwjFx3O^2Q@+KKq)1$#VvCARTAf5uD<)AcJo3v*@z`|aE2=&d= zK}}lc@ukgZ7B2>)bD(jn0W@21zkNf^VRDXoZ9Z9XwkUjM5V#k|AjV7pbz$?0aG`*P z2<)W{D26CF4QVsLEI|p3;)wJ>9a9J%0Fr|`GtUY;y}~V#%!AmJ5VgX7!LC9E)yRk{ zn)kBV1$S1G_D2F{S=g7#YYJ;yn%|&;&Dt7mi+8k_f``G|ZM0IWvpm`h26Kg1wf1mY zZrcAzCR<>h;ad(s`M4FViDK=?yksg-w)X{F{n1nz0hQVWaV}hvbR|_vHT52@{Sz8I z(aNL;S)@C*^SW zz?sJ6BbJJllOxMMh*3DbOg(m$1eNwNGYP}v>{f#48J8spp}ITN-2NfVA&{F&9$46n zl`5raF|2hzgdfz8^~8o_emyCG{A?VNLA{5F+&#i*{K4)7hCucj;TAsbCHY9-s#nNL z<0a5f`e;B1d3S_@6m$0rmqJFGgVcJ}Z+|3oaJ?a&7MBBkDmN^FMWUbE|A{DCMF(m#UhqLYN&v~mG^2n-#S!F$ zvf|EHi2QIAQ~!_y2i2>gaIJ>q{I-MU+>B^stTs^g4Cua!JDZci#AM6`J^hp2u6^CI zkfEm{+S025bX+{3C^C2f{=}u$sED9;vce<6pSI#BP-??IAPj)t1y)Ci#;F#k-Nv`_^yy>pt}=8 z%ePf9Jy_;C)dCem@!no>oiWXtYM-mTT2R0K>(!+@niHYX;Cgm~x2d=v91P;(t zI7!WIz|5=I)}yQu%>h1$(cVh!40>9LvWxSVdNwPX1G{ zTNjqZn`V+wxR|?V17>v^JA_YfN|^(9bl#0}wqbYWMpNDt6sGhz*8^*BH}`j#(L7Bm72a358PJ)2czvfiR+3zEWh8D$yD^%{Wb1|y z@0swCno?rNBzX#ynprJroNfdUVgdMA-x7a^`ojUJr`*}_(S{cqUTpYllqY}#_R+KR z_gjT@dnf|IhI$8F^swD%AUKF<{?@a^ckvG&j-cHb6a_;25VhbM+MWKDLC96mePqR( z-_Q6ep%>Vo{(h#-jd~Wn33tikq`rfnR8aczw>h2QKHwqb3aP_{UX<#Sr2CMM&0k-o z*AaEm-$aEF|E+bjEM3cugdt%>>ieZqC16;#wR2yA%c|}M&PX@en7J*f<;ZuK-dP=K zwiH9bNCOVdliad7*G>gnl8VvB#qXQZtW_GYlvH@GyW{Vf(y%#18p zu3>#z7!-RJe>>$kq73cBPlnJ(2uP%3FlT8gM|Xxlo0dRWHz9TS7Z`V-IkEnZuvc2- zW!hRTaU`Yj*vLh0RL!+V4Kpx0X6Sv3T2CoTi>n6oXr>V)d%O1cYAmc0P$`Qk)4otv zA@Kufny1k+K7$qzxI0l!ftgmDW_y$`T5d3NX(pzi%;N?D`yGfG!2V3gA>zE3eI3^L ze9i6+#7JaM=Ob4?AR!(jR@N6_rHm+q#im*v9af^UlT|>o1jW{9XsW-A5DDQqSz*LK z6cy7){`+X}kp;kwC@CY0f*=PL9{gW24-{RIb4JAMw;92{)YCAZ>_kyNS$ljt$k>BY z4{JguzlgsOzaV`_R1SE>Cn6xOfdzrW8Gb0bp5iCrFG^eAcj+2y$BqN35jivUSdj^5 zmq&Si-C6TVmPpUu7qesSgXqQK;kdF`oiU6-bSD6w8bEl>hQSuQUALhbl7;dIqY1&F z+-~UNmTR+nw}GF_i~S#0H1i8Bj!^_!0DXBLiGmo5H+L02qUjO#c>JqX&Zp0ylzT7B zwrqxa^SHDDEy?W@?4I7D)_&x?o4j~=m4_SqcDAk0o7>hKT)w1w!u3{Oz|V;dt?FJl zU$klJ;&1`{wvwn6<3j+sWHe>>wqV>=7y6M@EomD0%bQDw$_P3V`(l$wN7ZDcE9<(} z_Ua>i@*uO$6o)77nmEVL-th!C|MA(eV<-yvMD8FU9#0R)OFK=u$azGT?H?QbM&-SxJ$&V+cd=!&}o z;i9IdBw34vfUV3uoCN?#x>Qv2r@p>L(DcLa=8z~Eod$pJkfsSft0#SA!qD4+-~>Al z%ocbm+VeyMKn~ao=Z>%i`<%c6G;`ou+N{{yxSZN1@CWV@Po=L0X{FW0sFv@zyhZrO zNTThU(QLkWUT)&D!K{B=aY@KRgBy0%J-u^ZV+*#J#rV=d8yCKb(JVRn!HsJiBW8Bi z{+W+YY-mEi`ufXu2+Mfl(p%dMIXHf1`$*T4rG?~7-n|ADl&R=~@^srLhnq8M!C4_i zEPHkwV{NAqE@$M)vUf$f>YUrq5;2lOaCT-rv=mo9_t8aInoU_2*h} zc`4}V+WO_mvr?c(j|A3k2?h9;sveTMqx@@^wynMFXxw&JkL@o6)aN@wk|MHr^p8X0 zL#cF|uqLLOfu_$e`^(T$PrAvs8&)4(w8Aunf3`O@;Z!9@mIruaty72^C|!gPd^8t~ z{ONAjIn1B_BPW~^Nq)va$#J!+((LL=tjvR{s*ATO3n*8W%DlqM_) zn0ZY|WP-uk*WpqG%)t9^577m2Ga0pnz_y`qIQ-MUcJi z<{Dj5K;Cw>B5=?&>A6%9<9|{LAc}F&Nk#cMdW=fchpausBf1dK4ec6(xF!!7(v8s zB(M@bky!P<_k?}IhrmTpe8wZrQ(}b+qC`v#q3$7v;gN9?zl4&=7Xh^qC$Fi=K{5+nCRbnZ4#%8hh6HstU=jWbt4+7a@2e~*Gg{mQNN@v?f z{Jn=LqJT%y*20_|7~u$j14EXuCSX7#TvPOsU!@?Hl;!!VGYRHb{}|9nNYZ(>Z3Uwb zFMcEkVvAS6`;2qP^pI_w=&BUW1!Aq`fVQnW-oG0y!e|~Yin}2UQoB@n^_ZYb|3Ex!X1C@n_3*ms=J*0W zQvD#VE*e6eWb~+t_2)*P_{W^$z7{+*Q1pDy>OXpX4C~mU!!FmJpXt{l zmUfNsS+qfUq2C#+^Q$hsEysEXo#5%}hZk2H)aFK}YHT{U8pH5$PZPRC@3x6s)BeEF(9veRYc%EywmP z+F=FUgS)LAX(Iul_JE_US}dK4mTXyzdd~|;a4g|)SK5t-lC|H(#)fY@mg-1g1mc}p z$IHYPvdneeVRi22cOBVx8ol?%e8wwxEX^5eYlpCuDc`z$xEp;cf%Pw435~V{v?7F! z`)yTdL7%@i6ORJoFb_g#3Pwv8gF*M=Wm-7h6vYI@>yt(_4=L>0AI0qWsV1TL-cV7F zaLo@DlN2eEzmj2d>oam&P<>q8u!$@#HX6AeGO0lMsop^T8U+U6`LL-~q$}8ih$m=s z=Obi2@O1)UvBfDG#9^2HY^AQq0Ae?8fv+_Y*jq1xVXu=(kzpVFITny}!hOVA%E=epk4&W$MT!d^8-LcPhA$s)I9p_71%Q(j~ zd07o~7fZ@|#81+p$I%-t=CML0UjC>O?(CRvJM$Qa0?6Lihii+j}i6FOT&Dw0cwL!$4fBQi?cDNu$hnJz5u=BF#q+}tOzOmB|@I7TP7QS?XpEdNfXqZ;8@Mt0` zn@;%ChcoKFn2{Bm2Zf`VvArCmWvRA94C$c+I8J6TP!*nvr)5#P%I4J}F58BtW%mn0 zQuw#=K?9m_+1R`@0Z}KmG@4Ma&^O3XrIRhR&IX>UBD$TU~{yLGlK^&3>)9v@o zk#FSY?Gcscv0S^IMkIS)>dn(hz;Ld6rx~<6#eGRk4^2c8&kH~9;btx|_ZvHvk~2a5 z)4P*y*p^u=a=mQ?+D+l2lx^rp4R4;qJ1S9c;Zcmw-20qLP)inU* z^{X{5Xoe=PU+W51Ovt|1r2VJtw{Fu0z=Qgd)PngJjac(5FuAb7r!w~9S-%(2`~9c; zpRzs5{%`o(Z1c0QQiUbeH62A>i1Wc7{qwY*rr$suqsAGvXJx|j;%;0(W2Y{RGDFs zU||ZVXzi<#e-iz(TZ9wDDUM`8Kc*rFMGUn1u0q-E{CD)-qW@LeIruseXX*O-1r$iv zcZ?cEctRxGI)dms+@RXZQL}VTm&`1j)7joM1`nxG3g?Ra_le2{mD*=TK_yBSzd@ zw_etG{A8!_2}5pf79=36N@r_s?h~5#g-h2bG*iu{1J4Q)rj7E>fBeYonceG!oe_{T zt%n0{vS)Gi4YjRT!$8p$ma$P3?X&?>PH1n!f$*fMA1z8)CyaTPE326kM9pRW@Vfk9 zsL?F_ zwrs0x*>_-);pz>;off8wq4CK)H!8cfZmqjv-;&NS;+SQ~9X}xDeSB_T!;{cc`yU_@ zWPxFQ>#Qa^?V)*p3ldcwoq&HJzSR7Zy$jwzI+oCV6)>KHqumQ&gYXQxUj;3T_?R|| zSAiEmKu=bHHq?JO6`Xal8&Nv-uH>5-Y)|Yz<-b7%zm?eB?OxEuYI~&+I*Jf^>y<}^ zXUwdH+CV(~IRSGJI}Gtd9^lhGmL*6j?arun}OHY$@=04Gf9+P%ce2bj`sT-2Bn zWV^J)DTgq4&9R(o*M%cuFzwtmrDt2>!DP}+zxUYxUqIu|#RVI~Z-rOgS}7L`_S?cu zt$@;^+8#Za#g5W1jVDBk3a*1i3PoVvgL@1w=+>5GqUlBHV63=+85srs0?1#+R8Tt| zhTS)0E3S>(;Bs0hPl)OYxbtjORBwq1Bh^ktwy70S3;5@sVyi!wJU_PD4XovfhEKuP zKTj*~FU5ZmUCI9QCSU9zIU{DOe=_~^F8bvgq+X!Y)jhn2AdJ?U*3bVZLW>^8sl^h% zbGjY>mcES$!Pxuz#kc(PmYz(2=X(dbktKwDHZ3|@L7~V0u>0zN9gMwfEE@{y3wk9s zG`3>ZIcJjCoRCNQaMWKKPA1vs`W-(jacIJ$5>utio8sXOeCSp5W>gEcI1cZ!u zczCRvtJWL%if4xqbu9qd@%fN<4t&obB|5em3eo-|$7HW>l)!v1Aq;@j~xgtN*!FU&S zGZ!?$NLNk$S_ri1DO>!F8dzTz1T0`*#-BD!j(GtQF5pOf`P)c`!9 zZuk8Z6~_<;D0uCHV%#aAuR#qqCcm9%Pv(K?W@19?V0=Ln&TSCN$g(c;SgdQZ(6oN3 zp#=8~K-K1#LMXgq#ecK2#O-Fe4UA}4DfDQX;nkMwMztddHnZL|u0{Sn5J#7Rq@p62 z)}irS>r;4O_2X+(UJ~&oN;c8Q@1^A;+qZ)ZSEA79S()d<(2DOuiDhJxffj& zmmQkE?-q0Hqm$EjZp20dN?C!=`GyoiEhPy~M+fvnCVYk4S#YXp;s?pK5&T1}rk-Xh z1XB4Az%4rUA#z0-0c2~@OGg(_?2#o^1<0qM_D57KyrJKJ14(UR3xJ*tFccdN*>ogY zpHt&^R^#hvOh#l;Rgg}DrcJ;c-c2-ZFtEWL^gB>B5q%Eo6&c2kJjLUO8QXuW zhJi}vD;yP@<}e#@58|zAXE-kYjDK;Ro3EX>+&u7S-kUhZ)iE=wt$eP?SXUKO-;*Ve zL3Tcu0F*b#q=iVyHKkY+)~TOA4dp}gsndCXwjT|MpCom90D1?|5P&g8NwY-&1nvad z8B|9hF9)?9W==>v)66C&OF7I@Xp&G6MV_C;oNT~bV4WA)VCp`kv%rf%7-QL#r>T$c!`(5>t&Yo3 zJd;9{8wPT453wKW;4790y8vVbtXe-hpkbiM^3aP|OnglEDvf<2JvWbs|%{pcCU_j{xIDLA&uft?T z-WJL*Cka=*~0h^tJtWs zKoc}+5T-M^1>d0xY96!(y_iA!hvuyRDQYPuIsz{O;`Dj!AFKwBtj}H|ev_~GMH>Xh zg>bn#cbMqDw2H7Zaue+(zoQ$(QEVv~1lnMnGghPKFT_Ee~(q{YK zpT%CbZVh2f+P2xXG5shXQ!$%(uQS2YQQ>=uh#5$9YjB;Sj;U7QK&|M)STzVD#n|pO zjcjj8!ov=A37@!a1{_|=>_bEwdc{oyEY=z%MQ&mNf;H%1(6M2gBUllXemD?tsf(%x zW2y?x$n2A>3aijDVBF_g0V57_Ie2T^hN>2rJ{`KaTyECXT_boyiJy{Uop;tY?^LBn zu}S*=u+Vw6aO-&2RgqvKbnh!~p62%ny(fMdssojlBCd6$^$1wg49R>kPZ# zC$7>y)LQ3mwj;Wc)ImfvjZRY)@8E8%OXqW&jOabr%`AspiqbwXnCLIno{MS;B@qgK z20CtXNX%x1mvyT(x^BV5pNkOx3+}w^#3R`)C=#^i1s&ZMEy8b8IR6~Au(@ZN#D%e@ z`p4Duvl9IN)CZQ9&=4ETx7m~XqC6cFw7^jRa$ znpV}drM;C~X0Gi9KSC|%8Uy-KgY~y6!lkW(Li?ig?Wcf2KL)je8LCMkD;4o$(^5(( zl;Fq4VMPK1;u@mQ5U@?!4Gjn$C_k!aNcdg-KlKZ-H+<3@AV1_!^j7Sgv2N|>+;&4o zB63a`+#OB4cr1sU-cS?kMs`}EWKa}8XD(Fl_m-~R+g3%@kZMP54}>r#!-l^ECydT?A=0WMmTqRqXaehE0n-1{Ed z)RYQ4hrh7l+|nhg2slYj#)7dQ)t*})Ei@m9hGKE@tD;|H{v7)EJBa%NSESm=`>4_Z z3KxPK(x9UCg)>7`i%IZd*f3ZyxGjHMD89D?A>OZZ;^9z4SFLz(i9TC~whmZZ*h+XG z;wii;olGT3C@>QKSfkszizVT?_zg`b7VJV#?EzPnt|~q|INrLI9qz_>S5a$eeyDpV z#_0{usxd8$A$;jJvj~}?ziO%8(;U9NG4YXQ;#YHBwGZ6>lCyD(g!8j-!?7NcfqtO& z-LWGVX9Q_&`8Z^Rvi7ccIB?O5BdRgm@A8q{A8fb=BI&l)nU-cNRwOe%TzqtU%IJ6= za5&X3C*d>Rhx(<3u7Uxm(xZEn1aA;x;@77VE`^`i?ZgJ6ND=0lHor%T26%$>saFlt z8S(|6=vlyqpu$dtdH+)SoB~jCC76u@$K!_y@JnR%sf3RP1su3{`EaN~QviyyRu4zv zGw?B9rMmEjCY+kVVd3g=Xd^<8fSJ#Z;?%RQh1*SY14cHR=G^0`MlwhTp(q1l7t?#a zLW*^jF!`^$DRh=zEwU5F896C!DzNGdbFP3mXxYs~INqeH!cQMSFCZ^EthnwOqj2Mj z`_JWH&i+|rI5cY}PhlkBWGO#xZ^dY5I04}^pE{y9Lkl4l(FYMjCfyV0AW{pZ&TVEH zjW6g0#JXBm%!uHJbbp+YwZww)Cq?DvdpvvZA1GlBjBGRX2hhBi*nJRg%GCGlBy<&M zmSAVY`^Zw&Maum>3mfh9glNAi^a$y+;-u0h?gi*fe}iD;KXdqgS`Pn#V|U|XvhN7t zeg8|Vil_Ng zcBma=jwjD!;(N6xmj%~k)NC(|&wwehr4Qp#_ARGLqj2MGd=z8N9>asMqjBT_#mVTt zaHBbJ2*mXlS!`R!?9n%#>bStI2#bUcl%0Dz%(HL3Vupqu6vS+4Hxkw&x34i4T!Ug= zvO<#`Fpu3}6@=7I)`K|nxruB(5G}gHMHC4;@no@#VF1mPw_=0OU$^!b&^sfoISbL=_<7}yj7E3PB z=k5yaET!MFR;IgE$=QoC!?H^RC(>nz*I4PGY({E^UFobn_+tz3e-HAk!e^{No#fk4X^cc54Pm=aY&LHUw(<`qdVSY5j8gla6> zyQpW87P8;mw^;d@|D1Qg=N?7AtV27e2Q~f%hxQ>h8fwaDhXAZXyN%o|8iUCoAWx+< z#doQFFf2B;0AZUWv#lErKUgFa1v5gM--Q!KXAk&lFu?v8Rgd;G`g9OW_Jg$ZH@5jb zVLO{a%@&P)FHG3rnjBu{T!A<|U-MS*J*8Oo^Jv<-3KRtsQccP8yWpzFKznBsyRivX z0ei70cN`e>#aG6JEneld*SrsB{`yuv1biOQL5zZB7b97^?=J35=Dn5yP4Byd-Gxd{Vj{eO^H* zxSsK26#Vjbo?42?_lk59fotj)cAC%%Di#!6>pq^OnWI+JJcU5-E(p2e)CAB z^cw8stjj0RRL9aH3x4rR%^>2_FeG=MjkmF+{Z&|D#*fVA4@O|Q#hGzvFLZ3fWyDD_#MN;<_Jb+Kwg@|yDIS$?6KH!BL(XaNu00UciJ3pU*WJGRrp}X zTb&^;kqwZch4ooH>lCWR4){^S4R1HwfiHO9*L3xucv%m~_|IP=Jc{7e~LpZ+e#p7*g9Td&35GDi@ zTm&;bOuat+Z(4uov>zI@*^DzHNlpLGru7iiyOXc5zJRS3*G=)Q1-KWRnREY~C}eka z^!F{^+r>k=Tbi}<2hHr?m6FBrbATNzyC1m>-wHM2s}Gl!ww~aBCYr*F5Axt0!owpf zD{qOje6Vd-?LoD0q;k%6Do*)KajvseiC%E<$x(PEDpvoYlL#u`(PL1 z(6DJiH{&j1rq%h=RGWF%*doUEHQ9WlT(`-t4B#AmDWJ1JTnV@J0b7Vu&{#acyOg7l z!Zo5a3SZ}O*Cv=0>#L$TY|k)pAYAm zE}B3qhreTnz*A3{_D|TQh3#RrEhGLC>TkZe{oPy1>}!%9SMmwrLNAFSv4Ps+NH4!n zTcsGz)YGREiUvZ3B`*!eb!`PfWJ#@kW$E&DiXm_@XBwj2H#AmzG8SxBKb`YzQ4O0h zTejGlw5du0@AmMli}SuNglwRrVLNK3m`Q=i?Y*wfjr!>TBDLRt{-C9xXB1hXS}$)= zY5Se!NegwfoXXgwn2fjsv$~KOIQ>*f@t-mAdI3X0`V3|rA_5QCjS%v%y$(bAlU#Q5 zYE842FC<4-Hbh}=^Pfikc}w0sEPT2xfP%0S`HT>h;_ZPCb%yfMAFmE)6}u%~`x6g3 zXkE;fO(nE&F4V$>SUgdilR`P?WXv(+DL6SqaP%;j#Lg!NmqGcq;QWV^``tezS1KPj zUze5J^aY6b6#Z`01JP(=S|8KYycQ4wXP72rQes#Lq@CKrT=e;30;HL6)Kor(;UX;& ziIZF;o^99L4@j3w#fA{}aW8ZRp)B9*;^8hv-yQ;qx`Yw}2|tOdS03qA1QHBShQa|P z;rO)K=b)f+O=?Xz#X`{sjz(kf+aRJkG{mgZ{x!TZc=v@^lJ z_+J(Ou$YEELpmJMUly{aId*wynbyIgD=5)5LhZ&D zZp!jaLO_fLTck(jP!2VEWOcRM;$|X%(vu!B6k$rQ{VbA5X_&yJ2_3xl6%{%)=>Q^) zc-W6LHpP+#YEb6fhte&koS(7Pk|+MoOPh!@-e@tB)_#NvR(8=5LvusQ$_t97)qGnC zwZYn7%7P{=O}RL0APm+%uBV(JuYI==6`}lNHKG$pWct~F5KEy6=_D8;IZ;H`OY2d7 zPh8gtCJ1!U|HqM?>{X$F*-U`E1QX&vb~3~7X38)r)Xss$BWRVXgYR7tibI;9fB9}G z_ox3kl&g)@-d5g>)cy(Vi`rLY6Y@wX1UL6x8!jZ#yCjC8bhI{=jwM6UA3Kdrf20So zx8vB-wZ9QUPX2!y!>Q&ITsUgq5vaY?fpO-?lC~?$JS^_QgdovNn6f9B;V7hzEIqQK z3*+BHYVE_hD4uqSIrH4%al|1vK_}9HCh<5bZCBFHu2PkOLN~+=1cLx3q#z8A4)kj! z0-h6QeZUhs#0i84#n(#kix_%CZVn`hDx4b%89iusMX*LqR4sW(A>gj{s3q@{xL!KK_s`fA3DgCf9WyO?etp={SG00A>>9))9 z#G$6tpi}r%QB>>~gPVnY;}#TC8UGUD_!aM%%Xbyw5nnk{RInXxTrAZ_h#d;6Z(L=SnLj+?1`LJ2`7UsBZzNhY=qrgSrq7I&i#p zu2fmqv$f-IPES<^Dt8ZTy6OH2wjH#1jEec_`ik&9B~*qK_dKiKj{RL^9>HPhHd) z$?|gENld6tPwrUw>njr?Ri^+y*;0@fX-w^pOH8zat#PT8Plh0n+_*IrQoVe3p#k;5oW!IEKs|9JmOx*9z^sjW zF-J|OewR+AV$H?IV5cKBKq8*3%pJNH?~cb!3g%aP@(m_l-}Z$Y=s?mwqwa=Y;J2`*;Md0ac<1K;w=@p{UccbzcOHI_1jXQaiu+ zdjIR_+;{aHDmEiHFE``71RCP-1Z7P5n?TR>6O`|`zJf5$1tb*!sDLm}YRG+MSP=@< zN$#2waAY*T8WSRLYQUm+(=N&|ITWe;K{`$XWk`g)P-_--AbJ%@ZMOITJJM2og|I5T zg>D?iO$eQ&gNj>_*aNa8>>*$?1h~|P8BpMfMXmk|lRG|3(dExMyGHGD=*oq*oME}kEE(Wboihok48`(4}eu3eaR%5!1iTf3fj z`!OoZzcgLM9O zzx{&rIJB~QkPocDT$tM^2cri{D2fjpA`Ze61qeg<)I2VZ14Pe|TEEEe_RB5QzZ0Ry zt`y0<_4iewlRuAy`Swz#b&SwUxQwhKRp==5Z2}_T_kzOwiIDJu5v7n8h>Y;_tYdU1 zw?7e!vaZI#1($7 zezP-Day?46@Q|JR3}IDl$AFjYi!gp}VYAJ^9<%RWyatm<#@caco`z71qofnHM{jsz z*69@P`ZVx2XM_E*ljAoI-||*zcGUtwS%k4AzeUlbqy!fS*;x#RfR@jcx!T1M`3&g9 z(4AE`2G_>fYJ69H375n9z5SZkI>2I3-p9)1MDKnFL+6Jh(Rv+}L9gti(hdG>sx)G% zhp+;5q#YtfXsq{P12`B0-(kc0K+7UcF<0_o1Dq}LD>yqi6qJkAQ3EuY(MP^c4e}~H zRlEa|A1+lb$xGGi=k%cKXU%WU?L#I;jthTCmxL1Py3B7K7pr37P}&rh;8#llZoCjY z{&5XuZd)p=?{_icuRJ};&fsa+^8JDQ=1cNs?Jf7fzE7FPwq>pqUw4l?J>piqqMRv~ zF_2Y;e#41@Q+$6K;8sP|W<3>sM-0dEcxPW_%8I>`RSn@VOIe9tqc4FL-*|B0iv^5S z8Y~IlIv42KzV|b=Yw}L>77McWL-FG;Wd;2RW>RA^l7)Z5+R~$_wy?F$5fQUJ!neB? z7&q!wb}VVJEms#KQubvG84lb%&QGpF!3#hp3+PuEo22-Sy<1G4b7HGj6(rWjzyu%h!=y`vH7v} z3ERP1k@}E|ECoDV(oVoN0M`9BFu!A|h(&8btrAC!QfF9KT!sblOXU<$NdB?GV$*5G z&qPZlJ~)aFdQ6EDYVCZQd*Ly51c!rYasz13?2 zQ2sf&tr}>_p5|EG>7+t~33ht| zItjP9$pb&EZ5ueQB8cu69{%cyQI4epk17ky{|3~F<3N}D5#6mvtr({a4-kt0EQt_f z!qn&iqx^*hq(}~DjI0|>9-cA!=5Z;knqMG5PYO;)wHLVx9zUJTg8vnY5NW;XzA~1b zZY0ub!2@Ac9Z(EV`~~VkN@HXNn~LypemLl#X2P?)N^jLhz;}$(c4QCl%l%*q)pb+& zl@YNI3Ypjim!;XtMerDq`QIR@7$<2!P5^5EH5^F+!cE<^zqTvC9m!^8tM*pvI~Ur? zqL(aej`EH15SYcRcHfop=kpL)l~wUxFhp2a?;dV$DnJg9y=52UO+j?IheHKPbu(9> zad9fgAemCkyZz?Hs2fl3HQ!`$bexoX;_=$Qt=yZp7YmhQjHwk?v{bWFwQbYYz5<~d z9z$kj7HYe@N?mHGEW{x^dO-a~6x}Me+4@Y;8OS|Y{ATUTiAZ1@IIqq6@SDQd2&_`T zJNhWFDw-3Lhi}^1a96|Q$ZSyChuD&T1ZcuuPZiDDv`|<&iXi#`G1oO`B!4jhtPqx3 zUtU@loHSZ#tSCw@q!dLT;sexg=p^CkD-mKyelTg(|(d;hAB6MZ}0(>(22akdw28H$I`-!ZOkjqE?i}qU;d%}|3C4e z(4f~CUWeH)lehg35Ce%-!zSo}Gqf#D}XIwxBEMRKo`_ za}KB@H3EYcgXdc%Fc(h`#|Z2mS|P-T&#Ui*)j61#t8#UI7y1 zNWoVL_?b#e!iDMZ^YqD`+>;vXF?4I7wVo!k^ zX7XtKdA@N7&vfo+b*%lONH*Fxp6p9M&!dq;C-T_?e8<-z!`ZZcNsk_F+K@eQmTd?k zZiI2G79D4Yd7L%20JP^_hzvDL3!yWMO|wDnzW>8K7ql^4*5)s7>W*-pg4piD2d8$R z7Ib^x<4tFsQY7(6?ZoKbQ z_lF}_4P?#Ut!tLqj(IE?e+bG!tKWlJt^$s>t6>jV7Y8Xu{2yYMga!8zL4)A8IwQja zumf_2Uqr%(F2xx>3)!m_$aT>%1J0(R!lXi?$p2R&`gxz^Lp<|1CZR$kEc2f+%*Wrw zFz;f4Jg?n`S#)VknGQQZ*xoM&#IW7UYK!cYqFM_2g>KA2kT{*Gy}U>mxqJ~{a&)OM ze$7(8^15IOc!i=T+W!K~ZWafwF8&YU%-31K>&^*A$m$iYOGjj;7;bH6N|-G`8BaDN zwFXywEx=P4l^s`AU?5_xM>@l9##*$<*UQJ)Nu{zKIGAsj|w(y z79BRsPp^wX`3V~S!kWPPA!$QENGdLLCVCuP+Jh*ko|92t1#e0Eo~WuIROyL;T#v=` zRWuOzVA_L!j`3w`HwVqKUM-+d)DKmVi4kamGfPJix5MYaIeK0J^1pElV#5e>D0ZNX zc6|sPT;0R7Ze|@{XP8>;mUiug3Th!^a#O0GwFiLsm*8+<)>O@8=;=eZx|>E82PPc; zduRd)o_QApz}VgBGzEQ5Rb^f)MO)RiciTW```7ie!T#cv3wVn>`cR8$PE_AwZ{^CM za+=DpOvt_jM>snJXSiuD&0NR5r`L@X!SM*bzG>PFoSH&~!gWP7HG> zGcr*@Z8#|m9nhOUuXmRAjxN|V88g~L`D2*daa&A%V7m6bHMM_V))C&$Al%kNrW@rt z_4pxS_cCt;EQ(_DhcR(}dQ)oM5HmpTwODouP&4HK_Cd+VtRncxgO_Y?ScsGPNW&)) zn?g$yrZW%DELN-P^Jk&tr}%rJf=~p7ZgPx;KAL8X^o;B$776+d=v1v28j(56NZ0C; z0$6f*L3KwQu{U+B9PWWR3io|Oo`6@ipk4`5)`E^?$jQTTZDyH zOp=mjDaOXoGKfd+<@zCw8iR*i9P8o-cv5`P?w7QlyG7-)5N>YQguXRere`JeGWuI3 zX)lE2Bf&A)Sg7nFt8+nBW&BXByV11Fsj}I#?cZlLOfwcf9E-U|NR3}0cd4;#(aG{{ z9eeN1!oQ-Ke!v~8?qH>AwpTUt=S1bmj$mVP$7jynGSr625&I72XNS*B9{O4B?#Yd9 zYM09X^o2cV+%;|XG%F1h47AlF-tnN<_A=_A%Q1&(JFM&pcuHby;b8b6BzzgAZ2%VY zNaCUuxqeDv66x3=q@>gg;U&(=(%+0Y6k)AWuXXzGga0g&4mQO`eyWGt zQ%M=9riz__0w1CxT6!#4`jCN>ix6c0a%ysa0~VH6V?c>nQoQdZ05Wo?P{6vnr#~@wc7A0vS*qLhIQLK(0+jT<{=|Uuk-T@1;Z+a;3MNHNWqkzjk zehk6~ zcE(EJAF68lymBl8K`DSqAZ&mz1*Zsvb~;J0oQOl9YXWnLKLCm;^2BF25;%gDDva(s zOaY%qbOh=cf(Fl9!Ak$nTEMSnDW12^9kqI&N>`)^VIBTNW1P^QiPC zO{56ucI>^z&7WN`yt>NlIdZv$`FR|%0Xy*LE z2IM0~)#jgv-hAZA)C8j7!Lv+DOii-S#Ju5ijeTITLgULwg`#FLoL)EgwO#J`>Q!tY z(kPr38nVU$SjlOu<@9$O~lHd5jvhcTj1m}ff{2EBs+xfcR;NVP!Yggv&(sbTd^fs`{5%&v6pkb8B;^%!9;qK{4I-aY4X@%% z(qn|PBB@$}5`B#>ddxl(q28TBCk5xON<*k<&RspBtK-VOgmeLjp*9jaJ|d{Gh+Olk zpM6k&2%WEY-sI;R6%UaU?FkBjuxR)Re5*jmn^qJHj#nUc?%N0YkTwkdf>9{a`s3}s zKruc?@<~4$m6v*p!ba7B?7VtDomTmY&TYclKbDghJnFKcOPiOe+7Hai6yz%1R~2E` zBoe?*g|Sn4r7>m%mti4dSudmR1yTAQc%R9z56;v7R3)PmFuKvQ(X3h&!5WVwQ3Oc`V5^Ac1-U8Q}Ha(0x zv$HD1(qL1v*iRhMkvSo^(7sR+3q3AfhqFBld}kdz=ze%mLQ-))aqQp$$zJ5JlLJcjJ|PoM5}OLTR=~79AUuG-af)dh0G~k}qMure$gDT_ zF+u`JdC8A#v7|8kA<_du!wT&LAEbcTHc%r8F*%tP7;RxC5uP9Lyx41O^DPq6wAK45 z05Q>X!};SEBJnRi-xYDd(zL&>?hw?5w-xpqP_B@$PyS*Bvnw{Mk;xXiYja|Ou$*9H z?1;yX8?2UVij1)Hp)f$$&*OC|(eDH;@JVzOskA#g;%9&1YYm2 zVj2OhHl8(=qp-wu0;&7}yGo0Im&slbq)^P0HOB4HT_#~!eLF(bJ**z3Q$8#~>srU3 z@E#DLhysN@;A2K)GW`1z=ujs}kxRhDyT=%UnxqFn>^z@jG53GFJ*eBFoy!F^T9QuK zU4;v19=gi*R4|S2!V-MmBQTnr?HsB-soldJ^WbXWM`J+aiZ-Si<;}*Wyj&1NP@ASZ z1-gI;IyPLm95g$(F}AHI%iq|-? zpEes~JCw*b&wByAG!1GbJ8LReVjQkV5 z19`JJnaj@oB~+e=4+cfRT53cRD!1o*mIPO-#sUh(VEd`xhircdC@$5i?rV6K;xTwo zmrfa_n?AZu1a9A0Q}0HdV@8HLk5)2l4=fW9AhPE;m9)zEloDJ29YCDu9%Q+3WC7Wu zo{v>UaNx&Qv=7;Z{29_3lg+9m7l=uq>?6_LYT}T`wx+Gm(_kVTw=ouEtU%( zW5=T|zaq;z7wcLn>-0Md942hVScz9l>Dor7H}|0OglC1Bd|T@ z2>bFUcy63kAf~WSY7Z?%>nU%|ut>Bxc3qTDx=W6lQv^0ET5@8--m4C(R#m_K{9ssc z{^+usdio7Bb@6hwJT_nyxG}40>zY{Bn@MORkTN?fN3{h z;eAWhzxnQ&A1U3*@{ZcCo}P*~nfb|yrHc(#G!8OV7Vo0f`_yrs4EOb44x&)G#6fvH$g)p#_nLjF}aJHu^i)EIWk-#<0( z+SR;p@JCfJOuv-B;DaSW+u@C6nbgwj z4igO+{(E#jHXrcoGdqz1U*B*qPB&Hr*goOHu=n+?0DF&Mt=_gvWCEH{Kp{B!icpn; zE1W>MYm9e*pY{(b&4s{$g%_zJ2EZcIk;hjQAZ92jP?(1WN8E@nQ3l+Sr?{JJL`3vo z1U(9?N?8?7y*QeG7SQlT-9`#@>)uZ>>FeXaZj6F^4;CH+LtvKi#|CMU?>M-^t#YGQ zWK+DHei&>3b*m(GUc&f@+CLo)pl1Ktr@Js-gAD~lNcQ2s^3EYx`fCvcaN)Cwcyb() zbC8PFPn6BXf^20Nm++qyRk?MlV;e-w1wF~kcnb@M&O4RbWch<1I>PeiG2;*iLaHam zWZiKy&EOvIf-gv|$YA#8V-99+2UI=%I*aYlx|-x#$4i+dZK2-fZe@8$Xc0iZ_0Qu7 z=D^;Gn96~N9%qhj9U=Am+sYoa%jwX6M zDpmI#BnfaQ48Z)XE}xu0_#_!-Ft6}ML~pR%Rot^~yM=$qi;I=_rD8nyp&|KRh5{z5 zTrj9!3y%yjy5+!~i~xfxerAlDrJh-w$OC|zjAX4eq3$v|Z0}jvXF+vNV1V5!gwGJL z>3PhHo;0dYf-KzD3C*b_KQ|l>DjzYX*jDO@iuMl3&}wAwKg!gzgBYBVH`&*bZwqPk zg(09-SKx$2^dseJz6@Q?;-we$I*k_?sBSlI!_4!(W*!iBhlY+W(j%{BU;mMLT7@B`>U)V3~Uk79;;ZXq+7J}-10|IaZOsdTnh^jU!JkME?7KOglcs^c8@A* z{1^tfsUtS#4P&at&S=kS!Q#cx(5>Ju+>F=~ve#gmH%2X%Cm&pf4?YPFIR_##3@yFO z8eT$Oj?g1tnh}eKE**S^oqz*FZB;m2SUv0m0!Q#kFPYmSYBxplviPWnj#fR8rY%K| z)nB11B5J2z%7LlHfgBXDQ}Wm-i1BC<$@=1BivOr^Lz}{P==cg|_R`8z1)TtE3MT8M zB6O?aqbLNSsnSHyHAQH`x322R^83|8iFE1}U=qtZC1V53CgY7O%Z+z}^2Uoz5f&4! z>i2%;xqKWWDd3KA0YGT;?rE$oda0*Si#@8SELj^I<%f_jH)3)kd!34f`Nbm#Z-6P|64fD?+^g2hHhFdo?ZQpByYEN4oUp6Qh;d4{HC~zHGR8J?e-9 z!ouCcmM%Nly~eieT(aZ_BiSt1+dNEy>%*|PX%;&vI~#6f4j^c4kb*uX%wm_IX@RrV z?H~{o$D$G6PU{U}%uzonRltIvNvx-VHh;C@dkz022m)#o)KyC?=;%D9-PeGE5G5FO zIJG!@DB@5!HP37(8m})w=7-_9fDSC)jba403aER-CXj&3?~FFlUNFq1i3l9B=qM29zqPkoF<6iLweA5 zZZ`A~<}E5>IMm_uVsg$?dQubxNz=IP7*GG-?+9Xi35K)=f<3k=DTZk~v$Jkc&DoNM zk?xF#OyhSyGHlgpdQ;V7MlvOAN;rzdgF&M<91;HBnTq`Gr&=(qr{im(!kv*73^?H{ zm2oqaTpkKoHhy+!gTb3Mh4D6~h65#8RK!LN-Lb0_Fd7RWt80&kIt?xS)o<_JHwyx` z2`-IkLt&VWCn3@k-MjVbm$yfRrxXj^RRI(D#DI6JUf5eImL?@B{%2?9p-0zG1NEK2wiD zXM6_5)`z`$O@Vsi5v4DGoB6(t>XPZ2Dt<|+M~p<^T5r1jv-MhQ$Dw;_v0s1atN%Jy zaE~osn~3R3dPDIefBMSa>25FDSL8P)l4xzI+_|`Wc(63#rMoX5)VZ?j%sBem;;~X7 zTAaCM5di|ddFk*R+{*dHE)LMX1lPLQxCihnIjY&#qH4?8*k*E~1(K}Ej1}4UfsIk> z)1yP$x@rb9?uOayvgj;vSwQ-&YxLRfFvGAKysqtCD{P)UL3a9VC+@LvOVEm%Ur~TK z2<_3YEI34tQrrsIo;WQbI|%F~v68e2RDuFB1avHkE8$EUG{P3@>##@$(*5!?Np0!E zU`^7Jl`1mj0~r34U?=!VN+o6$VsR4HEaeOK=#$9=oa8r88-F==f~9L0z?szNdL8xq zw~i)${rY+;l*r$2kChHM@mcDOWmS4fzdLV^Wc_4Fy{29?E@hoEJ1S&$t$l?Nd57_( z9S~s4ciIVO`#~y~P^A6;G~B59otfLV72Hsw{VSVD7}VEed5X*l$_zI${OeD}Z_%|2 zki{PZf6~Q4Jx@n9VJ~FMwpM3P4a8Ov^|)$$o~oz5=`ww;8F#O|A~{h>P~mtSr&?NnY#P)3mf7CRtq7Gn@pf-{l|mRIA%BrTF~ zC3eCw@|6!sadoA-&x^lqAq3~UfUWUjIhp)Oe9x7)>wjUnZ z5A|Oi-e1dpu-JD~PiSh|T`X?cIHxi<_6EJrQ@{%W=J~EjN1bX3`oGFH)n@s=YMZ zYTwuX_(I_Z4gE}gCy)wOV}4CJM6U8y@PxfgfZb0&_1j&4Z*ZZdP0DdF0Y`#y@k^;k z27QS`;i^n>VAK9obmxH-mDP*l5gay#z}P{FL8Kl-ak*L~6f&RJNjZ2ynQ-U$a2E9vT>{ZRBWOW+Ri*>6}q-Mx1?v(NH2A zIvakVCv68p`QNev7IZxwuuGx-xmctaYK^?TJ8xGU_t=grLlNib?$9vr@Abb5!~Y4S zb2QSzQ1yTLccpyqmL(??IDq=L)83KMNt$frOo#F;oiarNka|mE&&TmqgX}X9Y~(=K zOFk;%cbbVH!YlPzrT%~mfrv^M|AMJBJN;#HSF!-VE+c|W3xyKuoj(Un-2}cxkO8v9 zGZBWGQ{wW(+4=s@bfqFL8 z^v-Q4hr9CwOfIf!B_*<&FwN6fz0-+Rvg(mScj7W@b5Bh^#g{^?dGqXKs68^X#wa_J zsQ1pPmmZrA_GTk*Sx~WX|MpO9q#O5fp{UNe3rk2~^)K0i|C&3p;jPH6kO|s$BPOC-&Z{sHxFEQH3(14{e&{ zC2Bv$>7D&7`*QR`B;h0?D^9jMyf_h5;r$0?D)PqK?NWy!t~%eEVcy(9OpZ zCo9-%)oShC5bVuet4a;%(ciWJd^R|d=qDEhE1UOg@HFwOn>lW&!@tt?U1At##=(&J z(Hwyr2MK5z3YXuu63POn=nr?x?nOKZv@EH92RINZxuvxgBKu&pHJg^>^g|y&JPJV; zSqBl0Qkt?kOR~b5hAbbF!&-PwMyy8M5p5W1{|*d8P@59x#S2RPEw2D91R@9OBNpgq zv~L1=^wfa}E;2Wf<@%scuH0Lb0#5;8)#FA6v>;qSSgMI1q_#8>pnprF#Ys-$cj!$j+Fo68-Eh z{piBsxw6fu)dfg70Io1R_1+9Ge701kouQ4tdTovjDLgnzKvHLT3%S79R33g7+A4DL z9REE3)7vZaeZlGUX#2-EOg38V_jvfkIhG1W#?@-N&ovJq=yg8JE@ocSzDK9BV;j8` z!d}-U)vm+2p1}?LZP5A_dy6aGS6zU8QVTKmO&&L2#$iA)TH_=S^c68@T#DNuE}_Tqy0Z<3U3wMdqEb!isj%Xl)LNj#zCZlqLvNwgNOH%p4}X!4XP<6Vle zuLaMD%;yws*Y@{0&&m~!y21em@j{94062Hloz}`#{h$149`y)2CdOO;c)_)}uN*%ixJejRSdX(;nSK zh*4-1YSGryZq>_Atu64A!aZzH-gc$ldM_)KuOgE=r>pUO`<=j;3ZeK0pT?PAYoDc4 zcj3SGvShLb-700hXTANQp?^G?oQ|)@Vt_Z<-Nv-Bc2G57;S`E9R?yDFuXDoP)$BPa zSydg36|wbbtcu~~T$1*zs_y=s>&A=42ctpTI$fPz7#)Q?$DMdAJ2VgpCGFY5QmUrH zX1u7vu`_jDI1N*?Wv!mR`LX6$PfUa->H0BzR$9K(zOU#5EVMdR5~kKCnvhlnKp2C>wTNq?$iY6z zy|Xwsejx6J{2N;og1B#?STG)PT9&~H5-e_tED(&aqq}7J&&C`tJ_Jh>hef2lxB%79 z%B4~x7~=l$sGVm{bvB~u&52v2rIa~cQbBPig%*Qqvo#gioR0u3lmclrfw?#}mAS^r$I3QB zp|EL7Ia?2~7KHS=HRu|lyNyf9*W<+-$1P?bi)2vFm5nS1s8`Mubd6<}v6@~ewA)4> z1(#W~%Kxw>lUAv?aa*x)#c1=uZ)MkJv(LYUWz6T;eHx3X>du&3OW3v4Y*TOCda+9;NpNj^*ZZy`4ot`=v= zKf|*&oX>%XHVZXEm+EKaDY3u(-anams6Mebj*o5`uJMUtihOF8xsvz8I~ee5zo&_u z-}mcy&J)jpi=b}yQELCNFa-&pa&*lJGutFc`Dl$ik`u-t#J)hj#ToguRuUQp0RHm2uTU$Ag}$AvBJ#>i^0}3KZ;t3P{FOaCsJ-^qo=No?}2quFR1n2dH3Oiv68o}X6{2C?1#qxO3fm|;#5azciF1oBoa@* zwcql*X5D&QWx|Ul7mnr}6hvyv&XG5|>R;b2V1)8Ek;5Oy;7?# zT-M{npL$OYvRtcjF_td-T!en~MXo~S>A|iqcd$T-M1ua%JC=Cm13&@?`IvwoKc@oz zSrfQ8wNAc6?87jy8lSUIgcHJ9Cq>5~Wm1Wx{vv|tM>laOzJL@QT;HyUnlkGGaD?Z> z93x$$t=K7fVtFPOvMRuN`yWE)Qtpxj$*Fp1LQum;bj4_obV1zoEytnEW zEiV+_om6&)5C8U3DV8_xxvnn^!=yiuUTnXx+;nHjk%=WB11&bE;lT9Wsa}tT#m~RmZ#nT&(=CvR+5NUNVwsa(t>2bRiZRgo zct(Xc)N4rPg_ko4$-9fp!@PqXJNGcd^!cto>H5!Ie<7O1tqxKkvnpw%P&k~1LAfs) zX#|>1tq#yY^^Z83FQDOzX^4u#?E@lj$}tLH1O|QE`SI;&{tIW|0}exkO`zC+cG_C zp!FZ4-5YN+mJx1}a=9>g)R@~6$&~-D+YCl~GPfCb1S9N0a#IQW{F91Z8^J(6tW(>! zCg+;Qqei9}j^~`=d@z>`hn1Oo`=FgdQz;sY70bq3@_jTOv>w?wWJe+cV_A&?Lcoa) z8i%9FtYt*g^a~yd82>dM$c%(dH&YCK7d4mEmdg`I#sXfh8EL=wYUA(&YoP<5GK{(K zM@23*9cr0&AeOe1+MX-q8C~BT4uvDuM2{KlPsB2F5g9w(mUn3ZY|;jTHn}hNblYmM zmI`(|-?T16F~o~IQ&VB)K@x1^8BON_#En4cWbhwYwL4rjp4mUY=iISYEVbvIzxJ8i zhusocP9T)b7@24!xSUIxM&{sZK5ruNin83UbRiZr3#B(IW!DKYK8= zGq@$Vpo}|eeaU??FDG^WJIqOHx;z_wM%ll67;ftmFyr92vYwFE8jhf!dX20Hi;?aW zvePo@gL4R7grBAQ1HY6Un!D}iGejyMQ-<==30}O0#58e0oq|r>MUX6-=ZL?;RS~s# znxsEcAxiY4dm%p;4#fkBU#JRw+ z@3-491mnj4^oJT%_&+k0^T#pzq#rYt+Fc2SRN?XdxHrBV#Q!n6O*RBH$(oJPqWVvN zpradJxlk$uBkhOFg}*O5ZmIB3DI29k#APFFF4d$+evX6-P>-usWo^vg8MpS~h4?4i z*5@wSUuw5>pQmcO-@leyn90?sE;l69F(|BPB9&;Ntyx{oW5{wzeWc(|8B-M3v2;Y4WmZMS6= z2vyrLIFk>WWjs-qxv$+Y*so+F?=I9FpJV##udx$OTvE@9fY79;)K4Ut7w3Ki)ghg2 z$~j9;B4O=^RxbtIi4`jrI6;bNoJNoS;6&xXRB={aor(+B90M1=_und^+EK(bZ zc;rKV%0tB_H5~3u?8*ybPJT`ktdP~9K{)Z(_g$6kF9ctD% zxV?g2=HP6o{T0PvyD~0E=R_upoVL3|H}%&F9?XyN0I1WtQsd)=_D_nY#_sGC@#X5k z$PQkJ;$`qwrS?v%c(DDMnQYvdZCr5s`p1F{j{M-raKnr~sWz0)I4P&GKrxB#XJxm! zscUEqpE zd8le$w%4-W2DTK6Xd_Wj-Z4^;ndR(d#Y=iU^&De7fqBLROXih^HKuIJ{wvS7e^LM9 zAEZyx9b>F!Y3Pu%#wuice{Nm%?$gmJ#)tK6_$Y-O{OfHo7EVRzd7oW6A9uWC-iqCG zG+usgDOI@!Hb~u_zN5!fCl9KEwbX#ZRH)UbbCDil+b`bDiY{FbZ;17r-PHZcU z^5B6iDbuQP>vtNJi1Tmwq)gn%?DuB;)Y-1T^LuPq1*3<6iWELRQ0>Pp`C5P<2`S8a zca(fMWo*uON8$=H2|uUB5;#AyCV8|Ap-cuZ{PG?*!f+H1!a|6Qm;r4yFBvVySAf2Tm!+F#!oEcOu znE-wL_dUBP2Y2l%AR%+ z-I#G!`Vy;5_v02Frcu%57=FvPq`F~;OJSi>PmX!)sBY@-ZAF*Rw94Ct)y@IqaDk}` zwxchz>((Lq=>teR^Ae(NdM>jOA+}3%_2|ih*J|D!9ey}g8EC(C=I}We?gL+q03Jqn<ltv^^6nr$D@ZK-uz4eR zk=R&IS53C%p*%&(%^}p}u>H`-m!cp*0u8vy>ElPR2@YP2LE?_IcIDB&O}uF%>6%~I zOJARnJDx%zjfL>jw6G8TX;+?5g*(bq#s2&e1gXJ&8b-g^oLCIMPO5r*oDU-K6N)e? z%sXf#j5PdJpM~G*_(1WC*t$=6mi!E}71W-m@OnO{J;YB0*cWRShbzulpa==})8A1q z@)^}WsP-M<1m7~cx8TLAj&b=|#Z&3=nH`0Jt-aCSe93L8zQcvzWw>NVG5fIALDEN6 z+m}%d+AHg-e+KX1I3k-ghe9{J$ES#hZq6wDx9MU$lJ`pX;JdVbiXf++TvLk=p1e%C zKiO|izO|{4^|ks>rqysdOLD9+-)Zd0$>dTeOskJ^k@QK|uIr*(*tkn2POhf!n^6}) zqPMBgxJ=Fi%Bwz(#e+ut;T~<{BzzIP)Eu{ zybC_eN4Y87C4Z(%M*zc!d^?r&O#-9ea1p9GkH-0k>2_?cc2Q$PBx`&jHF0M|U0T4> zlA|8Dh^dq4h0ISbG_k3FO$fst;L=s>oNluWd3lpN9~=cE~@GcFYCe$(TvLP?vI25kx(Xak~$yU zyaCB^%<|HG(`sYjyL9*8anLkxHEhy%u!EAm0qOg1&({t;FbEmNwc8%85zgV}#_4_0 z^5&YQj2nytWbw3z9@l6T;ZdVc+^w@C6DI|(KN06&>N{S_ai{yjs=xna)qzve6H+tR zG+pobRo%e}eo-<_6*<7hS5)ur?}>OSl{PJJXz%99q`vo3r851>`s10jR_Uoz>+y$Q zT(?J>#x>?g65S=W1RE~fD3p0G%CzlpG@e~b9aE7jc2AjBridn6ctg9a^rw%{92lef zc=m!}>^U;NO@m1e_&kLfUz7PLJnO$Pu%{HxqHl5zO;uO`P!s+_H}G8ZesJl1`G08lmHcJC=9ZR4HqV$!nA-9I2}QdE`kjB zAOBlnXAe%pJGl5g-(bpb{;B`u&i4cV?&Vwm{%=P-=C3Dchv;H~dH63$lGpc#(;L&7 z%4juh^pQ8)Ab+{gRx(SrTnCplCA zou;K<6Y-w>Qpk7JV02Tw7VlD8o@SsP-Q`@p(^8MHDtya{tzS7gq66>0ar)%TB-LTX0qETRjYCHSx?p5KU*?mmsbD29Q;I&^u7CXoAFR;l54XQDd$ z1C4N@wLeV%k9eu?KSDoJeK%_@sqcO@mUwMoCmrzdYs`DMmm-10@bJ~zt+G#mR%xni z^K|uvA-eIRI&EyNqBG@KMF_{L$`Tigx8y1+;;cSV(lw`$uo|dfBHbOTmQ$h8nsPVL z;7*C$r3!Lid1LhF>3+o-k_y{au22lIpqEzAhM?B*fpIe(haafR6IGpxW?w$*HX#

This is a Palpo homeserver running with Octos AI bot support.

` and `
`) from the given `text`. pub fn trim_start_html_whitespace(mut text: &str) -> &str { @@ -589,9 +612,7 @@ pub fn linkify_get_urls<'t>( const MAILTO: &str = "mailto:"; use linkify::{Link, LinkFinder, LinkKind}; - let mut links = LinkFinder::new() - .links(text) - .peekable(); + let mut links = LinkFinder::new().links(text).peekable(); if links.peek().is_none() { return Cow::Borrowed(text); } @@ -611,18 +632,19 @@ pub fn linkify_get_urls<'t>( let link_txt = link.as_str(); // Only linkify the URL if it's not already part of an HTML or mailto href attribute. - let is_link_within_href_attr = text.get(.. link.start()) - .is_some_and(ends_with_href); + let is_link_within_href_attr = text.get(..link.start()).is_some_and(ends_with_href); let is_link_within_html_tag = |link: &Link| { - text.get(link.end() ..) + text.get(link.end()..) .is_some_and(|after| after.trim_end().starts_with("
")) }; let is_mailto_link_within_href_attr = |link: &Link| { - if !matches!(link.kind(), LinkKind::Email) { return false; } + if !matches!(link.kind(), LinkKind::Email) { + return false; + } let mailto_start = link.start().saturating_sub(MAILTO.len()); - text.get(mailto_start .. link.start()) + text.get(mailto_start..link.start()) .is_some_and(|t| t == MAILTO) - .then(|| text.get(.. mailto_start)) + .then(|| text.get(..mailto_start)) .flatten() .is_some_and(ends_with_href) }; @@ -668,9 +690,7 @@ pub fn linkify_get_urls<'t>( } last_end_index = link.end(); } - linkified_text.push_str( - &escaped(text.get(last_end_index..).unwrap_or_default()) - ); + linkified_text.push_str(&escaped(text.get(last_end_index..).unwrap_or_default())); Cow::Owned(linkified_text) } @@ -696,7 +716,7 @@ pub fn ends_with_href(text: &str) -> bool { match substr.as_bytes().last() { Some(b'\'' | b'"') => { if substr - .get(.. substr.len().saturating_sub(1)) + .get(..substr.len().saturating_sub(1)) .map(|s| { substr = s.trim_end(); substr.as_bytes().last() == Some(&b'=') @@ -729,19 +749,19 @@ pub fn ends_with_href(text: &str) -> bool { /// ``` pub fn human_readable_list(names: &[S], limit: usize) -> String where - S: AsRef + S: AsRef, { let mut result = String::new(); match names.len() { 0 => return result, // early return if no names provided 1 => { result.push_str(names[0].as_ref()); - }, + } 2 => { result.push_str(names[0].as_ref()); result.push_str(" and "); result.push_str(names[1].as_ref()); - }, + } _ => { let display_count = names.len().min(limit); for (i, name) in names.iter().take(display_count - 1).enumerate() { @@ -769,7 +789,6 @@ where result } - /// Returns the sender's display name if available. /// /// If not available, and if the `room_id` is provided, this function will @@ -832,7 +851,12 @@ pub fn safe_substring_by_byte_indices(text: &str, start_byte: usize, end_byte: u /// Safely replaces text between byte indices with a new string, /// ensuring proper grapheme boundaries are respected -pub fn safe_replace_by_byte_indices(text: &str, start_byte: usize, end_byte: usize, replacement: &str) -> String { +pub fn safe_replace_by_byte_indices( + text: &str, + start_byte: usize, + end_byte: usize, + replacement: &str, +) -> String { let text_graphemes: Vec<&str> = text.graphemes(true).collect(); let start_grapheme_idx = byte_index_to_grapheme_index(text, start_byte); @@ -877,7 +901,10 @@ pub struct RoomNameId { impl RoomNameId { /// Create a new `RoomNameId` with the given display name and room ID. pub fn new(display_name: RoomDisplayName, room_id: OwnedRoomId) -> Self { - Self { display_name, room_id } + Self { + display_name, + room_id, + } } /// Creates a new `RoomNameId` with an empty display name. @@ -939,19 +966,20 @@ impl PartialEq for RoomNameId { self.room_id == other.room_id } } -impl Eq for RoomNameId { } +impl Eq for RoomNameId {} impl std::fmt::Debug for RoomNameId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut ds = f.debug_struct("RoomNameId"); match &self.display_name { RoomDisplayName::Empty => ds.field("name", &"Empty"), - RoomDisplayName::EmptyWas(name) => ds.field("name", &format!("Empty Room (was \"{name}\")")), + RoomDisplayName::EmptyWas(name) => { + ds.field("name", &format!("Empty Room (was \"{name}\")")) + } RoomDisplayName::Aliased(name) | RoomDisplayName::Calculated(name) - | RoomDisplayName::Named(name) => ds.field("name", name) + | RoomDisplayName::Named(name) => ds.field("name", name), }; - ds.field("ID", &self.room_id) - .finish() + ds.field("ID", &self.room_id).finish() } } impl std::ops::Deref for RoomNameId { @@ -1011,15 +1039,16 @@ impl From<(Option, OwnedRoomId)> for RoomNameId { /// /// Skips the first character if it is a `#` or `!`, the sigils used for Room aliases and Room IDs. pub fn avatar_from_room_name(room_name: Option<&str>) -> FetchedRoomAvatar { - let first = room_name.and_then(|rn| rn - .graphemes(true) - .find(|&g| g != "#" && g != "!") - .map(ToString::to_string) - ).unwrap_or_else(|| String::from("?")); + let first = room_name + .and_then(|rn| { + rn.graphemes(true) + .find(|&g| g != "#" && g != "!") + .map(ToString::to_string) + }) + .unwrap_or_else(|| String::from("?")); FetchedRoomAvatar::Text(first) } - #[cfg(test)] mod tests_room_name { use super::*; @@ -1034,7 +1063,10 @@ mod tests_room_name { #[test] fn to_string_prefers_display_name() { let room_id = sample_room_id("!preferred:example.org"); - let room_name = RoomNameId::new(RoomDisplayName::Named("Hello World".into()), room_id.clone()); + let room_name = RoomNameId::new( + RoomDisplayName::Named("Hello World".into()), + room_id.clone(), + ); assert_eq!(room_name.to_string(), "Hello World"); assert_eq!(room_name.room_id().as_str(), room_id.as_str()); } @@ -1043,7 +1075,10 @@ mod tests_room_name { fn to_string_falls_back_to_id_when_empty() { let room_id = sample_room_id("!fallback:example.org"); let room_name = RoomNameId::new(RoomDisplayName::Empty, room_id.clone()); - assert_eq!(room_name.to_string(), format!("Room ID {}", room_id.as_str())); + assert_eq!( + room_name.to_string(), + format!("Room ID {}", room_id.as_str()) + ); } #[test] @@ -1087,7 +1122,34 @@ mod tests_human_readable_list { #[test] fn test_human_readable_list_long() { - let names: Vec<&str> = vec!["Alice", "Bob", "Charlie", "Dennis", "Eudora", "Fanny", "Gina", "Hiroshi", "Ivan", "James", "Karen", "Lisa", "Michael", "Nathan", "Oliver", "Peter", "Quentin", "Rachel", "Sally", "Tanya", "Ulysses", "Victor", "William", "Xenia", "Yuval", "Zachariah"]; + let names: Vec<&str> = vec![ + "Alice", + "Bob", + "Charlie", + "Dennis", + "Eudora", + "Fanny", + "Gina", + "Hiroshi", + "Ivan", + "James", + "Karen", + "Lisa", + "Michael", + "Nathan", + "Oliver", + "Peter", + "Quentin", + "Rachel", + "Sally", + "Tanya", + "Ulysses", + "Victor", + "William", + "Xenia", + "Yuval", + "Zachariah", + ]; let result = human_readable_list(&names, 3); assert_eq!(result, "Alice, Bob, Charlie, and 23 others"); } @@ -1106,7 +1168,8 @@ mod tests_linkify { #[test] fn test_linkify1() { let text = "Check out this website: https://example.com"; - let expected = "Check out this website: https://example.com"; + let expected = + "Check out this website: https://example.com"; let actual = linkify(text, false); println!("{:?}", actual.as_ref()); assert_eq!(actual.as_ref(), expected); @@ -1136,7 +1199,6 @@ mod tests_linkify { assert_eq!(actual.as_ref(), expected); } - #[test] fn test_linkify5() { let text = "html test Link title Link 2 https://example.com"; @@ -1181,7 +1243,6 @@ mod tests_linkify { assert_eq!(linkify(text, true).as_ref(), expected); } - #[test] fn test_linkify11() { let text = "And then https://google.com call read_until or other BufRead methods."; @@ -1198,8 +1259,10 @@ mod tests_linkify { #[test] fn test_linkify13() { - let text = "Check out this website: https://example.com"; - let expected = "Check out this website: https://example.com"; + let text = + "Check out this website: https://example.com"; + let expected = + "Check out this website: https://example.com"; assert_eq!(linkify(text, true).as_ref(), expected); } diff --git a/src/verification.rs b/src/verification.rs index 85e503f02..0585c9e4b 100644 --- a/src/verification.rs +++ b/src/verification.rs @@ -4,15 +4,24 @@ use makepad_widgets::{log, Cx}; use matrix_sdk_base::crypto::{AcceptedProtocols, CancelInfo, EmojiShortAuthString}; use matrix_sdk::{ encryption::{ - verification::{SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState}, VerificationState}, ruma::{ + verification::{ + SasState, SasVerification, Verification, VerificationRequest, VerificationRequestState, + }, + VerificationState, + }, + ruma::{ events::{ key::verification::{request::ToDeviceKeyVerificationRequestEvent, VerificationMethod}, room::message::{MessageType, OriginalSyncRoomMessageEvent}, }, UserId, - }, Client + }, + Client, +}; +use tokio::{ + runtime::Handle, + sync::mpsc::{UnboundedReceiver, UnboundedSender}, }; -use tokio::{runtime::Handle, sync::mpsc::{UnboundedReceiver, UnboundedSender}}; #[derive(Clone, Debug, Default)] pub enum VerificationStateAction { @@ -23,7 +32,10 @@ pub enum VerificationStateAction { pub fn add_verification_event_handlers_and_sync_client(client: Client) { let mut verification_state_subscriber = client.encryption().verification_state(); - log!("Initial verification state is {:?}", verification_state_subscriber.get()); + log!( + "Initial verification state is {:?}", + verification_state_subscriber.get() + ); Handle::current().spawn(async move { while let Some(state) = verification_state_subscriber.next().await { log!("Received a verification state update: {state:?}"); @@ -42,8 +54,7 @@ pub fn add_verification_event_handlers_and_sync_client(client: Client) { .await { Handle::current().spawn(request_verification_handler(client, request)); - } - else { + } else { // warning!("Skipping invalid verification request from {}, transaction ID: {}\n Content: {:?}", // ev.sender, ev.content.transaction_id, ev.content, // ); @@ -60,22 +71,28 @@ pub fn add_verification_event_handlers_and_sync_client(client: Client) { .await { Handle::current().spawn(request_verification_handler(client, request)); - } - else { + } else { // warning!("Skipping invalid verification request from {}, event ID: {}\n Content: {:?}", // ev.sender, ev.event_id, ev.content, // ); } } - } + }, ); } - async fn dump_devices(user_id: &UserId, client: &Client) -> String { let mut devices = String::new(); - for device in client.encryption().get_user_devices(user_id).await.unwrap().devices() { - let current = client.device_id().is_some_and(|id| id == device.device_id()); + for device in client + .encryption() + .get_user_devices(user_id) + .await + .unwrap() + .devices() + { + let current = client + .device_id() + .is_some_and(|id| id == device.device_id()); devices.push_str(&format!( " {:<10} {:<30} {:<}{}\n", device.device_id(), @@ -84,12 +101,16 @@ async fn dump_devices(user_id: &UserId, client: &Client) -> String { if current { " <-- this device" } else { "" }, )); } - format!("Currently-known devices of user {user_id}:\n{}", - if devices.is_empty() { " (none)" } else { &devices }, + format!( + "Currently-known devices of user {user_id}:\n{}", + if devices.is_empty() { + " (none)" + } else { + &devices + }, ) } - async fn sas_verification_handler( client: Client, sas: SasVerification, @@ -100,7 +121,10 @@ async fn sas_verification_handler( &sas.other_device().user_id(), &sas.other_device().device_id() ); - log!("[Pre-verification] {}", dump_devices(sas.other_device().user_id(), &client).await); + log!( + "[Pre-verification] {}", + dump_devices(sas.other_device().user_id(), &client).await + ); let mut stream = sas.changes(); // Accept the SAS verification with both default methods: emoji and decimal. @@ -114,12 +138,11 @@ async fn sas_verification_handler( let mut receiver_opt = Some(response_receiver); while let Some(state) = stream.next().await { match state { - SasState::Created { .. } - | SasState::Started { .. } => { } // we've already passed these states + SasState::Created { .. } | SasState::Started { .. } => {} // we've already passed these states - SasState::Accepted { accepted_protocols } => Cx::post_action( - VerificationAction::SasAccepted(accepted_protocols) - ), + SasState::Accepted { accepted_protocols } => { + Cx::post_action(VerificationAction::SasAccepted(accepted_protocols)) + } SasState::KeysExchanged { emojis, decimals } => { Cx::post_action(VerificationAction::KeysExchanged { emojis, decimals }); @@ -132,7 +155,9 @@ async fn sas_verification_handler( log!("User confirmed SAS verification keys"); if let Err(e) = sas2.confirm().await { log!("Failed to confirm SAS verification keys; error: {:?}", e); - Cx::post_action(VerificationAction::SasConfirmationError(Arc::new(e))); + Cx::post_action(VerificationAction::SasConfirmationError( + Arc::new(e), + )); } // If successful, SAS verification will now transition to the Confirmed state, // which will be sent to the main UI thread in the `SasState::Confirmed` match arm below. @@ -148,14 +173,17 @@ async fn sas_verification_handler( // confirmed their keys match the ones we have *before* we confirmed them. log!("The other side confirmed that the displayed keys matched."); }; - } SasState::Confirmed => Cx::post_action(VerificationAction::SasConfirmed), - SasState::Done { verified_devices, verified_identities } => { + SasState::Done { + verified_devices, + verified_identities, + } => { let device = sas.other_device(); - log!("SAS verification done. + log!( + "SAS verification done. Devices: {verified_devices:?} Identities: {verified_identities:?}", ); @@ -165,7 +193,10 @@ async fn sas_verification_handler( device.device_id(), device.local_trust_state() ); - log!("[Post-verification] {}", dump_devices(sas.other_device().user_id(), &client).await); + log!( + "[Post-verification] {}", + dump_devices(sas.other_device().user_id(), &client).await + ); // We go ahead and send the RequestCompleted action here, // because it is not guaranteed that the VerificationRequestState stream loop // will receive an update an enter the `Done` state. @@ -173,7 +204,10 @@ async fn sas_verification_handler( break; } SasState::Cancelled(cancel_info) => { - log!("SAS verification has been cancelled, reason: {}", cancel_info.reason()); + log!( + "SAS verification has been cancelled, reason: {}", + cancel_info.reason() + ); // We go ahead and send the RequestCancelled action here, // because it is not guaranteed that the VerificationRequestState stream loop // will receive an update an enter the `Cancelled` state. @@ -185,61 +219,78 @@ async fn sas_verification_handler( } async fn request_verification_handler(client: Client, request: VerificationRequest) { - log!("Received a verification request in room {:?}: {:?}", request.room_id(), request.state()); - let (sender, mut response_receiver) = tokio::sync::mpsc::unbounded_channel::(); - Cx::post_action( - VerificationAction::RequestReceived( - VerificationRequestActionState { - request: request.clone(), - response_sender: sender.clone(), - } - ) + log!( + "Received a verification request in room {:?}: {:?}", + request.room_id(), + request.state() ); + let (sender, mut response_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + Cx::post_action(VerificationAction::RequestReceived( + VerificationRequestActionState { + request: request.clone(), + response_sender: sender.clone(), + }, + )); let mut stream = request.changes(); // We currently only support SAS verification. let supported_methods = vec![VerificationMethod::SasV1]; match response_receiver.recv().await { - Some(VerificationUserResponse::Accept) => match request.accept_with_methods(supported_methods).await { - Ok(()) => { - Cx::post_action(VerificationAction::RequestAccepted); - // Fall through to the stream loop below. - } - Err(e) => { - Cx::post_action(VerificationAction::RequestAcceptError(Arc::new(e))); - return; + Some(VerificationUserResponse::Accept) => { + match request.accept_with_methods(supported_methods).await { + Ok(()) => { + Cx::post_action(VerificationAction::RequestAccepted); + // Fall through to the stream loop below. + } + Err(e) => { + Cx::post_action(VerificationAction::RequestAcceptError(Arc::new(e))); + return; + } } } Some(VerificationUserResponse::Cancel) | None => match request.cancel().await { - Ok(()) => { } // response will be sent in the stream loop below + Ok(()) => {} // response will be sent in the stream loop below Err(e) => { Cx::post_action(VerificationAction::RequestCancelError(Arc::new(e))); return; } - } + }, }; while let Some(state) = stream.next().await { match state { VerificationRequestState::Created { .. } | VerificationRequestState::Requested { .. } - | VerificationRequestState::Ready { .. } => { } + | VerificationRequestState::Ready { .. } => {} VerificationRequestState::Transitioned { verification } => match verification { // We only support SAS verification. Verification::SasV1(sas) => { log!("Verification request transitioned to SAS V1."); - Handle::current().spawn(sas_verification_handler(client, sas, response_receiver)); + Handle::current().spawn(sas_verification_handler( + client, + sas, + response_receiver, + )); return; } unsupported => { - log!("Verification request transitioned to unsupported method: {:?}", unsupported); - Cx::post_action(VerificationAction::RequestTransitionedToUnsupportedMethod(unsupported)); + log!( + "Verification request transitioned to unsupported method: {:?}", + unsupported + ); + Cx::post_action(VerificationAction::RequestTransitionedToUnsupportedMethod( + unsupported, + )); return; } - } + }, VerificationRequestState::Cancelled(info) => { - log!("Verification request was cancelled, reason: {}", info.reason()); + log!( + "Verification request was cancelled, reason: {}", + info.reason() + ); Cx::post_action(VerificationAction::RequestCancelled(info)); } VerificationRequestState::Done => { @@ -251,7 +302,6 @@ async fn request_verification_handler(client: Client, request: VerificationReque } } - /// Actions related to verification that should be handled by the top-level app context. #[derive(Clone, Debug, Default)] pub enum VerificationAction { diff --git a/src/verification_modal.rs b/src/verification_modal.rs index 2dcdc78db..b231c2fdc 100644 --- a/src/verification_modal.rs +++ b/src/verification_modal.rs @@ -3,7 +3,9 @@ use std::borrow::Cow; use makepad_widgets::*; use matrix_sdk::encryption::verification::Verification; -use crate::verification::{VerificationAction, VerificationRequestActionState, VerificationUserResponse}; +use crate::verification::{ + VerificationAction, VerificationRequestActionState, VerificationUserResponse, +}; script_mod! { use mod.prelude.widgets.* @@ -89,12 +91,15 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct VerificationModal { - #[deref] view: View, - #[rust] state: Option, + #[deref] + view: View, + #[rust] + state: Option, /// Whether the modal is in a "final" state, /// meaning that the verification process has ended /// and that any further interaction with it should close the modal. - #[rust(false)] is_final: bool, + #[rust(false)] + is_final: bool, } /// Actions emitted by the `VerificationModal`. @@ -158,7 +163,10 @@ impl WidgetMatchEvent for VerificationModal { VerificationAction::RequestCancelled(cancel_info) => { self.label(cx, ids!(prompt)).set_text( cx, - &format!("Verification request was cancelled: {}", cancel_info.reason()) + &format!( + "Verification request was cancelled: {}", + cancel_info.reason() + ), ); accept_button.set_enabled(cx, true); accept_button.set_text(cx, "Ok"); @@ -170,7 +178,7 @@ impl WidgetMatchEvent for VerificationModal { self.label(cx, ids!(prompt)).set_text( cx, "You successfully accepted the verification request.\n\n\ - Waiting for the other device to agree on verification methods..." + Waiting for the other device to agree on verification methods...", ); accept_button.set_enabled(cx, false); accept_button.set_text(cx, "Waiting..."); @@ -180,7 +188,8 @@ impl WidgetMatchEvent for VerificationModal { } VerificationAction::RequestAcceptError(error) => { - self.label(cx, ids!(prompt)).set_text(cx, + self.label(cx, ids!(prompt)).set_text( + cx, &format!( "Error accepting verification request: {}\n\n\ Please try the verification process again.", @@ -196,7 +205,7 @@ impl WidgetMatchEvent for VerificationModal { VerificationAction::RequestCancelError(error) => { self.label(cx, ids!(prompt)).set_text( cx, - &format!("Error cancelling verification request: {}.", error) + &format!("Error cancelling verification request: {}.", error), ); accept_button.set_enabled(cx, true); accept_button.set_text(cx, "Ok"); @@ -226,7 +235,7 @@ impl WidgetMatchEvent for VerificationModal { self.label(cx, ids!(prompt)).set_text( cx, "Both sides have accepted the same verification method(s).\n\n\ - Waiting for both devices to exchange keys..." + Waiting for both devices to exchange keys...", ); accept_button.set_enabled(cx, false); accept_button.set_text(cx, "Waiting..."); @@ -241,7 +250,8 @@ impl WidgetMatchEvent for VerificationModal { "Keys have been exchanged. Please verify the following emoji:\ \n {}\n\n\ Do these emoji keys match?", - emoji_list.emojis + emoji_list + .emojis .iter() .map(|em| format!("{} ({})", em.symbol, em.description)) .collect::>() @@ -267,7 +277,7 @@ impl WidgetMatchEvent for VerificationModal { self.label(cx, ids!(prompt)).set_text( cx, "You successfully confirmed the Short Auth String keys.\n\n\ - Waiting for the other device to confirm..." + Waiting for the other device to confirm...", ); accept_button.set_enabled(cx, false); accept_button.set_text(cx, "Waiting..."); @@ -288,13 +298,14 @@ impl WidgetMatchEvent for VerificationModal { } VerificationAction::RequestCompleted => { - self.label(cx, ids!(prompt)).set_text(cx, "Verification completed successfully!"); + self.label(cx, ids!(prompt)) + .set_text(cx, "Verification completed successfully!"); accept_button.set_text(cx, "Ok"); accept_button.set_enabled(cx, true); cancel_button.set_visible(cx, false); self.is_final = true; } - _ => { } + _ => {} } // If we received a `VerificationAction`, we need to redraw the modal content. needs_redraw = true; @@ -313,25 +324,21 @@ impl VerificationModal { self.is_final = false; } - fn initialize_with_data( - &mut self, - cx: &mut Cx, - state: VerificationRequestActionState, - ) { + fn initialize_with_data(&mut self, cx: &mut Cx, state: VerificationRequestActionState) { log!("Initializing verification modal with state: {:?}", state); let request = &state.request; let prompt_text = if request.is_self_verification() { Cow::from("Do you wish to verify your own device?") } else { if let Some(room_id) = request.room_id() { - format!("Do you wish to verify user {} in room {}?", + format!( + "Do you wish to verify user {} in room {}?", request.other_user_id(), room_id, - ).into() + ) + .into() } else { - format!("Do you wish to verify user {}?", - request.other_user_id() - ).into() + format!("Do you wish to verify user {}?", request.other_user_id()).into() } }; self.label(cx, ids!(prompt)).set_text(cx, &prompt_text); @@ -351,11 +358,7 @@ impl VerificationModal { } impl VerificationModalRef { - pub fn initialize_with_data( - &self, - cx: &mut Cx, - state: VerificationRequestActionState, - ) { + pub fn initialize_with_data(&self, cx: &mut Cx, state: VerificationRequestActionState) { if let Some(mut inner) = self.borrow_mut() { inner.initialize_with_data(cx, state); } From 5ac8c40ec034738bb8a4fa84b9e1e7de86668248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 24 Mar 2026 06:40:48 +0800 Subject: [PATCH 003/283] Finish migrate app service and register to makepad 2.0 --- src/app.rs | 139 ++++++++- src/home/app_service_panel.rs | 234 ++++++++++++++ src/home/create_bot_modal.rs | 315 +++++++++++++++++++ src/home/delete_bot_modal.rs | 249 +++++++++++++++ src/home/home_screen.rs | 2 +- src/home/mod.rs | 6 + src/home/room_context_menu.rs | 62 +++- src/home/room_screen.rs | 523 +++++++++++++++++++++++++++++++- src/home/rooms_list.rs | 7 + src/room/room_input_bar.rs | 64 ++++ src/settings/bot_settings.rs | 214 +++++++++++++ src/settings/mod.rs | 2 + src/settings/settings_screen.rs | 29 +- src/sliding_sync.rs | 72 +++++ 14 files changed, 1904 insertions(+), 14 deletions(-) create mode 100644 src/home/app_service_panel.rs create mode 100644 src/home/create_bot_modal.rs create mode 100644 src/home/delete_bot_modal.rs create mode 100644 src/settings/bot_settings.rs diff --git a/src/app.rs b/src/app.rs index e506eb4b0..65aae738d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,7 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; use matrix_sdk::{ RoomState, - ruma::{OwnedEventId, OwnedRoomId, RoomId}, + ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}, }; use serde::{Deserialize, Serialize}; use crate::{ @@ -449,6 +449,54 @@ impl MatchEvent for App { cx.action(MainDesktopUiAction::LoadDockFromAppState); continue; } + Some(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id, + warning, + }) => { + self.app_state + .bot_settings + .set_room_bound(room_id.clone(), *bound); + let kind = if warning.is_some() { + PopupKind::Warning + } else { + PopupKind::Success + }; + let message = match (*bound, bot_user_id.as_ref(), warning.as_deref()) { + (true, Some(bot_user_id), Some(warning)) => { + format!( + "BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}" + ) + } + (true, Some(bot_user_id), None) => { + format!("Bound room {room_id} to BotFather {bot_user_id}.") + } + (false, Some(bot_user_id), Some(warning)) => { + format!( + "Unbound BotFather {bot_user_id} from room {room_id}, with warning: {warning}" + ) + } + (false, Some(bot_user_id), None) => { + format!("Unbound BotFather {bot_user_id} from room {room_id}.") + } + (false, None, Some(warning)) => { + format!("Unbound room {room_id} from BotFather, with warning: {warning}") + } + (false, None, None) => { + format!("Unbound room {room_id} from BotFather.") + } + (true, None, Some(warning)) => { + format!("BotFather is available for room {room_id}, with warning: {warning}") + } + (true, None, None) => { + format!("Bound room {room_id} to BotFather.") + } + }; + enqueue_popup_notification(message, kind, Some(5.0)); + self.ui.redraw(cx); + continue; + } Some(AppStateAction::NavigateToRoom { room_to_close, destination_room, @@ -1051,6 +1099,88 @@ pub struct AppState { pub saved_dock_state_per_space: HashMap, /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, + /// Local configuration and UI state for bot-assisted room binding. + #[serde(default)] + pub bot_settings: BotSettingsState, +} + +/// Local app service settings persisted per Matrix account. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct BotSettingsState { + /// Whether app service related UI and commands are enabled. + pub enabled: bool, + /// The configured BotFather user, either as a full MXID or localpart. + pub botfather_user_id: String, + /// Rooms currently considered bound to BotFather. + pub bound_rooms: Vec, +} + +impl Default for BotSettingsState { + fn default() -> Self { + Self { + enabled: false, + botfather_user_id: Self::DEFAULT_BOTFATHER_LOCALPART.to_string(), + bound_rooms: Vec::new(), + } + } +} + +impl BotSettingsState { + pub const DEFAULT_BOTFATHER_LOCALPART: &'static str = "bot"; + + pub fn is_room_bound(&self, room_id: &RoomId) -> bool { + self.bound_rooms + .iter() + .any(|bound_room_id| bound_room_id == room_id) + } + + pub fn set_room_bound(&mut self, room_id: OwnedRoomId, bound: bool) { + if bound { + if !self.is_room_bound(&room_id) { + self.bound_rooms.push(room_id); + self.bound_rooms + .sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + } + } else { + self.bound_rooms + .retain(|existing_room_id| existing_room_id != &room_id); + } + } + + pub fn resolved_bot_user_id( + &self, + current_user_id: Option<&UserId>, + ) -> Result { + let raw = self.botfather_user_id.trim(); + if raw.starts_with('@') || raw.contains(':') { + let full_user_id = if raw.starts_with('@') { + raw.to_string() + } else { + format!("@{raw}") + }; + return UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid bot user ID: {full_user_id}")); + } + + let Some(current_user_id) = current_user_id else { + return Err( + "Current user ID is unavailable, so the bot homeserver cannot be resolved." + .into(), + ); + }; + + let localpart = if raw.is_empty() { + Self::DEFAULT_BOTFATHER_LOCALPART + } else { + raw + }; + let full_user_id = format!("@{localpart}:{}", current_user_id.server_name()); + UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid bot user ID: {full_user_id}")) + } } /// A snapshot of the main dock: all state needed to restore the dock tabs/layout. @@ -1194,6 +1324,13 @@ pub enum AppStateAction { /// The given app state was loaded from persistent storage /// and is ready to be restored. RestoreAppStateFromPersistentState(AppState), + /// A room-level BotFather bind or unbind action completed. + BotRoomBindingUpdated { + room_id: OwnedRoomId, + bound: bool, + bot_user_id: Option, + warning: Option, + }, /// The given room was successfully loaded from the homeserver /// and is now known to our client. /// diff --git a/src/home/app_service_panel.rs b/src/home/app_service_panel.rs new file mode 100644 index 000000000..51de116e8 --- /dev/null +++ b/src/home/app_service_panel.rs @@ -0,0 +1,234 @@ +use makepad_widgets::*; + +use crate::home::room_screen::RoomScreenProps; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + + mod.widgets.AppServicePanel = #(AppServicePanel::register_widget(vm)) { + visible: false + width: Fill + height: Fit + margin: Inset{left: 12, right: 12, top: 6, bottom: 8} + flow: Down + align: Align{x: 0.0, y: 0.0} + + card := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 10 + padding: Inset{top: 12, right: 12, bottom: 12, left: 12} + + show_bg: true + draw_bg +: { + color: #xEEF4FB + border_radius: 14.0 + border_size: 1.0 + border_color: #xD6E2F0 + } + + header := View { + width: Fill + height: Fit + flow: Right + spacing: 10 + align: Align{y: 0.5} + + title_group := View { + width: Fill + height: Fit + flow: Down + spacing: 4 + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT {font_size: 11.5} + color: #111 + } + text: "App Service Actions" + } + + subtitle := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.0} + color: #556070 + } + text: "Commands are sent into this room after BotFather is bound, similar to an inline bot tools card." + } + } + + dismiss_button := RobrixIconButton { + width: Fit + height: Fit + padding: 8 + spacing: 0 + align: Align{x: 0.5, y: 0.0} + draw_icon.svg: (ICON_CLOSE) + draw_icon.color: #66768A + icon_walk: Walk{width: 14, height: 14, margin: 0} + draw_bg +: { + border_size: 0 + border_radius: 999.0 + color: #0000 + color_hover: #00000012 + color_down: #x0000001e + } + text: "" + } + } + + first_row := View { + width: Fill + height: Fit + flow: Right + spacing: 8 + + create_button := RobrixPositiveIconButton { + width: Fill + padding: Inset{top: 11, bottom: 11, left: 12, right: 12} + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16} + text: "Create Bot" + } + + list_button := RobrixNeutralIconButton { + width: Fill + padding: Inset{top: 11, bottom: 11, left: 12, right: 12} + draw_icon.svg: (ICON_SEARCH) + icon_walk: Walk{width: 15, height: 15} + text: "List Bots" + } + } + + second_row := View { + width: Fill + height: Fit + flow: Right + spacing: 8 + + delete_button := RobrixNegativeIconButton { + width: Fill + padding: Inset{top: 11, bottom: 11, left: 12, right: 12} + draw_icon.svg: (ICON_TRASH) + icon_walk: Walk{width: 16, height: 16} + text: "Delete Bot" + } + + help_button := RobrixNeutralIconButton { + width: Fill + padding: Inset{top: 11, bottom: 11, left: 12, right: 12} + draw_icon.svg: (ICON_INFO) + icon_walk: Walk{width: 15, height: 15} + text: "Bot Help" + } + } + + third_row := View { + width: Fill + height: Fit + flow: Right + spacing: 8 + + unbind_button := RobrixNeutralIconButton { + width: Fill + padding: Inset{top: 11, bottom: 11, left: 12, right: 12} + draw_icon.svg: (ICON_HIERARCHY) + icon_walk: Walk{width: 16, height: 16} + text: "Unbind BotFather" + } + } + } + } +} + +#[derive(Clone, Debug, Default)] +pub enum AppServicePanelAction { + Dismiss, + OpenCreateBotModal, + OpenDeleteBotModal, + SendListBots, + SendBotHelp, + Unbind, + #[default] + None, +} + +impl ActionDefaultRef for AppServicePanelAction { + fn default_ref() -> &'static Self { + static DEFAULT: AppServicePanelAction = AppServicePanelAction::None; + &DEFAULT + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct AppServicePanel { + #[deref] + view: View, +} + +impl Widget for AppServicePanel { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + let room_screen_props = scope + .props + .get::() + .expect("BUG: RoomScreenProps should be available in Scope::props for AppServicePanel"); + + if let Event::Actions(actions) = event { + if self.view.button(cx, ids!(card.header.dismiss_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::Dismiss, + ); + } + + if self.view.button(cx, ids!(card.first_row.create_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::OpenCreateBotModal, + ); + } + + if self.view.button(cx, ids!(card.first_row.list_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::SendListBots, + ); + } + + if self.view.button(cx, ids!(card.second_row.delete_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::OpenDeleteBotModal, + ); + } + + if self.view.button(cx, ids!(card.second_row.help_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::SendBotHelp, + ); + } + + if self.view.button(cx, ids!(card.third_row.unbind_button)).clicked(actions) { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::Unbind, + ); + } + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} diff --git a/src/home/create_bot_modal.rs b/src/home/create_bot_modal.rs new file mode 100644 index 000000000..8132e7b78 --- /dev/null +++ b/src/home/create_bot_modal.rs @@ -0,0 +1,315 @@ +//! A modal dialog for creating a Matrix child bot through BotFather slash commands. + +use makepad_widgets::*; + +use crate::utils::RoomNameId; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + + mod.widgets.CreateBotModal = #(CreateBotModal::register_widget(vm)) { + width: Fit + height: Fit + + card := RoundedView { + width: 448 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 28, right: 24, bottom: 20, left: 24} + spacing: 16 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT {font_size: 13} + color: #000 + } + text: "Create Bot" + } + + body := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "" + } + + form := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 12 + padding: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + } + + username_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "Username" + } + + username_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + empty_text: "weather" + } + + username_hint := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 9.5} + color: #666 + } + text: "Lowercase letters, digits, and underscores only. BotFather will create @bot_:server." + } + + display_name_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "Display Name" + } + + display_name_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + empty_text: "Weather Bot" + } + + prompt_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "System Prompt (Optional)" + } + + prompt_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + empty_text: "You are a weather assistant." + } + } + + status_label := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #000 + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 16 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + create_button := RobrixPositiveIconButton { + width: 130 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Create Bot" + } + } + } + } +} + +fn is_valid_bot_username(username: &str) -> bool { + !username.is_empty() + && username + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CreateBotRequest { + pub username: String, + pub display_name: String, + pub system_prompt: Option, +} + +#[derive(Clone, Debug)] +pub enum CreateBotModalAction { + Close, + Submit(CreateBotRequest), +} + +#[derive(Script, ScriptHook, Widget)] +pub struct CreateBotModal { + #[deref] + view: View, + #[rust] + room_name_id: Option, + #[rust] + is_showing_error: bool, +} + +impl Widget for CreateBotModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + if let Event::Actions(actions) = event { + self.handle_actions(cx, actions); + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl CreateBotModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { + let cancel_button = self.view.button(cx, ids!(card.buttons.cancel_button)); + let create_button = self.view.button(cx, ids!(card.buttons.create_button)); + let username_input = self.view.text_input(cx, ids!(card.form.username_input)); + let display_name_input = self.view.text_input(cx, ids!(card.form.display_name_input)); + let prompt_input = self.view.text_input(cx, ids!(card.form.prompt_input)); + let mut status_label = self.view.label(cx, ids!(card.status_label)); + + let dismissed = actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))); + if cancel_button.clicked(actions) || dismissed { + // If the modal was dismissed by clicking outside of it, do not re-emit + // our own Close action or we will feed the outer Modal another close request. + if !dismissed { + cx.action(CreateBotModalAction::Close); + } + return; + } + + if self.is_showing_error + && (username_input.changed(actions).is_some() + || display_name_input.changed(actions).is_some() + || prompt_input.changed(actions).is_some()) + { + self.is_showing_error = false; + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if create_button.clicked(actions) || prompt_input.returned(actions).is_some() { + let username = username_input.text().trim().to_string(); + if !is_valid_bot_username(&username) { + self.is_showing_error = true; + script_apply_eval!(cx, status_label, { + text: "Username must use lowercase letters, digits, or underscores." + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + } + + let display_name = display_name_input.text().trim().to_string(); + let system_prompt = prompt_input.text().trim().to_string(); + + cx.action(CreateBotModalAction::Submit(CreateBotRequest { + username: username.clone(), + display_name: if display_name.is_empty() { + username + } else { + display_name + }, + system_prompt: (!system_prompt.is_empty()).then_some(system_prompt), + })); + } + } + + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { + self.room_name_id = Some(room_name_id.clone()); + self.is_showing_error = false; + + self.view.label(cx, ids!(card.title)).set_text(cx, "Create Room Bot"); + self.view.label(cx, ids!(card.body)).set_text( + cx, + &format!( + "Robrix will send `/createbot` to BotFather in {}. The bot becomes available immediately after octos creates it.", + room_name_id + ), + ); + self.view + .text_input(cx, ids!(card.form.username_input)) + .set_text(cx, ""); + self.view + .text_input(cx, ids!(card.form.display_name_input)) + .set_text(cx, ""); + self.view + .text_input(cx, ids!(card.form.prompt_input)) + .set_text(cx, ""); + self.view.label(cx, ids!(card.status_label)).set_text(cx, ""); + self.view + .button(cx, ids!(card.buttons.create_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(card.buttons.cancel_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(card.buttons.create_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(card.buttons.cancel_button)) + .reset_hover(cx); + self.view.redraw(cx); + } +} + +impl CreateBotModalRef { + pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.show(cx, room_name_id); + } +} diff --git a/src/home/delete_bot_modal.rs b/src/home/delete_bot_modal.rs new file mode 100644 index 000000000..2b2b8ad04 --- /dev/null +++ b/src/home/delete_bot_modal.rs @@ -0,0 +1,249 @@ +//! A modal dialog for deleting a Matrix bot through BotFather slash commands. + +use makepad_widgets::*; + +use crate::utils::RoomNameId; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + + mod.widgets.DeleteBotModal = #(DeleteBotModal::register_widget(vm)) { + width: Fit + height: Fit + + card := RoundedView { + width: 448 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 28, right: 24, bottom: 20, left: 24} + spacing: 16 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT {font_size: 13} + color: #000 + } + text: "Delete Bot" + } + + body := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "" + } + + form := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 12 + padding: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + } + + user_id_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #333 + } + text: "Bot Matrix User ID" + } + + user_id_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + empty_text: "@bot_weather:server or bot_weather" + } + + user_id_hint := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 9.5} + color: #666 + } + text: "Use the full Matrix user ID when possible. A plain localpart like `bot_weather` will be resolved on your current homeserver." + } + } + + status_label := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: REGULAR_TEXT {font_size: 10.5} + color: #000 + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 16 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + delete_button := RobrixNegativeIconButton { + width: 130 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Delete Bot" + } + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeleteBotRequest { + pub user_id_or_localpart: String, +} + +#[derive(Clone, Debug)] +pub enum DeleteBotModalAction { + Close, + Submit(DeleteBotRequest), +} + +#[derive(Script, ScriptHook, Widget)] +pub struct DeleteBotModal { + #[deref] + view: View, + #[rust] + room_name_id: Option, + #[rust] + is_showing_error: bool, +} + +impl Widget for DeleteBotModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + if let Event::Actions(actions) = event { + self.handle_actions(cx, actions); + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl DeleteBotModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { + let cancel_button = self.view.button(cx, ids!(card.buttons.cancel_button)); + let delete_button = self.view.button(cx, ids!(card.buttons.delete_button)); + let user_id_input = self.view.text_input(cx, ids!(card.form.user_id_input)); + let mut status_label = self.view.label(cx, ids!(card.status_label)); + + let dismissed = actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))); + if cancel_button.clicked(actions) || dismissed { + // If the modal was dismissed by clicking outside of it, do not re-emit + // our own Close action or we will feed the outer Modal another close request. + if !dismissed { + cx.action(DeleteBotModalAction::Close); + } + return; + } + + if self.is_showing_error && user_id_input.changed(actions).is_some() { + self.is_showing_error = false; + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if delete_button.clicked(actions) || user_id_input.returned(actions).is_some() { + let user_id_or_localpart = user_id_input.text().trim().to_string(); + if user_id_or_localpart.is_empty() { + self.is_showing_error = true; + script_apply_eval!(cx, status_label, { + text: "Enter the bot Matrix user ID or localpart to delete." + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + } + + cx.action(DeleteBotModalAction::Submit(DeleteBotRequest { user_id_or_localpart })); + } + } + + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { + self.room_name_id = Some(room_name_id.clone()); + self.is_showing_error = false; + + self.view.label(cx, ids!(card.title)).set_text(cx, "Delete Room Bot"); + self.view.label(cx, ids!(card.body)).set_text( + cx, + &format!( + "Robrix will send `/deletebot` to BotFather in {}. This only removes bots already managed by octos.", + room_name_id + ), + ); + self.view + .text_input(cx, ids!(card.form.user_id_input)) + .set_text(cx, ""); + self.view.label(cx, ids!(card.status_label)).set_text(cx, ""); + self.view + .button(cx, ids!(card.buttons.delete_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(card.buttons.cancel_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(card.buttons.delete_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(card.buttons.cancel_button)) + .reset_hover(cx); + self.view.redraw(cx); + } +} + +impl DeleteBotModalRef { + pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.show(cx, room_name_id); + } +} diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 123925f50..2fe10a9be 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -473,7 +473,7 @@ impl Widget for HomeScreen { { settings_page .settings_screen(cx, ids!(settings_screen)) - .populate(cx, None); + .populate(cx, None, &app_state.bot_settings); self.view.redraw(cx); } else { error!("BUG: failed to set active page to show settings screen."); diff --git a/src/home/mod.rs b/src/home/mod.rs index 23a1de96d..482564feb 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -1,6 +1,9 @@ use makepad_widgets::ScriptVm; pub mod add_room; +pub mod app_service_panel; +pub mod create_bot_modal; +pub mod delete_bot_modal; pub mod edited_indicator; pub mod editing_pane; pub mod event_source_modal; @@ -35,6 +38,9 @@ pub fn script_mod(vm: &mut ScriptVm) { loading_pane::script_mod(vm); location_preview::script_mod(vm); add_room::script_mod(vm); + app_service_panel::script_mod(vm); + create_bot_modal::script_mod(vm); + delete_bot_modal::script_mod(vm); space_lobby::script_mod(vm); link_preview::script_mod(vm); event_reaction_list::script_mod(vm); diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 4020ca502..dce0f47b3 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -4,9 +4,10 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; use crate::{ + app::AppState, home::invite_modal::InviteModalAction, shared::popup_list::{PopupKind, enqueue_popup_notification}, - sliding_sync::{MatrixRequest, submit_async_request}, + sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, }; @@ -104,6 +105,11 @@ script_mod! { text: "Invite" } + bot_binding_button := mod.widgets.RoomContextMenuButton { + draw_icon +: { svg: (ICON_HIERARCHY) } + text: "Bind BotFather" + } + divider2 := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -128,6 +134,8 @@ pub struct RoomContextMenuDetails { pub is_favorite: bool, pub is_low_priority: bool, pub is_marked_unread: bool, + pub app_service_enabled: bool, + pub is_bot_bound: bool, } /// Actions emitted from the RoomContextMenu widget, as they must be handled @@ -190,7 +198,7 @@ impl Widget for RoomContextMenu { } impl WidgetMatchEvent for RoomContextMenu { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { let Some(details) = self.details.as_ref() else { return; }; @@ -241,6 +249,41 @@ impl WidgetMatchEvent for RoomContextMenu { } else if self.button(cx, ids!(invite_button)).clicked(actions) { cx.action(InviteModalAction::Open(details.room_name_id.clone())); close_menu = true; + } else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { + if let Some(app_state) = scope.data.get::() { + let room_id = details.room_name_id.room_id().clone(); + match app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + { + Ok(bot_user_id) => { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id, + bound: !details.is_bot_bound, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + if details.is_bot_bound { + format!("Removing BotFather {bot_user_id} from this room...") + } else { + format!("Inviting BotFather {bot_user_id} into this room...") + }, + PopupKind::Info, + Some(5.0), + ); + } + Err(error) => { + enqueue_popup_notification(error, PopupKind::Error, Some(5.0)); + } + } + } else { + enqueue_popup_notification( + "Bot settings are unavailable right now.", + PopupKind::Error, + Some(5.0), + ); + } + close_menu = true; } else if self.button(cx, ids!(leave_button)).clicked(actions) { use crate::join_leave_room_modal::{JoinLeaveRoomModalAction, JoinLeaveModalKind}; use crate::room::BasicRoomDetails; @@ -293,6 +336,14 @@ impl RoomContextMenu { priority_button.set_text(cx, "Set Low Priority"); } + let bot_binding_button = self.button(cx, ids!(bot_binding_button)); + bot_binding_button.set_visible(cx, details.app_service_enabled); + if details.is_bot_bound { + bot_binding_button.set_text(cx, "Unbind BotFather"); + } else { + bot_binding_button.set_text(cx, "Bind BotFather"); + } + // Reset hover states mark_unread_button.reset_hover(cx); favorite_button.reset_hover(cx); @@ -301,13 +352,14 @@ impl RoomContextMenu { self.button(cx, ids!(room_settings_button)).reset_hover(cx); self.button(cx, ids!(notifications_button)).reset_hover(cx); self.button(cx, ids!(invite_button)).reset_hover(cx); + bot_binding_button.reset_hover(cx); self.button(cx, ids!(leave_button)).reset_hover(cx); self.redraw(cx); - // Calculate height (rudimentary) - sum of visible buttons + padding - // 8 buttons * 35.0 + 2 dividers * ~10.0 + padding - (8.0 * BUTTON_HEIGHT) + 20.0 + 10.0 // approx + // Calculate height (rudimentary) - sum of visible buttons + padding. + let button_count = if details.app_service_enabled { 9.0 } else { 8.0 }; + (button_count * BUTTON_HEIGHT) + 20.0 + 10.0 } fn close(&mut self, cx: &mut Cx) { diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index b4be33658..e70a6eca7 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -27,6 +27,7 @@ use matrix_sdk::{ FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent, + RoomMessageEventContent, }, }, sticker::{StickerEventContent, StickerMediaSource}, @@ -49,7 +50,7 @@ use ruma::{ }; use crate::{ - app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{ plaintext_body_of_timeline_item, text_preview_of_encrypted_message, @@ -58,6 +59,9 @@ use crate::{ text_preview_of_timeline_item, }, home::{ + app_service_panel::AppServicePanelAction, + create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, + delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, @@ -94,8 +98,8 @@ use crate::{ }, sliding_sync::{ BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, - TimelineKind, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, - take_timeline_endpoints, + TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, + submit_async_request, take_timeline_endpoints, }, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime}, }; @@ -130,6 +134,60 @@ const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); /// #FFEACC const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); +fn escape_slash_command_arg(value: &str) -> String { + value.trim().replace('\\', "\\\\").replace('"', "\\\"") +} + +fn format_create_bot_command( + username: &str, + display_name: &str, + system_prompt: Option<&str>, +) -> String { + let mut command = format!("/createbot {} {}", username.trim(), display_name.trim()); + if let Some(system_prompt) = system_prompt.map(str::trim).filter(|value| !value.is_empty()) { + command.push_str(" --prompt \""); + command.push_str(&escape_slash_command_arg(system_prompt)); + command.push('"'); + } + command +} + +fn format_delete_bot_command(matrix_user_id: &UserId) -> String { + format!("/deletebot {matrix_user_id}") +} + +fn resolve_delete_bot_user_id( + user_id_or_localpart: &str, + current_user_id: Option<&UserId>, +) -> Result { + let raw = user_id_or_localpart.trim(); + if raw.is_empty() { + return Err("Please enter the bot Matrix user ID to delete.".into()); + } + + if raw.starts_with('@') || raw.contains(':') { + let full_user_id = if raw.starts_with('@') { + raw.to_string() + } else { + format!("@{raw}") + }; + return UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")); + } + + let Some(current_user_id) = current_user_id else { + return Err( + "Current user ID is unavailable, so the bot homeserver cannot be resolved.".into(), + ); + }; + + let full_user_id = format!("@{raw}:{}", current_user_id.server_name()); + UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")) +} + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -630,6 +688,9 @@ script_mod! { // Below that, display a typing notice when other users in the room are typing. typing_notice := TypingNotice { } + // Show app service tools inline with the message area instead of as a floating overlay. + app_service_panel := AppServicePanel {} + room_input_bar := RoomInputBar { // margin: Inset{top: 20} } @@ -649,6 +710,18 @@ script_mod! { // to finish loading, e.g., when loading an older replied-to message. loading_pane := LoadingPane { } + create_bot_modal := Modal { + content +: { + create_bot_modal_inner := CreateBotModal {} + } + } + + delete_bot_modal := Modal { + content +: { + delete_bot_modal_inner := DeleteBotModal {} + } + } + /* * TODO: add the action bar back in as a series of floating buttons. @@ -696,6 +769,9 @@ pub struct RoomScreen { /// Whether or not all rooms have been loaded (received from the homeserver). #[rust] all_rooms_loaded: bool, + /// Whether the in-room app service actions panel is currently visible. + #[rust] + show_app_service_actions: bool, } impl Drop for RoomScreen { @@ -916,6 +992,212 @@ impl Widget for RoomScreen { } } + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast_ref::() + { + MessageAction::ToggleAppServiceActions => { + self.toggle_app_service_actions(cx); + continue; + } + _ => {} + } + + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast_ref::() + { + AppServicePanelAction::Dismiss => { + self.set_app_service_actions_visible(cx, false); + continue; + } + AppServicePanelAction::OpenCreateBotModal => { + if let Some(app_state) = scope.data.get::() { + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before creating bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else if let Some(room_id) = self.room_id() { + if !app_state.bot_settings.is_room_bound(room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before creating a bot.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else { + self.open_create_bot_modal(cx); + } + } + } else { + enqueue_popup_notification( + "App state is unavailable, so bot creation is temporarily unavailable.", + PopupKind::Error, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } + continue; + } + AppServicePanelAction::OpenDeleteBotModal => { + if let Some(app_state) = scope.data.get::() { + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before deleting bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else if let Some(room_id) = self.room_id() { + if !app_state.bot_settings.is_room_bound(room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before deleting a bot.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else { + self.open_delete_bot_modal(cx); + } + } + } else { + enqueue_popup_notification( + "App state is unavailable, so bot deletion is temporarily unavailable.", + PopupKind::Error, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } + continue; + } + AppServicePanelAction::SendListBots => { + if let Some(app_state) = scope.data.get::() { + self.send_botfather_command( + cx, + app_state, + "/listbots", + "Sent `/listbots` to BotFather.", + ); + } + continue; + } + AppServicePanelAction::SendBotHelp => { + if let Some(app_state) = scope.data.get::() { + self.send_botfather_command( + cx, + app_state, + "/bothelp", + "Sent `/bothelp` to BotFather.", + ); + } + continue; + } + AppServicePanelAction::Unbind => { + if let Some(app_state) = scope.data.get::() { + if let Some(room_id) = self.room_id().cloned() { + if !app_state.bot_settings.is_room_bound(&room_id) { + enqueue_popup_notification( + "This room is not currently bound to BotFather.", + PopupKind::Warning, + Some(4.0), + ); + } else { + match app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + { + Ok(bot_user_id) => { + submit_async_request( + MatrixRequest::SetRoomBotBinding { + room_id, + bound: false, + bot_user_id: bot_user_id.clone(), + }, + ); + enqueue_popup_notification( + format!( + "Removing BotFather {bot_user_id} from this room..." + ), + PopupKind::Info, + Some(4.0), + ); + } + Err(error) => { + enqueue_popup_notification( + error, + PopupKind::Error, + Some(4.0), + ); + } + } + } + } + } else { + enqueue_popup_notification( + "App state is unavailable, so BotFather could not be removed from this room.", + PopupKind::Error, + Some(4.0), + ); + } + self.set_app_service_actions_visible(cx, false); + continue; + } + AppServicePanelAction::None => {} + } + + match action.downcast_ref::() { + Some(CreateBotModalAction::Close) => { + self.close_create_bot_modal(cx); + continue; + } + Some(CreateBotModalAction::Submit(request)) => { + let Some(app_state) = scope.data.get::() else { + enqueue_popup_notification( + "App state is unavailable, so the create-bot command was not sent.", + PopupKind::Error, + Some(4.0), + ); + self.close_create_bot_modal(cx); + continue; + }; + self.send_create_bot_command( + cx, + app_state, + &request.username, + &request.display_name, + request.system_prompt.as_deref(), + ); + continue; + } + None => {} + } + + match action.downcast_ref::() { + Some(DeleteBotModalAction::Close) => { + self.close_delete_bot_modal(cx); + continue; + } + Some(DeleteBotModalAction::Submit(request)) => { + let Some(app_state) = scope.data.get::() else { + enqueue_popup_notification( + "App state is unavailable, so the delete-bot command was not sent.", + PopupKind::Error, + Some(4.0), + ); + self.close_delete_bot_modal(cx); + continue; + }; + self.send_delete_bot_command(cx, app_state, &request.user_id_or_localpart); + continue; + } + None => {} + } + // Handle the highlight animation for a message. let Some(tl) = self.tl_state.as_mut() else { continue; @@ -1040,6 +1322,16 @@ impl Widget for RoomScreen { ) }) .unwrap_or((RoomDisplayName::Empty, None)); + let (app_service_enabled, app_service_room_bound) = scope + .data + .get::() + .map(|app_state| { + ( + app_state.bot_settings.enabled, + app_state.bot_settings.is_room_bound(&room_id), + ) + }) + .unwrap_or((false, false)); RoomScreenProps { room_screen_widget_uid, @@ -1047,9 +1339,22 @@ impl Widget for RoomScreen { timeline_kind: tl.kind.clone(), room_members, room_avatar_url, + app_service_enabled, + app_service_room_bound, } } else if let Some(room_name) = &self.room_name_id { // Fallback case: we have a room_name but no tl_state yet + let room_id = room_name.room_id().clone(); + let (app_service_enabled, app_service_room_bound) = scope + .data + .get::() + .map(|app_state| { + ( + app_state.bot_settings.enabled, + app_state.bot_settings.is_room_bound(&room_id), + ) + }) + .unwrap_or((false, false)); RoomScreenProps { room_screen_widget_uid, room_name_id: room_name.clone(), @@ -1059,6 +1364,8 @@ impl Widget for RoomScreen { .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, room_avatar_url: None, + app_service_enabled, + app_service_room_bound, } } else { // No room selected yet, skip event handling that requires room context @@ -1076,6 +1383,8 @@ impl Widget for RoomScreen { timeline_kind: TimelineKind::MainRoom { room_id }, room_members: None, room_avatar_url: None, + app_service_enabled: false, + app_service_room_bound: false, } }; let mut room_scope = Scope::with_props(&room_props); @@ -2359,6 +2668,8 @@ impl RoomScreen { MessageAction::HighlightMessage(..) => {} // This is handled by the top-level App itself. MessageAction::OpenMessageContextMenu { .. } => {} + // This is handled in RoomScreen::handle_event because it needs room-level state. + MessageAction::ToggleAppServiceActions => {} // This isn't yet handled, as we need to completely redesign it. MessageAction::ActionBarOpen { .. } => {} // This isn't yet handled, as we need to completely redesign it. @@ -2368,6 +2679,207 @@ impl RoomScreen { } } + fn set_app_service_actions_visible(&mut self, cx: &mut Cx, visible: bool) { + let was_visible = self.show_app_service_actions; + self.show_app_service_actions = visible; + self.view + .child_by_path(ids!(room_screen_wrapper.keyboard_view.app_service_panel)) + .set_visible(cx, visible); + if visible && !was_visible { + self.anchor_timeline_to_bottom(cx); + } + self.redraw(cx); + } + + fn toggle_app_service_actions(&mut self, cx: &mut Cx) { + self.set_app_service_actions_visible(cx, !self.show_app_service_actions); + } + + fn anchor_timeline_to_bottom(&self, cx: &mut Cx) { + let portal_list = self.portal_list(cx, ids!(timeline.list)); + portal_list.set_tail_range(true); + portal_list.scroll_to_end(cx); + self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)) + .update_visibility(cx, true); + } + + fn close_create_bot_modal(&self, cx: &mut Cx) { + let modal = self.view.modal(cx, ids!(create_bot_modal)); + if modal.is_open() { + modal.close(cx); + } + } + + fn close_delete_bot_modal(&self, cx: &mut Cx) { + let modal = self.view.modal(cx, ids!(delete_bot_modal)); + if modal.is_open() { + modal.close(cx); + } + } + + fn open_create_bot_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.clone() else { + return; + }; + self.set_app_service_actions_visible(cx, false); + self.view + .create_bot_modal(cx, ids!(create_bot_modal_inner)) + .show(cx, room_name_id); + self.view.modal(cx, ids!(create_bot_modal)).open(cx); + } + + fn open_delete_bot_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.clone() else { + return; + }; + self.set_app_service_actions_visible(cx, false); + self.view + .delete_bot_modal(cx, ids!(delete_bot_modal_inner)) + .show(cx, room_name_id); + self.view.modal(cx, ids!(delete_bot_modal)).open(cx); + } + + fn reset_app_service_ui(&mut self, cx: &mut Cx) { + self.set_app_service_actions_visible(cx, false); + self.close_create_bot_modal(cx); + self.close_delete_bot_modal(cx); + } + + fn send_botfather_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + command: &str, + success_message: &str, + ) { + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; + if timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let Some(room_id) = self.room_id().cloned() else { + return; + }; + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before using BotFather commands in a room.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + if !app_state.bot_settings.is_room_bound(&room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before using BotFather commands.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + submit_async_request(MatrixRequest::SendMessage { + timeline_kind, + message: RoomMessageEventContent::text_plain(command), + replied_to: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + + enqueue_popup_notification(success_message.to_string(), PopupKind::Info, Some(4.0)); + self.set_app_service_actions_visible(cx, false); + } + + fn send_create_bot_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + username: &str, + display_name: &str, + system_prompt: Option<&str>, + ) { + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; + if timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot creation commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let Some(room_id) = self.room_id().cloned() else { + return; + }; + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before creating bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + if !app_state.bot_settings.is_room_bound(&room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before creating a bot.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let command = format_create_bot_command(username, display_name, system_prompt); + submit_async_request(MatrixRequest::SendMessage { + timeline_kind, + message: RoomMessageEventContent::text_plain(command), + replied_to: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + + enqueue_popup_notification( + format!("Sent `/createbot` for `{username}` to BotFather."), + PopupKind::Info, + Some(4.0), + ); + self.close_create_bot_modal(cx); + } + + fn send_delete_bot_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + user_id_or_localpart: &str, + ) { + let matrix_user_id = match resolve_delete_bot_user_id( + user_id_or_localpart, + current_user_id().as_deref(), + ) { + Ok(user_id) => user_id, + Err(error) => { + enqueue_popup_notification(error, PopupKind::Error, Some(4.0)); + return; + } + }; + + let command = format_delete_bot_command(matrix_user_id.as_ref()); + self.send_botfather_command( + cx, + app_state, + &command, + &format!("Sent `/deletebot` for {matrix_user_id} to BotFather."), + ); + self.close_delete_bot_modal(cx); + } + /// Jumps to the target event ID in this timeline by smooth scrolling to it. /// /// This function searches backwards from the given `max_tl_idx` in the timeline @@ -2774,6 +3286,7 @@ impl RoomScreen { return; } + self.reset_app_service_ui(cx); self.hide_timeline(); // Reset the the state of the inner loading pane. self.loading_pane(cx, ids!(loading_pane)).take_state(); @@ -2929,6 +3442,8 @@ pub struct RoomScreenProps { pub timeline_kind: TimelineKind, pub room_members: Option>>, pub room_avatar_url: Option, + pub app_service_enabled: bool, + pub app_service_room_bound: bool, } /// Actions for the room screen's tooltip. @@ -4967,6 +5482,8 @@ pub enum MessageAction { OpenThread(OwnedEventId), /// The user requested to jump to a specific event in this room. JumpToEvent(OwnedEventId), + /// The user requested toggling the in-room app service actions panel. + ToggleAppServiceActions, /// The user clicked the "delete" button on a message. #[doc(alias("delete"))] Redact { diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 0d08156fd..0d5a1520a 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -1360,6 +1360,13 @@ impl Widget for RoomsList { is_favorite: jr.tags.contains_key(&TagName::Favorite), is_low_priority: jr.tags.contains_key(&TagName::LowPriority), is_marked_unread: jr.is_marked_unread, + app_service_enabled: scope + .data + .get::() + .is_some_and(|app_state| app_state.bot_settings.enabled), + is_bot_bound: scope.data.get::().is_some_and(|app_state| { + app_state.bot_settings.is_room_bound(jr.room_name_id.room_id()) + }), }; cx.widget_action( self.widget_uid(), diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 614017021..d581085f5 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -345,6 +345,18 @@ impl RoomInputBar { { let entered_text = mentionable_text_input.text().trim().to_string(); if !entered_text.is_empty() { + if self.try_handle_bot_shortcut(cx, &entered_text, room_screen_props) { + self.clear_replying_to(cx); + mentionable_text_input.set_text(cx, ""); + submit_async_request(MatrixRequest::SendTypingNotice { + room_id: room_screen_props.timeline_kind.room_id().clone(), + typing: false, + }); + self.enable_send_message_button(cx, false); + self.redraw(cx); + return; + } + let message = mentionable_text_input.create_message_with_mentions(&entered_text); let replied_to = self .replying_to @@ -434,6 +446,58 @@ impl RoomInputBar { } } + /// Intercepts `/bot` commands and opens the room-level app service actions UI instead + /// of sending the raw command text into the room. + fn try_handle_bot_shortcut( + &mut self, + cx: &mut Cx, + entered_text: &str, + room_screen_props: &RoomScreenProps, + ) -> bool { + if !(entered_text == "/bot" || entered_text.starts_with("/bot ")) { + return false; + } + + let popup_message = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + Some(( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + )) + } else if entered_text != "/bot" { + Some(( + "Only `/bot` is supported right now. Use `/bot` and choose an action from the room panel.", + PopupKind::Info, + )) + } else if !room_screen_props.app_service_enabled { + Some(( + "Enable App Service in Settings before using /bot.", + PopupKind::Warning, + )) + } else if !room_screen_props.app_service_room_bound { + Some(( + "Bind BotFather to this room before using /bot.", + PopupKind::Warning, + )) + } else { + None + }; + + if let Some((message, kind)) = popup_message { + enqueue_popup_notification(message, kind, Some(4.0)); + } else { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::ToggleAppServiceActions, + ); + } + + true + } + /// Shows a preview of the given event that the user is currently replying to /// above the message input bar. /// diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs new file mode 100644 index 000000000..a9aa5c5a8 --- /dev/null +++ b/src/settings/bot_settings.rs @@ -0,0 +1,214 @@ +use makepad_widgets::*; + +use crate::{ + app::{AppState, BotSettingsState}, + shared::popup_list::{PopupKind, enqueue_popup_notification}, +}; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + + mod.widgets.BotSettings = #(BotSettings::register_widget(vm)) { + width: Fill, height: Fit + flow: Down + spacing: 10 + + TitleLabel { + text: "App Service" + } + + description := Label { + width: Fill, + height: Fit + margin: Inset{left: 5, right: 8, bottom: 4} + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT {font_size: 10.5} + } + text: "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands." + } + + enable_row := View { + width: Fill, + height: Fit + flow: Right + align: Align{y: 0.5} + spacing: 12 + margin: Inset{left: 5, bottom: 2} + + enable_label := SubsectionLabel { + width: Fit, height: Fit + margin: 0 + text: "Enable App Service" + } + + enable_button := RobrixNeutralIconButton { + width: Fit, + height: Fit + padding: Inset{top: 9, bottom: 9, left: 12, right: 14} + spacing: 0 + text: "Disabled" + } + } + + bot_details := View { + visible: false + width: Fill, height: Fit + flow: Down + spacing: 8 + + SubsectionLabel { + text: "BotFather User ID:" + } + + bot_user_id_input := RobrixTextInput { + margin: Inset{top: 2, left: 5, right: 5, bottom: 2} + width: 280, height: Fit + empty_text: "bot or @bot:server" + } + + details_hint := Label { + width: Fill, + height: Fit + margin: Inset{left: 5, right: 8} + flow: Flow.Right{wrap: true} + draw_text +: { + color: #666 + text_style: REGULAR_TEXT {font_size: 9.7} + } + text: "Use either a localpart like `bot` or a full Matrix user ID. Bind or unbind BotFather from a room via the room menu or `/bot`." + } + + save_button := RobrixPositiveIconButton { + width: Fit, + height: Fit + margin: Inset{left: 5} + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16} + text: "Save App Service Settings" + } + } + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct BotSettings { + #[deref] + view: View, +} + +impl Widget for BotSettings { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + if let Event::Actions(actions) = event { + self.handle_actions(cx, actions, scope); + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + +impl BotSettings { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + let Some(app_state) = scope.data.get_mut::() else { + return; + }; + + if self.view.button(cx, ids!(enable_row.enable_button)).clicked(actions) { + app_state.bot_settings.enabled = !app_state.bot_settings.enabled; + self.sync_ui(cx, &app_state.bot_settings); + return; + } + + if self.view.button(cx, ids!(bot_details.save_button)).clicked(actions) { + let bot_user_id = self + .view + .text_input(cx, ids!(bot_details.bot_user_id_input)) + .text() + .trim() + .to_string(); + app_state.bot_settings.botfather_user_id = if bot_user_id.is_empty() { + BotSettingsState::DEFAULT_BOTFATHER_LOCALPART.to_string() + } else { + bot_user_id + }; + self.sync_ui(cx, &app_state.bot_settings); + enqueue_popup_notification( + "Saved Matrix app service settings.", + PopupKind::Success, + Some(3.0), + ); + } + } + + fn sync_enable_button(&mut self, cx: &mut Cx, enabled: bool) { + let mut enable_button = self.view.button(cx, ids!(enable_row.enable_button)); + enable_button.set_text(cx, if enabled { "Enabled" } else { "Disabled" }); + if enabled { + script_apply_eval!(cx, enable_button, { + draw_bg +: { + color: mod.widgets.COLOR_ACTIVE_PRIMARY + color_hover: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER + color_down: #x0C5DAA + border_color: mod.widgets.COLOR_ACTIVE_PRIMARY + border_color_hover: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER + border_color_down: #x0C5DAA + } + draw_text +: { + color: mod.widgets.COLOR_PRIMARY + color_hover: mod.widgets.COLOR_PRIMARY + color_down: mod.widgets.COLOR_PRIMARY + } + }); + } else { + script_apply_eval!(cx, enable_button, { + draw_bg +: { + border_color: mod.widgets.COLOR_BG_DISABLED + border_color_hover: mod.widgets.COLOR_BG_DISABLED + border_color_down: mod.widgets.COLOR_BG_DISABLED + color: mod.widgets.COLOR_SECONDARY + color_hover: #D0D0D0 + color_down: #C0C0C0 + } + draw_text +: { + color: mod.widgets.COLOR_TEXT + color_hover: mod.widgets.COLOR_TEXT + color_down: mod.widgets.COLOR_TEXT + } + }); + } + } + + fn sync_ui(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { + self.sync_enable_button(cx, bot_settings.enabled); + self.view + .view(cx, ids!(bot_details)) + .set_visible(cx, bot_settings.enabled); + self.view + .text_input(cx, ids!(bot_details.bot_user_id_input)) + .set_text(cx, &bot_settings.botfather_user_id); + self.view + .button(cx, ids!(bot_details.save_button)) + .reset_hover(cx); + self.redraw(cx); + } + + pub fn populate(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { + self.sync_ui(cx, bot_settings); + } +} + +impl BotSettingsRef { + pub fn populate(&self, cx: &mut Cx, bot_settings: &BotSettingsState) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.populate(cx, bot_settings); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 579bf0849..3155e1186 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -2,8 +2,10 @@ use makepad_widgets::ScriptVm; pub mod settings_screen; pub mod account_settings; +pub mod bot_settings; pub fn script_mod(vm: &mut ScriptVm) { account_settings::script_mod(vm); + bot_settings::script_mod(vm); settings_screen::script_mod(vm); } diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 201ae14cc..28915895d 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,9 +1,13 @@ use makepad_widgets::*; use crate::{ + app::BotSettingsState, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, profile::user_profile::UserProfile, - settings::account_settings::AccountSettingsWidgetExt, + settings::{ + account_settings::AccountSettingsWidgetExt, + bot_settings::BotSettingsWidgetExt, + }, }; script_mod! { @@ -61,6 +65,10 @@ script_mod! { LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + bot_settings := BotSettings {} + + LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + // The TSP wallet settings section. tsp_settings_screen := TspSettingsScreen {} @@ -170,7 +178,12 @@ impl Widget for SettingsScreen { impl SettingsScreen { /// Fetches the current user's profile and uses it to populate the settings screen. - pub fn populate(&mut self, cx: &mut Cx, own_profile: Option) { + pub fn populate( + &mut self, + cx: &mut Cx, + own_profile: Option, + bot_settings: &BotSettingsState, + ) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { error!("Failed to get own profile for settings screen."); return; @@ -178,6 +191,9 @@ impl SettingsScreen { self.view .account_settings(cx, ids!(account_settings)) .populate(cx, profile); + self.view + .bot_settings(cx, ids!(bot_settings)) + .populate(cx, bot_settings); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); @@ -186,10 +202,15 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. - pub fn populate(&self, cx: &mut Cx, own_profile: Option) { + pub fn populate( + &self, + cx: &mut Cx, + own_profile: Option, + bot_settings: &BotSettingsState, + ) { let Some(mut inner) = self.borrow_mut() else { return; }; - inner.populate(cx, own_profile); + inner.populate(cx, own_profile, bot_settings); } } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 99f799ae0..200c0339b 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -771,6 +771,12 @@ pub enum MatrixRequest { /// * If `false` (recommended), details will be fetched from the server. local_only: bool, }, + /// Request to bind or unbind the configured BotFather for the given room. + SetRoomBotBinding { + room_id: OwnedRoomId, + bound: bool, + bot_user_id: OwnedUserId, + }, /// 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. @@ -1551,6 +1557,72 @@ async fn matrix_worker_task( }); } + MatrixRequest::SetRoomBotBinding { + room_id, + bound, + bot_user_id, + } => { + let Some(client) = get_client() else { continue }; + let _bot_binding_task = Handle::current().spawn(async move { + let Some(room) = client.get_room(&room_id) else { + let error_message = + format!("Room {room_id} was not found for the bot binding request."); + error!("{error_message}"); + enqueue_popup_notification(error_message, PopupKind::Error, None); + return; + }; + + let membership_result = if bound { + room.invite_user_by_id(&bot_user_id).await + } else { + room.kick_user(&bot_user_id, Some("Robrix app service unbind")) + .await + }; + + match membership_result { + Ok(()) => { + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: None, + }); + } + Err(error) => { + let membership_exists = room + .get_member_no_sync(&bot_user_id) + .await + .ok() + .flatten() + .is_some(); + let should_mark_bound = if bound { membership_exists } else { false }; + + if should_mark_bound != bound { + error!( + "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", + if bound { "invite" } else { "remove" } + ); + enqueue_popup_notification( + format!( + "Failed to {} BotFather {bot_user_id}: {error}", + if bound { "invite" } else { "remove" } + ), + PopupKind::Error, + None, + ); + return; + } + + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: Some(error.to_string()), + }); + } + } + }); + } 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}"); From fbd0c5657f61a34da40bb2c26e97b0f24eb76bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Tue, 24 Mar 2026 16:08:00 +0800 Subject: [PATCH 004/283] Fix app service cleanup issues --- src/home/room_screen.rs | 9 +++------ src/settings/bot_settings.rs | 4 ++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index e70a6eca7..5874d3368 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -992,16 +992,13 @@ impl Widget for RoomScreen { } } - match action + if let MessageAction::ToggleAppServiceActions = action .as_widget_action() .widget_uid_eq(room_screen_widget_uid) .cast_ref::() { - MessageAction::ToggleAppServiceActions => { - self.toggle_app_service_actions(cx); - continue; - } - _ => {} + self.toggle_app_service_actions(cx); + continue; } match action diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index a9aa5c5a8..4ac342126 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -155,10 +155,10 @@ impl BotSettings { draw_bg +: { color: mod.widgets.COLOR_ACTIVE_PRIMARY color_hover: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER - color_down: #x0C5DAA + color_down: #x0c5daa border_color: mod.widgets.COLOR_ACTIVE_PRIMARY border_color_hover: mod.widgets.COLOR_ACTIVE_PRIMARY_DARKER - border_color_down: #x0C5DAA + border_color_down: #x0c5daa } draw_text +: { color: mod.widgets.COLOR_PRIMARY From d12cfa7d0f656036998a6ac7cac617c8762d4bbe Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:23:26 +0800 Subject: [PATCH 005/283] feat: add streaming_animation module with core data structures --- src/home/mod.rs | 1 + src/home/streaming_animation.rs | 127 ++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 src/home/streaming_animation.rs diff --git a/src/home/mod.rs b/src/home/mod.rs index 23a1de96d..90305240d 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 streaming_animation; pub fn script_mod(vm: &mut ScriptVm) { search_messages::script_mod(vm); diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs new file mode 100644 index 000000000..4d0fdede2 --- /dev/null +++ b/src/home/streaming_animation.rs @@ -0,0 +1,127 @@ +use std::time::Instant; +use matrix_sdk::ruma::OwnedUserId; + +/// How a streaming session was detected. +#[derive(Debug, Clone, PartialEq)] +pub enum StreamDetection { + /// Confirmed by MSC4357 live flag in event content. + Msc4357Live, + /// Detected by heuristic: prefix match + recency + not self. + Heuristic, +} + +/// Animation state for a single streaming message. +pub struct StreamingAnimState { + pub target_text: String, + pub target_char_count: usize, + pub displayed_char_count: usize, + pub displayed_byte_offset: usize, + pub chars_per_frame: f64, + pub fractional_chars: f64, + pub last_update_time: Instant, + pub animation_start_time: Instant, + pub chars_at_last_update: usize, + pub display_buffer: String, + pub sender_stopped_typing: bool, + pub sender_user_id: OwnedUserId, + pub was_at_end: bool, + pub detection: StreamDetection, +} + +impl StreamingAnimState { + pub fn new(initial_text: &str, sender_user_id: OwnedUserId, detection: StreamDetection, was_at_end: bool) -> Self { + let char_count = initial_text.chars().count(); + Self { + target_text: initial_text.to_string(), + target_char_count: char_count, + displayed_char_count: 0, + displayed_byte_offset: 0, + chars_per_frame: 1.0, + fractional_chars: 0.0, + last_update_time: Instant::now(), + animation_start_time: Instant::now(), + chars_at_last_update: 0, + display_buffer: String::with_capacity(initial_text.len() + 4), + sender_stopped_typing: false, + sender_user_id, + was_at_end, + detection, + } + } + + pub fn update_target(&mut self, new_text: &str) { + self.target_text.clear(); + self.target_text.push_str(new_text); + self.target_char_count = new_text.chars().count(); + self.chars_at_last_update = self.displayed_char_count; + self.last_update_time = Instant::now(); + let remaining = self.target_char_count.saturating_sub(self.displayed_char_count); + if remaining > 0 { + self.chars_per_frame = remaining as f64 / 60.0; + if self.chars_per_frame < 0.5 { self.chars_per_frame = 0.5; } + } + if self.display_buffer.capacity() < new_text.len() + 4 { + self.display_buffer.reserve(new_text.len() + 4 - self.display_buffer.capacity()); + } + } + + pub fn advance_displayed(&mut self, chars_to_add: usize) { + if chars_to_add == 0 || self.displayed_char_count >= self.target_char_count { return; } + let remaining = &self.target_text[self.displayed_byte_offset..]; + let mut byte_advance = 0; + let mut actual_chars = 0; + for (byte_idx, _char) in remaining.char_indices() { + if actual_chars >= chars_to_add { byte_advance = byte_idx; break; } + actual_chars += 1; + } + if actual_chars <= chars_to_add && byte_advance == 0 && !remaining.is_empty() { + byte_advance = remaining.len(); + } + self.displayed_char_count = (self.displayed_char_count + actual_chars).min(self.target_char_count); + self.displayed_byte_offset = (self.displayed_byte_offset + byte_advance).min(self.target_text.len()); + } + + pub fn tick(&mut self) -> bool { + if self.displayed_char_count >= self.target_char_count { return false; } + let gap = self.target_char_count - self.displayed_char_count; + let speed = if gap > 500 { + let jump = gap - 50; + self.advance_displayed(jump); + self.chars_per_frame + } else if gap > 200 { + self.chars_per_frame * 3.0 + } else { + self.chars_per_frame + }; + self.fractional_chars += speed; + let advance = self.fractional_chars.floor() as usize; + self.fractional_chars -= advance as f64; + if advance > 0 { self.advance_displayed(advance); true } else { false } + } + + pub fn fill_display_buffer(&mut self) { + self.display_buffer.clear(); + self.display_buffer.push_str(&self.target_text[..self.displayed_byte_offset]); + self.display_buffer.push_str(" \u{25CF}"); + } + + pub fn is_complete(&self) -> bool { + if self.displayed_char_count < self.target_char_count { return false; } + match self.detection { + StreamDetection::Msc4357Live => false, + StreamDetection::Heuristic => self.sender_stopped_typing, + } + } + + pub fn is_timed_out(&self) -> bool { + self.last_update_time.elapsed().as_secs() > 30 + } + + pub fn catch_up_to_wall_clock(&mut self) { + let elapsed = self.last_update_time.elapsed(); + let elapsed_frames = elapsed.as_secs_f64() * 60.0; + let expected = self.chars_at_last_update + (elapsed_frames * self.chars_per_frame) as usize; + let target = expected.min(self.target_char_count); + if target > self.displayed_char_count { self.advance_displayed(target - self.displayed_char_count); } + } +} From 468a4022b22d1ecf32fd46d0fabe56e6457396bf Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:30:28 +0800 Subject: [PATCH 006/283] fix: address code review issues in streaming_animation - tick(): always return true when a large-gap jump changes state (changed flag) - update_target(): clamp display pointers when new text is shorter to prevent panic in fill_display_buffer - update_target(): fix String::reserve wrong-base bug (compare capacity, reserve len deficit) - new(): capture Instant::now() once and reuse for last_update_time and animation_start_time - is_complete(): add doc comment explaining why Msc4357Live always returns false --- src/home/streaming_animation.rs | 39 ++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 4d0fdede2..2bf4e1027 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -31,6 +31,7 @@ pub struct StreamingAnimState { impl StreamingAnimState { pub fn new(initial_text: &str, sender_user_id: OwnedUserId, detection: StreamDetection, was_at_end: bool) -> Self { let char_count = initial_text.chars().count(); + let now = Instant::now(); Self { target_text: initial_text.to_string(), target_char_count: char_count, @@ -38,8 +39,8 @@ impl StreamingAnimState { displayed_byte_offset: 0, chars_per_frame: 1.0, fractional_chars: 0.0, - last_update_time: Instant::now(), - animation_start_time: Instant::now(), + last_update_time: now, + animation_start_time: now, chars_at_last_update: 0, display_buffer: String::with_capacity(initial_text.len() + 4), sender_stopped_typing: false, @@ -53,6 +54,17 @@ impl StreamingAnimState { self.target_text.clear(); self.target_text.push_str(new_text); self.target_char_count = new_text.chars().count(); + + // Clamp display pointers if the new text is shorter than what was already displayed. + if self.displayed_char_count > self.target_char_count { + self.displayed_char_count = self.target_char_count; + // Re-derive byte offset to stay on char boundary. + self.displayed_byte_offset = self.target_text + .char_indices() + .nth(self.target_char_count) + .map_or(self.target_text.len(), |(i, _)| i); + } + self.chars_at_last_update = self.displayed_char_count; self.last_update_time = Instant::now(); let remaining = self.target_char_count.saturating_sub(self.displayed_char_count); @@ -60,8 +72,11 @@ impl StreamingAnimState { self.chars_per_frame = remaining as f64 / 60.0; if self.chars_per_frame < 0.5 { self.chars_per_frame = 0.5; } } - if self.display_buffer.capacity() < new_text.len() + 4 { - self.display_buffer.reserve(new_text.len() + 4 - self.display_buffer.capacity()); + // Fix: reserve uses wrong base — reserve(n) guarantees capacity >= len + n, + // not capacity >= n. Compare against capacity and reserve only the deficit. + let needed = new_text.len() + 4; + if self.display_buffer.capacity() < needed { + self.display_buffer.reserve(needed - self.display_buffer.len()); } } @@ -84,19 +99,27 @@ impl StreamingAnimState { pub fn tick(&mut self) -> bool { if self.displayed_char_count >= self.target_char_count { return false; } let gap = self.target_char_count - self.displayed_char_count; + let mut changed = false; + let speed = if gap > 500 { let jump = gap - 50; self.advance_displayed(jump); + changed = true; self.chars_per_frame } else if gap > 200 { self.chars_per_frame * 3.0 } else { self.chars_per_frame }; + self.fractional_chars += speed; let advance = self.fractional_chars.floor() as usize; self.fractional_chars -= advance as f64; - if advance > 0 { self.advance_displayed(advance); true } else { false } + if advance > 0 { + self.advance_displayed(advance); + changed = true; + } + changed } pub fn fill_display_buffer(&mut self) { @@ -105,6 +128,12 @@ impl StreamingAnimState { self.display_buffer.push_str(" \u{25CF}"); } + /// Check if streaming is complete. + /// + /// For `Heuristic` detection, completes when the sender stops typing and + /// all text has been revealed. For `Msc4357Live`, this always returns `false` — + /// completion is signaled externally when the server removes the live flag, + /// which causes the entry to be removed from `streaming_messages` directly. pub fn is_complete(&self) -> bool { if self.displayed_char_count < self.target_char_count { return false; } match self.detection { From 9d74ec1025b6500c453e6046ca11520db462525c Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:32:29 +0800 Subject: [PATCH 007/283] test: add unit tests for StreamingAnimState --- src/home/streaming_animation.rs | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 2bf4e1027..8c4c32710 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -154,3 +154,121 @@ impl StreamingAnimState { if target > self.displayed_char_count { self.advance_displayed(target - self.displayed_char_count); } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_state(text: &str) -> StreamingAnimState { + let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); + StreamingAnimState::new(text, user_id, StreamDetection::Heuristic, true) + } + + #[test] + fn test_advance_ascii() { + let mut s = make_state("Hello, world!"); + s.advance_displayed(5); + assert_eq!(s.displayed_char_count, 5); + assert_eq!(&s.target_text[..s.displayed_byte_offset], "Hello"); + } + + #[test] + fn test_advance_utf8_multibyte() { + let mut s = make_state("你好世界abcd"); + s.advance_displayed(2); + assert_eq!(s.displayed_char_count, 2); + assert_eq!(&s.target_text[..s.displayed_byte_offset], "你好"); + } + + #[test] + fn test_advance_clamps_at_end() { + let mut s = make_state("abc"); + s.advance_displayed(100); + assert_eq!(s.displayed_char_count, 3); + assert_eq!(s.displayed_byte_offset, 3); + } + + #[test] + fn test_update_target_extends() { + let mut s = make_state("Hello"); + s.advance_displayed(5); + assert_eq!(s.displayed_char_count, 5); + s.update_target("Hello, world!"); + assert_eq!(s.target_char_count, 13); + assert_eq!(s.displayed_char_count, 5); + assert!(s.chars_per_frame > 0.0); + } + + #[test] + fn test_update_target_shrinks_safely() { + let mut s = make_state("Hello, world!"); + s.advance_displayed(10); + s.update_target("Hi"); + assert_eq!(s.displayed_char_count, 2); + assert_eq!(s.displayed_byte_offset, 2); + // Should not panic + s.fill_display_buffer(); + assert!(s.display_buffer.starts_with("Hi")); + } + + #[test] + fn test_tick_advances() { + let mut s = make_state("Hello, world!"); + s.chars_per_frame = 2.0; + let changed = s.tick(); + assert!(changed); + assert_eq!(s.displayed_char_count, 2); + } + + #[test] + fn test_tick_no_change_when_complete() { + let mut s = make_state("Hi"); + s.advance_displayed(2); + let changed = s.tick(); + assert!(!changed); + } + + #[test] + fn test_tick_large_gap_returns_true() { + let mut s = make_state(&"a".repeat(1000)); + s.chars_per_frame = 0.1; // very slow, fractional won't trigger + let changed = s.tick(); + assert!(changed); // should still return true due to the jump + assert!(s.displayed_char_count > 900); + } + + #[test] + fn test_fill_display_buffer() { + let mut s = make_state("Hello"); + s.advance_displayed(3); + s.fill_display_buffer(); + assert!(s.display_buffer.starts_with("Hel")); + assert!(s.display_buffer.contains('\u{25CF}') || s.display_buffer.contains('●')); + } + + #[test] + fn test_is_complete_heuristic() { + let mut s = make_state("Hi"); + s.advance_displayed(2); + assert!(!s.is_complete()); + s.sender_stopped_typing = true; + assert!(s.is_complete()); + } + + #[test] + fn test_is_complete_msc4357_never_self_completes() { + let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); + let mut s = StreamingAnimState::new("Hi", user_id, StreamDetection::Msc4357Live, true); + s.advance_displayed(2); + s.sender_stopped_typing = true; + assert!(!s.is_complete()); // Msc4357Live never self-completes + } + + #[test] + fn test_advance_zero_is_noop() { + let mut s = make_state("Hello"); + s.advance_displayed(0); + assert_eq!(s.displayed_char_count, 0); + assert_eq!(s.displayed_byte_offset, 0); + } +} From 48cec9090def1d79e9b4648244d6ae56de6d7c90 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:33:54 +0800 Subject: [PATCH 008/283] feat: add streaming_messages HashMap to TimelineUiState --- src/home/room_screen.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced9..930bebd88 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2249,6 +2249,7 @@ impl RoomScreen { pending_thread_summary_fetches: HashSet::new(), saved_state: SavedState::default(), message_highlight_animation_state: MessageHighlightAnimationState::default(), + streaming_messages: HashMap::new(), last_scrolled_index: usize::MAX, prev_first_index: None, scrolled_past_read_marker: false, @@ -2828,6 +2829,10 @@ struct TimelineUiState { /// If the animation was triggered, the state goes back to Off. message_highlight_animation_state: MessageHighlightAnimationState, + /// Active streaming animations, keyed by event ID. + /// Stores the typewriter animation state for messages being streamed by bots. + streaming_messages: HashMap, + /// The index of the timeline item that was most recently scrolled up past it. /// This is used to detect when the user has scrolled up past the second visible item (index 1) /// upwards to the first visible item (index 0), which is the top of the timeline, From ce8109aa9b13ceebf97e263d9424f7b2444d994b Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:36:06 +0800 Subject: [PATCH 009/283] feat: add NextFrame animation handler for streaming messages --- src/home/room_screen.rs | 61 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 930bebd88..f202e4caa 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -622,6 +622,9 @@ pub struct RoomScreen { #[rust] is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). #[rust] all_rooms_loaded: bool, + /// NextFrame subscription for driving streaming typewriter animation. + #[rust] + streaming_next_frame: NextFrame, } impl Drop for RoomScreen { @@ -656,6 +659,64 @@ impl Widget for RoomScreen { let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); + // Streaming animation frame handler + if let Some(_ne) = self.streaming_next_frame.is_event(event) { + if let Some(tl) = self.tl_state.as_mut() { + let mut any_active = false; + let mut completed_ids = Vec::new(); + + // Build event_id → index lookup for cache invalidation + let streaming_indices: Vec<(OwnedEventId, usize)> = tl.streaming_messages.keys() + .filter_map(|eid| { + tl.items.iter().enumerate().find_map(|(idx, item)| { + if let TimelineItemKind::Event(evt) = item.kind() { + if evt.event_id().is_some_and(|id| id == eid) { + return Some((eid.clone(), idx)); + } + } + None + }) + }) + .collect(); + + for (event_id, state) in tl.streaming_messages.iter_mut() { + if state.tick() { + any_active = true; + // Invalidate draw cache so item gets re-populated + if let Some((_, idx)) = streaming_indices.iter().find(|(eid, _)| eid == event_id) { + tl.content_drawn_since_last_update.remove(*idx..*idx + 1); + } + } + + if state.is_complete() || state.is_timed_out() { + completed_ids.push(event_id.clone()); + } + } + + for id in &completed_ids { + tl.streaming_messages.remove(id); + } + + // Safety cap: max 50 streaming entries + while tl.streaming_messages.len() > 50 { + if let Some(oldest_id) = tl.streaming_messages.iter() + .min_by_key(|(_, s)| s.animation_start_time) + .map(|(id, _)| id.clone()) + { + tl.streaming_messages.remove(&oldest_id); + } + } + + if any_active || !tl.streaming_messages.is_empty() { + self.streaming_next_frame = cx.new_next_frame(); + } + + if any_active || !completed_ids.is_empty() { + self.redraw(cx); + } + } + } + // Handle actions here before processing timeline updates. // Normally (in most other widgets), the order of event handling doesn't matter much. // However, since actions may refer to a specific timeline item's index, From f8ce5660c9173b8392783b4eede450592205f54d Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:39:15 +0800 Subject: [PATCH 010/283] feat: add streaming detection in process_timeline_updates --- src/home/room_screen.rs | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index f202e4caa..9e1c6b59c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1296,6 +1296,17 @@ impl RoomScreen { self.room_name_id.as_ref().map(|r| r.room_id()) } + /// Extract the text body from a timeline item, if it's a text message. + fn extract_message_text_from_item(item: &Arc) -> Option { + let TimelineItemKind::Event(event) = item.kind() else { return None }; + let TimelineItemContent::MsgLike(msg_like) = event.content() else { return None }; + let MsgLikeKind::Message(msg) = &msg_like.kind else { return None }; + match msg.msgtype() { + MessageType::Text(text) => Some(text.body.clone()), + _ => None, + } + } + /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. @@ -1440,6 +1451,71 @@ impl RoomScreen { tl.profile_drawn_since_last_update.remove(changed_indices.clone()); // log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update); } + + // --- Streaming detection --- + // Clear streaming state on timeline clear + if clear_cache { + tl.streaming_messages.clear(); + } + + // Compare old and new text for changed items to detect streaming + if !new_items.is_empty() && !changed_indices.is_empty() { + let current_uid = crate::sliding_sync::current_user_id(); + + for idx in changed_indices.clone() { + let Some(old_item) = tl.items.get(idx) else { continue }; + let Some(new_item) = new_items.get(idx) else { continue }; + + let Some(old_text) = Self::extract_message_text_from_item(old_item) else { continue }; + let Some(new_text) = Self::extract_message_text_from_item(new_item) else { continue }; + if old_text == new_text { continue; } + + // Get event_id and sender from new item + let TimelineItemKind::Event(new_evt) = new_item.kind() else { continue }; + let Some(event_id) = new_evt.event_id().map(|id| id.to_owned()) else { continue }; + let sender = new_evt.sender().to_owned(); + + // If already tracking: just update target text + if let Some(state) = tl.streaming_messages.get_mut(&event_id) { + state.update_target(&new_text); + self.streaming_next_frame = cx.new_next_frame(); + continue; + } + + // Heuristic detection: prefix extension + recency + not self + let is_prefix_extension = new_text.len() > old_text.len() + && new_text.starts_with(&old_text); + + let is_recent = { + let ts = new_evt.timestamp(); + let now_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + now_ms.saturating_sub(ts.0.into()) < 60_000 + }; + + let is_not_self = current_uid.as_ref() + .is_some_and(|uid| *uid != sender); + + if is_prefix_extension && is_recent && is_not_self { + use crate::home::streaming_animation::*; + let is_at_end = portal_list.is_at_end(); + tl.streaming_messages.insert( + event_id, + StreamingAnimState::new( + &new_text, + sender, + StreamDetection::Heuristic, + is_at_end, + ), + ); + self.streaming_next_frame = cx.new_next_frame(); + } + } + } + // --- End streaming detection --- + tl.items = new_items; done_loading = true; } From 5637a6f88243b7aba0ac9f7a0e5af72f593d8abc Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:42:48 +0800 Subject: [PATCH 011/283] feat: render streaming messages with typewriter animation --- src/home/room_screen.rs | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 9e1c6b59c..169ee303c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1179,6 +1179,7 @@ impl Widget for RoomScreen { &self.pinned_events, item_drawn_status, room_screen_widget_uid, + &mut tl_state.streaming_messages, ) }, // TODO: properly implement `Poll` as a regular Message-like timeline item. @@ -3130,6 +3131,7 @@ fn populate_message_view( pinned_events: &[OwnedEventId], item_drawn_status: ItemDrawnStatus, room_screen_widget_uid: WidgetUid, + streaming_messages: &mut HashMap, ) -> (WidgetRef, ItemDrawnStatus) { let mut new_drawn_status = item_drawn_status; let ts_millis = event_tl_item.timestamp(); @@ -3174,17 +3176,30 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - let mut link_preview_ref = - item.link_preview(cx, ids!(content.link_preview_view)); - new_drawn_status.content_drawn = populate_text_message_content( - cx, - &html_or_plaintext_ref, - body, - formatted.as_ref(), - Some(&mut link_preview_ref), - Some(media_cache), - Some(link_preview_cache), - ); + + // Check if this message is being streamed + let is_streaming = event_tl_item.event_id() + .and_then(|eid| streaming_messages.get_mut(&eid.to_owned())); + + if let Some(state) = is_streaming { + // STREAMING MODE: show partial plaintext with cursor + state.fill_display_buffer(); + html_or_plaintext_ref.show_plaintext(cx, &state.display_buffer); + new_drawn_status.content_drawn = false; // force re-render + } else { + // NORMAL MODE: existing logic + let mut link_preview_ref = + item.link_preview(cx, ids!(content.link_preview_view)); + new_drawn_status.content_drawn = populate_text_message_content( + cx, + &html_or_plaintext_ref, + body, + formatted.as_ref(), + Some(&mut link_preview_ref), + Some(media_cache), + Some(link_preview_cache), + ); + } (item, false) } } From 9f1213e6a9f4e6fc15dcf7916c2bb5a8fa434dd8 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:45:19 +0800 Subject: [PATCH 012/283] feat: enhance TypingUsers to carry user IDs for streaming detection --- src/home/room_screen.rs | 17 ++++++++++++++--- src/sliding_sync.rs | 17 ++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 169ee303c..d405f155c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1690,7 +1690,18 @@ impl RoomScreen { // Then, we "process" it later (by turning it into a string) after the // update loop has completed, which avoids unnecessary expensive work // if the list of typing users gets updated many times in a row. - typing_users = Some(users); + + // Update streaming sender_stopped_typing latch + { + let typing_user_ids: Vec<&OwnedUserId> = users.iter().map(|(uid, _)| uid).collect(); + for state in tl.streaming_messages.values_mut() { + if !typing_user_ids.contains(&&state.sender_user_id) { + state.sender_stopped_typing = true; + } + } + } + // Extract display names for the typing notice widget + typing_users = Some(users.iter().map(|(_, name)| name.clone()).collect::>()); } TimelineUpdate::PinnedEvents(pinned_events) => { self.pinned_events = pinned_events; @@ -2857,8 +2868,8 @@ pub enum TimelineUpdate { MediaFetched(MediaRequestParameters), /// A notice that one or more members of a this room are currently typing. TypingUsers { - /// The list of users (their displayable name) who are currently typing in this room. - users: Vec, + /// The list of users (user_id, display_name) who are currently typing in this room. + users: Vec<(OwnedUserId, String)>, }, /// The result of a pin/unpin request ([`MatrixRequest::PinEvent`]). PinResult { diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 30fccc5a2..e2c1bb34b 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1454,15 +1454,14 @@ async fn matrix_worker_task( // 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 display_name = 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()); + users.push((user_id, display_name)); } 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 68211cfe14bdbb6c88e5556a703f5475e54711d7 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:47:10 +0800 Subject: [PATCH 013/283] feat: add debug profiling for streaming animation frames --- src/home/room_screen.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index d405f155c..d21e8f767 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -661,6 +661,9 @@ impl Widget for RoomScreen { // Streaming animation frame handler if let Some(_ne) = self.streaming_next_frame.is_event(event) { + #[cfg(debug_assertions)] + let frame_start = std::time::Instant::now(); + if let Some(tl) = self.tl_state.as_mut() { let mut any_active = false; let mut completed_ids = Vec::new(); @@ -715,6 +718,17 @@ impl Widget for RoomScreen { self.redraw(cx); } } + + #[cfg(debug_assertions)] + { + if let Some(tl) = self.tl_state.as_ref() { + let elapsed = frame_start.elapsed(); + if elapsed.as_millis() > 2 { + log!("Streaming animation frame took {}ms ({} active streams)", + elapsed.as_millis(), tl.streaming_messages.len()); + } + } + } } // Handle actions here before processing timeline updates. From 8c3eb89e072ba9ab239d6bf40b3dffd87dec09e6 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:47:43 +0800 Subject: [PATCH 014/283] feat: handle streaming edge cases (edited indicator, restore) - Suppress edited indicator for actively-streaming messages to avoid misleading UI while text is still being updated - Re-request NextFrame in restore_state when streaming_messages is non-empty so the animation loop resumes after room switch - Verified streaming_messages.clear() on timeline clear is already present --- src/home/room_screen.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index d21e8f767..71a1f6a0c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -2605,6 +2605,12 @@ impl RoomScreen { tl_state.user_power, tl_state.tombstone_info.as_ref(), ); + + // 3. If there are active streaming animations, re-request the NextFrame event + // so the animation loop resumes (it stops when the room is hidden). + if !tl_state.streaming_messages.is_empty() { + self.streaming_next_frame = cx.new_next_frame(); + } } /// Sets this `RoomScreen` widget to display the timeline for the given room. @@ -3726,12 +3732,12 @@ fn populate_message_view( item.timestamp(cx, ids!(profile.timestamp)).set_date_time(cx, dt); } - // Set the "edited" indicator if this message was edited. - if msg_like_content.as_message().is_some_and(|m| m.is_edited()) { - item.edited_indicator(cx, ids!(profile.edited_indicator)).set_latest_edit( - cx, - event_tl_item, - ); + // Suppress "edited" indicator for actively streaming messages. + let is_streaming = event_tl_item.event_id() + .is_some_and(|eid| streaming_messages.contains_key(&eid.to_owned())); + if msg_like_content.as_message().is_some_and(|m| m.is_edited()) && !is_streaming { + item.edited_indicator(cx, ids!(profile.edited_indicator)) + .set_latest_edit(cx, event_tl_item); } #[cfg(feature = "tsp")] { From 9b6da745e5998104e560da0a17e72abdbf136cf4 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:48:32 +0800 Subject: [PATCH 015/283] fix: suppress unused variable warning in debug profiling --- src/home/room_screen.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 71a1f6a0c..2be406eb8 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -662,6 +662,7 @@ impl Widget for RoomScreen { // Streaming animation frame handler if let Some(_ne) = self.streaming_next_frame.is_event(event) { #[cfg(debug_assertions)] + #[allow(unused_variables)] let frame_start = std::time::Instant::now(); if let Some(tl) = self.tl_state.as_mut() { From 344fed7bfb2e1b021e350d9589490c61b9ff0cdc Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:53:34 +0800 Subject: [PATCH 016/283] chore: annotate dead code reserved for future use (MSC4357, was_at_end, catch_up) --- src/home/streaming_animation.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 8c4c32710..ec2df780b 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -5,6 +5,8 @@ use matrix_sdk::ruma::OwnedUserId; #[derive(Debug, Clone, PartialEq)] pub enum StreamDetection { /// Confirmed by MSC4357 live flag in event content. + /// Not yet implemented — placeholder for when crew-rs adds the live flag. + #[allow(dead_code)] Msc4357Live, /// Detected by heuristic: prefix match + recency + not self. Heuristic, @@ -24,6 +26,9 @@ pub struct StreamingAnimState { pub display_buffer: String, pub sender_stopped_typing: bool, pub sender_user_id: OwnedUserId, + /// Whether user was at list bottom when streaming started. + /// Reserved for auto-scroll gating in a future iteration. + #[allow(dead_code)] pub was_at_end: bool, pub detection: StreamDetection, } @@ -146,6 +151,9 @@ impl StreamingAnimState { self.last_update_time.elapsed().as_secs() > 30 } + /// Wall-clock catch-up: compute where cursor should be based on elapsed time. + /// Reserved for use after room restore or scroll-back — not yet called. + #[allow(dead_code)] pub fn catch_up_to_wall_clock(&mut self) { let elapsed = self.last_update_time.elapsed(); let elapsed_frames = elapsed.as_secs_f64() * 60.0; From c89bef1fbd39944f3c62eb3a615927f30249f776 Mon Sep 17 00:00:00 2001 From: Alvin Date: Tue, 24 Mar 2026 12:57:19 +0800 Subject: [PATCH 017/283] refactor: remove unimplemented dead code (Msc4357Live, was_at_end, catch_up_to_wall_clock) --- src/home/room_screen.rs | 2 -- src/home/streaming_animation.rs | 42 ++++----------------------------- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 2be406eb8..625ef513c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1516,14 +1516,12 @@ impl RoomScreen { if is_prefix_extension && is_recent && is_not_self { use crate::home::streaming_animation::*; - let is_at_end = portal_list.is_at_end(); tl.streaming_messages.insert( event_id, StreamingAnimState::new( &new_text, sender, StreamDetection::Heuristic, - is_at_end, ), ); self.streaming_next_frame = cx.new_next_frame(); diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index ec2df780b..33f8a1f8c 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -4,10 +4,6 @@ use matrix_sdk::ruma::OwnedUserId; /// How a streaming session was detected. #[derive(Debug, Clone, PartialEq)] pub enum StreamDetection { - /// Confirmed by MSC4357 live flag in event content. - /// Not yet implemented — placeholder for when crew-rs adds the live flag. - #[allow(dead_code)] - Msc4357Live, /// Detected by heuristic: prefix match + recency + not self. Heuristic, } @@ -26,15 +22,11 @@ pub struct StreamingAnimState { pub display_buffer: String, pub sender_stopped_typing: bool, pub sender_user_id: OwnedUserId, - /// Whether user was at list bottom when streaming started. - /// Reserved for auto-scroll gating in a future iteration. - #[allow(dead_code)] - pub was_at_end: bool, pub detection: StreamDetection, } impl StreamingAnimState { - pub fn new(initial_text: &str, sender_user_id: OwnedUserId, detection: StreamDetection, was_at_end: bool) -> Self { + pub fn new(initial_text: &str, sender_user_id: OwnedUserId, detection: StreamDetection) -> Self { let char_count = initial_text.chars().count(); let now = Instant::now(); Self { @@ -50,7 +42,6 @@ impl StreamingAnimState { display_buffer: String::with_capacity(initial_text.len() + 4), sender_stopped_typing: false, sender_user_id, - was_at_end, detection, } } @@ -134,33 +125,16 @@ impl StreamingAnimState { } /// Check if streaming is complete. - /// - /// For `Heuristic` detection, completes when the sender stops typing and - /// all text has been revealed. For `Msc4357Live`, this always returns `false` — - /// completion is signaled externally when the server removes the live flag, - /// which causes the entry to be removed from `streaming_messages` directly. + /// Completes when the sender stops typing and all text has been revealed. pub fn is_complete(&self) -> bool { if self.displayed_char_count < self.target_char_count { return false; } - match self.detection { - StreamDetection::Msc4357Live => false, - StreamDetection::Heuristic => self.sender_stopped_typing, - } + self.sender_stopped_typing } pub fn is_timed_out(&self) -> bool { self.last_update_time.elapsed().as_secs() > 30 } - /// Wall-clock catch-up: compute where cursor should be based on elapsed time. - /// Reserved for use after room restore or scroll-back — not yet called. - #[allow(dead_code)] - pub fn catch_up_to_wall_clock(&mut self) { - let elapsed = self.last_update_time.elapsed(); - let elapsed_frames = elapsed.as_secs_f64() * 60.0; - let expected = self.chars_at_last_update + (elapsed_frames * self.chars_per_frame) as usize; - let target = expected.min(self.target_char_count); - if target > self.displayed_char_count { self.advance_displayed(target - self.displayed_char_count); } - } } #[cfg(test)] @@ -169,7 +143,7 @@ mod tests { fn make_state(text: &str) -> StreamingAnimState { let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); - StreamingAnimState::new(text, user_id, StreamDetection::Heuristic, true) + StreamingAnimState::new(text, user_id, StreamDetection::Heuristic) } #[test] @@ -263,14 +237,6 @@ mod tests { assert!(s.is_complete()); } - #[test] - fn test_is_complete_msc4357_never_self_completes() { - let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); - let mut s = StreamingAnimState::new("Hi", user_id, StreamDetection::Msc4357Live, true); - s.advance_displayed(2); - s.sender_stopped_typing = true; - assert!(!s.is_complete()); // Msc4357Live never self-completes - } #[test] fn test_advance_zero_is_noop() { From 4e564b58ec4c72639f4368cc1ba18896f77e0cf1 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 25 Mar 2026 10:36:59 +0800 Subject: [PATCH 018/283] fix: harden streaming animation against timeline edge cases - Clamp changed_indices to prevent infinite iteration on usize::MAX sentinel - Preserve visible prefix when entering streaming mode (no replay from start) - Make typing latch bidirectional so transient drops don't cause early completion - Only request NextFrame when streams have unrevealed characters - Cache timeline indices to avoid O(streams*items) per-frame scan - Use Timer for idle timeout instead of per-frame polling - Time-based animation (chars_per_second) for frame-rate independence - Simplify test names and remove duplicate tests --- src/home/room_screen.rs | 219 +++++++++++++++++++++++++++----- src/home/streaming_animation.rs | 121 ++++++++++++++---- 2 files changed, 284 insertions(+), 56 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 625ef513c..8c0827e66 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,7 +1,7 @@ //! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline //! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc}; +use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc, time::Duration}; use bytesize::ByteSize; use hashbrown::{HashMap, HashSet}; @@ -54,6 +54,7 @@ const MAX_ITEMS_TO_SEARCH_THROUGH: usize = 100; /// The max size (width or height) of a blurhash image to decode. const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; +const STREAMING_IDLE_TIMEOUT: Duration = Duration::from_secs(30); static UNNAMED_ROOM: &str = "Unnamed Room"; @@ -62,6 +63,53 @@ const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); /// #FFEACC const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); +fn timeline_item_event_id(item: &Arc) -> Option<&EventId> { + let TimelineItemKind::Event(event) = item.kind() else { + return None; + }; + event.event_id() +} + +fn bounded_changed_indices( + changed_indices: &Range, + old_len: usize, + new_len: usize, +) -> Range { + let end = changed_indices.end.min(old_len.min(new_len)); + let start = changed_indices.start.min(end); + start..end +} + +fn refresh_streaming_message_indices<'a, I>( + event_ids: I, + streaming_messages: &mut HashMap, +) +where + I: IntoIterator>, +{ + for state in streaming_messages.values_mut() { + state.timeline_index = None; + } + + for (idx, event_id) in event_ids.into_iter().enumerate() { + let Some(event_id) = event_id else { + continue; + }; + if let Some(state) = streaming_messages.get_mut(event_id) { + state.timeline_index = Some(idx); + } + } +} + +fn next_streaming_timeout_duration<'a>( + states: impl IntoIterator, + idle_timeout: Duration, +) -> Option { + states + .into_iter() + .map(|state| idle_timeout.saturating_sub(state.last_update_time.elapsed())) + .min() +} script_mod! { use mod.prelude.widgets.* @@ -625,6 +673,9 @@ pub struct RoomScreen { /// NextFrame subscription for driving streaming typewriter animation. #[rust] streaming_next_frame: NextFrame, + /// Timeout used to evict stalled streaming states without per-frame polling. + #[rust] + streaming_timeout_timer: Timer, } impl Drop for RoomScreen { @@ -667,29 +718,19 @@ impl Widget for RoomScreen { if let Some(tl) = self.tl_state.as_mut() { let mut any_active = false; + let mut needs_another_frame = false; let mut completed_ids = Vec::new(); - // Build event_id → index lookup for cache invalidation - let streaming_indices: Vec<(OwnedEventId, usize)> = tl.streaming_messages.keys() - .filter_map(|eid| { - tl.items.iter().enumerate().find_map(|(idx, item)| { - if let TimelineItemKind::Event(evt) = item.kind() { - if evt.event_id().is_some_and(|id| id == eid) { - return Some((eid.clone(), idx)); - } - } - None - }) - }) - .collect(); - for (event_id, state) in tl.streaming_messages.iter_mut() { - if state.tick() { - any_active = true; - // Invalidate draw cache so item gets re-populated - if let Some((_, idx)) = streaming_indices.iter().find(|(eid, _)| eid == event_id) { - tl.content_drawn_since_last_update.remove(*idx..*idx + 1); + if state.needs_frame() { + if state.tick() { + any_active = true; + // Invalidate draw cache so item gets re-populated + if let Some(idx) = state.timeline_index { + tl.content_drawn_since_last_update.remove(idx..idx + 1); + } } + needs_another_frame |= state.needs_frame(); } if state.is_complete() || state.is_timed_out() { @@ -708,10 +749,11 @@ impl Widget for RoomScreen { .map(|(id, _)| id.clone()) { tl.streaming_messages.remove(&oldest_id); + any_active = true; } } - if any_active || !tl.streaming_messages.is_empty() { + if needs_another_frame { self.streaming_next_frame = cx.new_next_frame(); } @@ -730,6 +772,34 @@ impl Widget for RoomScreen { } } } + + self.schedule_streaming_timeout_if_needed(cx); + } + + if self.streaming_timeout_timer.is_event(event).is_some() { + if let Some(tl) = self.tl_state.as_mut() { + let timed_out_ids: Vec = tl + .streaming_messages + .iter() + .filter_map(|(event_id, state)| { + if state.is_timed_out() || state.is_complete() { + Some(event_id.clone()) + } else { + None + } + }) + .collect(); + + for event_id in &timed_out_ids { + tl.streaming_messages.remove(event_id); + } + + if !timed_out_ids.is_empty() { + self.redraw(cx); + } + } + + self.schedule_streaming_timeout_if_needed(cx); } // Handle actions here before processing timeline updates. @@ -1323,6 +1393,19 @@ impl RoomScreen { } } + fn schedule_streaming_timeout_if_needed(&mut self, cx: &mut Cx) { + cx.stop_timer(self.streaming_timeout_timer); + self.streaming_timeout_timer = next_streaming_timeout_duration( + self.tl_state + .as_ref() + .into_iter() + .flat_map(|tl| tl.streaming_messages.values()), + STREAMING_IDLE_TIMEOUT, + ) + .map(|duration| cx.start_timeout(duration.as_secs_f64())) + .unwrap_or_else(Timer::empty); + } + /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. @@ -1350,6 +1433,10 @@ impl RoomScreen { jump_to_bottom_button.update_visibility(cx, true); tl.items = initial_items; + refresh_streaming_message_indices( + tl.items.iter().map(timeline_item_event_id), + &mut tl.streaming_messages, + ); done_loading = true; } TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { @@ -1477,10 +1564,15 @@ impl RoomScreen { // Compare old and new text for changed items to detect streaming if !new_items.is_empty() && !changed_indices.is_empty() { let current_uid = crate::sliding_sync::current_user_id(); + let changed_indices = + bounded_changed_indices(&changed_indices, tl.items.len(), new_items.len()); - for idx in changed_indices.clone() { + for idx in changed_indices { let Some(old_item) = tl.items.get(idx) else { continue }; let Some(new_item) = new_items.get(idx) else { continue }; + if timeline_item_event_id(old_item) != timeline_item_event_id(new_item) { + continue; + } let Some(old_text) = Self::extract_message_text_from_item(old_item) else { continue }; let Some(new_text) = Self::extract_message_text_from_item(new_item) else { continue }; @@ -1518,7 +1610,8 @@ impl RoomScreen { use crate::home::streaming_animation::*; tl.streaming_messages.insert( event_id, - StreamingAnimState::new( + StreamingAnimState::new_from_visible_prefix( + &old_text, &new_text, sender, StreamDetection::Heuristic, @@ -1531,6 +1624,10 @@ impl RoomScreen { // --- End streaming detection --- tl.items = new_items; + refresh_streaming_message_indices( + tl.items.iter().map(timeline_item_event_id), + &mut tl.streaming_messages, + ); done_loading = true; } TimelineUpdate::NewUnreadMessagesCount(unread_messages_count) => { @@ -1706,11 +1803,14 @@ impl RoomScreen { // Update streaming sender_stopped_typing latch { - let typing_user_ids: Vec<&OwnedUserId> = users.iter().map(|(uid, _)| uid).collect(); + let typing_user_ids: HashSet<&OwnedUserId> = + users.iter().map(|(uid, _)| uid).collect(); for state in tl.streaming_messages.values_mut() { - if !typing_user_ids.contains(&&state.sender_user_id) { - state.sender_stopped_typing = true; - } + state.sender_stopped_typing = + !typing_user_ids.contains(&state.sender_user_id); + } + if !tl.streaming_messages.is_empty() { + self.streaming_next_frame = cx.new_next_frame(); } } // Extract display names for the typing notice widget @@ -1770,6 +1870,7 @@ impl RoomScreen { } if num_updates > 0 { + self.schedule_streaming_timeout_if_needed(cx); // log!("Applied {} timeline updates for room {}, redrawing with {} items...", num_updates, tl.kind.room_id(), tl.items.len()); self.redraw(cx); } @@ -2507,6 +2608,7 @@ impl RoomScreen { // Store the tl_state for this room into this RoomScreen widget, // such that it can be accessed in future functions like event/draw handlers. self.tl_state = Some(tl_state); + self.schedule_streaming_timeout_if_needed(cx); // Now that we have restored the TimelineUiState into this RoomScreen widget, // we can proceed to processing pending background updates. @@ -2518,6 +2620,7 @@ impl RoomScreen { /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown. fn hide_timeline(&mut self) { let Some(timeline_kind) = self.timeline_kind.clone() else { return }; + self.streaming_timeout_timer = Timer::empty(); self.save_state(); @@ -2605,9 +2708,14 @@ impl RoomScreen { tl_state.tombstone_info.as_ref(), ); - // 3. If there are active streaming animations, re-request the NextFrame event - // so the animation loop resumes (it stops when the room is hidden). - if !tl_state.streaming_messages.is_empty() { + refresh_streaming_message_indices( + tl_state.items.iter().map(timeline_item_event_id), + &mut tl_state.streaming_messages, + ); + + // 3. If there are active streaming animations that can still reveal text, + // re-request the NextFrame event so the animation loop resumes. + if tl_state.streaming_messages.values().any(|state| state.needs_frame()) { self.streaming_next_frame = cx.new_next_frame(); } } @@ -4983,3 +5091,54 @@ pub fn clear_timeline_states(_cx: &mut Cx) { states.clear(); }); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::home::streaming_animation::{StreamDetection, StreamingAnimState}; + use std::time::{Duration, Instant}; + + fn make_state(text: &str) -> StreamingAnimState { + let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); + StreamingAnimState::new(text, user_id, StreamDetection::Heuristic) + } + + #[test] + fn test_bounded_indices_clamps_max() { + let bounded = bounded_changed_indices(&(1..usize::MAX), 3, 4); + assert_eq!(bounded, 1..3); + } + + #[test] + fn test_refresh_stream_indices() { + let event_id_a: OwnedEventId = "$event-a:example.com".try_into().unwrap(); + let event_id_b: OwnedEventId = "$event-b:example.com".try_into().unwrap(); + let missing_event_id: OwnedEventId = "$missing:example.com".try_into().unwrap(); + + let mut streaming_messages = HashMap::new(); + streaming_messages.insert(event_id_a.clone(), make_state("alpha")); + streaming_messages.insert(missing_event_id.clone(), make_state("missing")); + + let event_ids = vec![None, Some(event_id_a.as_ref()), Some(event_id_b.as_ref())]; + refresh_streaming_message_indices(event_ids.into_iter(), &mut streaming_messages); + + assert_eq!(streaming_messages[&event_id_a].timeline_index, Some(1)); + assert_eq!(streaming_messages[&missing_event_id].timeline_index, None); + } + + #[test] + fn test_timeout_picks_earliest() { + let mut first = make_state("alpha"); + first.last_update_time = Instant::now() - Duration::from_secs(10); + let mut second = make_state("beta"); + second.last_update_time = Instant::now() - Duration::from_secs(29); + + let timeout = next_streaming_timeout_duration( + [&first, &second].into_iter(), + Duration::from_secs(30), + ) + .unwrap(); + + assert!(timeout <= Duration::from_secs(1)); + } +} diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 33f8a1f8c..4ac2fb0c1 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use matrix_sdk::ruma::OwnedUserId; /// How a streaming session was detected. @@ -14,15 +14,17 @@ pub struct StreamingAnimState { pub target_char_count: usize, pub displayed_char_count: usize, pub displayed_byte_offset: usize, - pub chars_per_frame: f64, + pub chars_per_second: f64, pub fractional_chars: f64, pub last_update_time: Instant, + pub last_tick_time: Instant, pub animation_start_time: Instant, pub chars_at_last_update: usize, pub display_buffer: String, pub sender_stopped_typing: bool, pub sender_user_id: OwnedUserId, pub detection: StreamDetection, + pub timeline_index: Option, } impl StreamingAnimState { @@ -34,22 +36,41 @@ impl StreamingAnimState { target_char_count: char_count, displayed_char_count: 0, displayed_byte_offset: 0, - chars_per_frame: 1.0, + chars_per_second: 1.0, fractional_chars: 0.0, last_update_time: now, + last_tick_time: now, animation_start_time: now, chars_at_last_update: 0, display_buffer: String::with_capacity(initial_text.len() + 4), sender_stopped_typing: false, sender_user_id, detection, + timeline_index: None, } } + pub fn new_from_visible_prefix( + visible_prefix: &str, + target_text: &str, + sender_user_id: OwnedUserId, + detection: StreamDetection, + ) -> Self { + let mut state = Self::new(target_text, sender_user_id, detection); + if target_text.starts_with(visible_prefix) { + state.displayed_char_count = visible_prefix.chars().count(); + state.displayed_byte_offset = visible_prefix.len(); + state.chars_at_last_update = state.displayed_char_count; + } + state.update_speed(); + state + } + pub fn update_target(&mut self, new_text: &str) { self.target_text.clear(); self.target_text.push_str(new_text); self.target_char_count = new_text.chars().count(); + self.sender_stopped_typing = false; // Clamp display pointers if the new text is shorter than what was already displayed. if self.displayed_char_count > self.target_char_count { @@ -61,21 +82,28 @@ impl StreamingAnimState { .map_or(self.target_text.len(), |(i, _)| i); } + let now = Instant::now(); self.chars_at_last_update = self.displayed_char_count; - self.last_update_time = Instant::now(); - let remaining = self.target_char_count.saturating_sub(self.displayed_char_count); - if remaining > 0 { - self.chars_per_frame = remaining as f64 / 60.0; - if self.chars_per_frame < 0.5 { self.chars_per_frame = 0.5; } - } - // Fix: reserve uses wrong base — reserve(n) guarantees capacity >= len + n, - // not capacity >= n. Compare against capacity and reserve only the deficit. + self.last_update_time = now; + self.last_tick_time = now; + self.update_speed(); + // Reserve only the deficit (reserve(n) guarantees capacity >= len + n). let needed = new_text.len() + 4; if self.display_buffer.capacity() < needed { self.display_buffer.reserve(needed - self.display_buffer.len()); } } + fn update_speed(&mut self) { + let remaining = self.target_char_count.saturating_sub(self.displayed_char_count); + if remaining > 0 { + self.chars_per_second = remaining as f64; + if self.chars_per_second < 30.0 { + self.chars_per_second = 30.0; + } + } + } + pub fn advance_displayed(&mut self, chars_to_add: usize) { if chars_to_add == 0 || self.displayed_char_count >= self.target_char_count { return; } let remaining = &self.target_text[self.displayed_byte_offset..]; @@ -93,6 +121,13 @@ impl StreamingAnimState { } pub fn tick(&mut self) -> bool { + let now = Instant::now(); + let elapsed = now.saturating_duration_since(self.last_tick_time); + self.last_tick_time = now; + self.tick_with_elapsed(elapsed) + } + + pub fn tick_with_elapsed(&mut self, elapsed: Duration) -> bool { if self.displayed_char_count >= self.target_char_count { return false; } let gap = self.target_char_count - self.displayed_char_count; let mut changed = false; @@ -101,14 +136,14 @@ impl StreamingAnimState { let jump = gap - 50; self.advance_displayed(jump); changed = true; - self.chars_per_frame + self.chars_per_second } else if gap > 200 { - self.chars_per_frame * 3.0 + self.chars_per_second * 3.0 } else { - self.chars_per_frame + self.chars_per_second }; - self.fractional_chars += speed; + self.fractional_chars += speed * elapsed.as_secs_f64(); let advance = self.fractional_chars.floor() as usize; self.fractional_chars -= advance as f64; if advance > 0 { @@ -124,10 +159,14 @@ impl StreamingAnimState { self.display_buffer.push_str(" \u{25CF}"); } + pub fn needs_frame(&self) -> bool { + self.displayed_char_count < self.target_char_count + } + /// Check if streaming is complete. /// Completes when the sender stops typing and all text has been revealed. pub fn is_complete(&self) -> bool { - if self.displayed_char_count < self.target_char_count { return false; } + if self.needs_frame() { return false; } self.sender_stopped_typing } @@ -178,7 +217,7 @@ mod tests { s.update_target("Hello, world!"); assert_eq!(s.target_char_count, 13); assert_eq!(s.displayed_char_count, 5); - assert!(s.chars_per_frame > 0.0); + assert!(s.chars_per_second > 0.0); } #[test] @@ -196,26 +235,24 @@ mod tests { #[test] fn test_tick_advances() { let mut s = make_state("Hello, world!"); - s.chars_per_frame = 2.0; - let changed = s.tick(); + s.chars_per_second = 4.0; + let changed = s.tick_with_elapsed(Duration::from_millis(500)); assert!(changed); assert_eq!(s.displayed_char_count, 2); } #[test] - fn test_tick_no_change_when_complete() { + fn test_tick_complete_noop() { let mut s = make_state("Hi"); s.advance_displayed(2); - let changed = s.tick(); - assert!(!changed); + assert!(!s.tick_with_elapsed(Duration::from_secs(1))); } #[test] - fn test_tick_large_gap_returns_true() { + fn test_tick_large_gap() { let mut s = make_state(&"a".repeat(1000)); - s.chars_per_frame = 0.1; // very slow, fractional won't trigger - let changed = s.tick(); - assert!(changed); // should still return true due to the jump + s.chars_per_second = 0.1; + assert!(s.tick_with_elapsed(Duration::from_secs(1))); assert!(s.displayed_char_count > 900); } @@ -237,6 +274,38 @@ mod tests { assert!(s.is_complete()); } + #[test] + fn test_visible_prefix_preserved() { + let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); + let s = StreamingAnimState::new_from_visible_prefix( + "Hello", "Hello, world!", user_id, StreamDetection::Heuristic, + ); + assert_eq!(s.displayed_char_count, 5); + assert_eq!(&s.target_text[..s.displayed_byte_offset], "Hello"); + } + + #[test] + fn test_update_target_resets_typing() { + let mut s = make_state("Hello"); + s.sender_stopped_typing = true; + s.update_target("Hello, world!"); + assert!(!s.sender_stopped_typing); + } + + #[test] + fn test_needs_frame_when_caught_up() { + let mut s = make_state("Hello"); + s.advance_displayed(5); + assert!(!s.needs_frame()); + } + + #[test] + fn test_tick_zero_elapsed() { + let mut s = make_state("Hello"); + s.chars_per_second = 20.0; + assert!(!s.tick_with_elapsed(Duration::ZERO)); + assert_eq!(s.displayed_char_count, 0); + } #[test] fn test_advance_zero_is_noop() { From e7e7f27430c4033230b58d7fa56c4fd4a646034e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 25 Mar 2026 11:07:31 +0800 Subject: [PATCH 019/283] Recover from invalid Matrix sessions Reset runtime state and return to the login loop when session tokens expire, and remove persisted Matrix stores alongside stale session files. --- src/persistence/matrix_state.rs | 55 +++- src/sliding_sync.rs | 436 ++++++++++++++++++-------------- 2 files changed, 304 insertions(+), 187 deletions(-) diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index f984a2f3b..7e51cb35a 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -1,8 +1,8 @@ //! Handles app persistence by saving and restoring client session data to/from the filesystem. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail}; -use makepad_widgets::{log, Cx}; +use makepad_widgets::{log, warning, Cx}; use matrix_sdk::{ authentication::matrix::MatrixSession, ruma::{OwnedUserId, UserId}, @@ -255,6 +255,26 @@ pub async fn delete_latest_user_id() -> anyhow::Result { } } +async fn delete_path_if_exists(path: &Path) -> anyhow::Result { + let metadata = match tokio::fs::metadata(path).await { + Ok(metadata) => metadata, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(e) => return Err(anyhow!("Failed to inspect path {}: {e}", path.display())), + }; + + if metadata.is_dir() { + tokio::fs::remove_dir_all(path) + .await + .map_err(|e| anyhow!("Failed to remove directory {}: {e}", path.display()))?; + } else { + tokio::fs::remove_file(path) + .await + .map_err(|e| anyhow!("Failed to remove file {}: {e}", path.display()))?; + } + + Ok(true) +} + /// Remove the persisted Matrix session file for the given user if it exists. /// /// Returns: @@ -265,6 +285,37 @@ pub async fn delete_session(user_id: &UserId) -> anyhow::Result { let session_file = session_file_path(user_id); if session_file.exists() { + let persisted_db_path = match tokio::fs::read_to_string(&session_file).await { + Ok(serialized_session) => { + match serde_json::from_str::(&serialized_session) { + Ok(session) => Some(session.client_session.db_path), + Err(e) => { + warning!( + "Failed to parse session file {} before cleanup: {e}", + session_file.display() + ); + None + } + } + } + Err(e) => { + warning!( + "Failed to read session file {} before cleanup: {e}", + session_file.display() + ); + None + } + }; + + if let Some(db_path) = persisted_db_path { + if let Err(e) = delete_path_if_exists(&db_path).await { + warning!( + "Failed to remove persisted Matrix store {} for {user_id}: {e}", + db_path.display() + ); + } + } + tokio::fs::remove_file(&session_file) .await .map_err(|e| anyhow::anyhow!("Failed to remove session file {session_file:?}: {e}")) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 99f799ae0..eaeddca78 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -325,6 +325,34 @@ async fn clear_persisted_session(user_id: Option<&UserId>) { } } +enum SessionResetAction { + Reauthenticate { message: String }, +} + +async fn reset_runtime_state_for_relogin() { + let sync_service = { SYNC_SERVICE.lock().unwrap().take() }; + if let Some(sync_service) = sync_service { + sync_service.stop().await; + } + + CLIENT.lock().unwrap().take(); + DEFAULT_SSO_CLIENT.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(), + }); + + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()) + .await + .is_err() + { + warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); + } +} + fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { matches!( error.client_api_error_kind(), @@ -2850,221 +2878,253 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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, validate_session) = 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, false), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e}"), - }); + loop { + let (client, sync_service, logged_in_user_id) = 'login_loop: loop { + let (client, _sync_token, validate_session) = 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, false), + 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; } - }, - 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; } - } - }, - }; + }, + }; - if validate_session { - match client.whoami().await { - Ok(_) => {} - Err(e) if is_invalid_token_http_error(&e) => { - clear_persisted_session(client.user_id()).await; - let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; - Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.to_string(), - }); - continue 'login_loop; - } - Err(e) => { - warning!( - "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" - ); + if validate_session { + match client.whoami().await { + Ok(_) => {} + Err(e) if is_invalid_token_http_error(&e) => { + clear_persisted_session(client.user_id()).await; + let err_msg = + "Your login token is no longer valid.\n\nPlease log in again."; + Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.to_string(), + }); + continue 'login_loop; + } + Err(e) => { + warning!( + "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" + ); + } } } - } - - // 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 }); + // Deallocate the default SSO client after a successful login. + if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { + let _ = client_opt.take(); + } - // 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." - ); - } + 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 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 updates to the ignored user list. + handle_ignore_user_list_subscriber(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}.") - }; - if is_invalid_token_error(&e) { - clear_persisted_session(client.user_id()).await; + 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}." + ) + }; + if is_invalid_token_error(&e) { + clear_persisted_session(client.user_id()).await; + } + 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; } - 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; - } - }; - - // Listen for session changes, e.g., when the access token becomes invalid. - handle_session_changes(client.clone()); + }; - break 'login_loop (client, sync_service, logged_in_user_id); - }; + 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); + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); - // 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; + // 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); - let room_list_service = sync_service.room_list_service(); + // 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; - 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 room_list_service = sync_service.room_list_service(); - 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)); + 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." + ); + } - // 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!"); + 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 + // matrix/background tasks for the currently-authenticated session. + #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. + let reauth_message = loop { + tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + break message; + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; } } - 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:?}"); + } + result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); + 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:?}"); + } + } + return; + } + result = &mut room_list_service_task => { + session_change_handler_task.abort(); + 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!("Rooms list update error: {e}"), + format!("Room list service error: {e}"), PopupKind::Error, None, ); + }, + Err(e) => { + error!("BUG: failed to join room list service loop task: {e:?}"); } - }, - 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:?}"); } + return; } - 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:?}"); + result = &mut space_service_task => { + session_change_handler_task.abort(); + 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:?}"); + } } + return; } - break; } - } + }; + + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + + reset_runtime_state_for_relogin().await; + Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: reauth_message, + }); + initial_client_opt = None; } } @@ -3919,7 +3979,10 @@ fn is_invalid_token_error(e: &sync_service::Error) -> bool { /// 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) { +fn handle_session_changes( + client: Client, + session_reset_sender: UnboundedSender, +) -> JoinHandle<()> { let mut receiver = client.subscribe_to_session_changes(); Handle::current().spawn(async move { loop { @@ -3932,7 +3995,10 @@ fn handle_session_changes(client: Client) { }; error!("Session token is no longer valid (soft_logout: {soft_logout}). Prompting re-login."); clear_persisted_session(client.user_id()).await; - Cx::post_action(LoginAction::LoginFailure(msg.to_string())); + let _ = session_reset_sender.send(SessionResetAction::Reauthenticate { + message: msg.to_string(), + }); + break; } Ok(SessionChange::TokensRefreshed) => {} Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3943,7 +4009,7 @@ fn handle_session_changes(client: Client) { } } } - }); + }) } fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { From 58957870ab3c907539ca2cd7692c0f795bfb477e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 25 Mar 2026 11:23:25 +0800 Subject: [PATCH 020/283] Simplify login screen layout Make the login form use a narrower centered layout and remove the extra outer login panel background so the desktop presentation matches the mobile-style card better. --- src/login/login_screen.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index dfa25fee7..6b23121d8 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -49,19 +49,17 @@ script_mod! { width: Fill, height: Fill, align: Align{x: 0.5, y: 0.5} - show_bg: true, + show_bg: false, draw_bg +: { - color: COLOR_SECONDARY - // color: COLOR_PRIMARY // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY + color: COLOR_TRANSPARENT } ScrollYView { width: Fill, height: Fill, // Note: *do NOT* vertically center this, it will break scrolling. align: Align{x: 0.5} - show_bg: true, - draw_bg.color: (COLOR_SECONDARY) - // draw_bg.color: (COLOR_PRIMARY) // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY + show_bg: false, + draw_bg.color: (COLOR_TRANSPARENT) // allow the view to be scrollable but hide the actual scroll bar scroll_bars: { @@ -71,26 +69,20 @@ script_mod! { } } - RoundedView { - margin: Inset{top: 40, bottom: 40} - width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` + View { + margin: Inset{top: 32, bottom: 32} + width: 360 height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, - show_bg: true, - draw_bg +: { - color: (COLOR_SECONDARY) - border_radius: 6.0 - } - View { - width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` + width: 360 height: Fit flow: Down align: Align{x: 0.5, y: 0.5} - padding: Inset{top: 30, bottom: 30} - margin: Inset{top: 40, bottom: 40} + padding: Inset{top: 34, bottom: 30, left: 24, right: 24} + margin: Inset{top: 12, bottom: 12} spacing: 15.0 logo_image := Image { From 9d674e71bb55020c4be8d44b7423f22c20d10752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 25 Mar 2026 11:45:29 +0800 Subject: [PATCH 021/283] Remove login panel container styling Keep the login screen layout intact while replacing the extra outer login panel with a plain view so the screen no longer draws a separate card container. --- src/login/login_screen.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 6b23121d8..db7ad8457 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -49,17 +49,19 @@ script_mod! { width: Fill, height: Fill, align: Align{x: 0.5, y: 0.5} - show_bg: false, + show_bg: true, draw_bg +: { - color: COLOR_TRANSPARENT + color: COLOR_SECONDARY + // color: COLOR_PRIMARY // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY } ScrollYView { width: Fill, height: Fill, // Note: *do NOT* vertically center this, it will break scrolling. align: Align{x: 0.5} - show_bg: false, - draw_bg.color: (COLOR_TRANSPARENT) + show_bg: true, + draw_bg.color: (COLOR_SECONDARY) + // draw_bg.color: (COLOR_PRIMARY) // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY // allow the view to be scrollable but hide the actual scroll bar scroll_bars: { @@ -70,19 +72,19 @@ script_mod! { } View { - margin: Inset{top: 32, bottom: 32} - width: 360 + margin: Inset{top: 40, bottom: 40} + width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, View { - width: 360 + width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit flow: Down align: Align{x: 0.5, y: 0.5} - padding: Inset{top: 34, bottom: 30, left: 24, right: 24} - margin: Inset{top: 12, bottom: 12} + padding: Inset{top: 30, bottom: 30} + margin: Inset{top: 40, bottom: 40} spacing: 15.0 logo_image := Image { From 69c788685f4358cd09472b09cffbfdc009f3de63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 25 Mar 2026 14:25:29 +0800 Subject: [PATCH 022/283] Reback fmt --- src/login/login_screen.rs | 172 +-- src/persistence/matrix_state.rs | 58 +- src/sliding_sync.rs | 1977 ++++++++++++------------------- 3 files changed, 824 insertions(+), 1383 deletions(-) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index db7ad8457..bf4ec59c2 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,9 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{ - submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount, -}; +use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -62,7 +60,7 @@ script_mod! { show_bg: true, draw_bg.color: (COLOR_SECONDARY) // draw_bg.color: (COLOR_PRIMARY) // TODO: once Makepad supports `Fill {max: 375}`, change this back to COLOR_PRIMARY - + // allow the view to be scrollable but hide the actual scroll bar scroll_bars: { scroll_bar_y: { @@ -169,7 +167,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } } - + login_button := RobrixIconButton { width: 275, @@ -261,7 +259,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - + mode_toggle_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} @@ -288,76 +286,45 @@ script_mod! { #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { - #[source] - source: ScriptObjectRef, - #[deref] - view: View, + #[source] source: ScriptObjectRef, + #[deref] view: View, /// Whether the screen is showing the in-app sign-up flow. - #[rust] - signup_mode: bool, + #[rust] signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight - #[rust] - sso_pending: bool, + #[rust] sso_pending: bool, /// The URL to redirect to after logging in with SSO. - #[rust] - sso_redirect_url: Option, + #[rust] sso_redirect_url: Option, /// The most recent login failure message shown to the user. - #[rust] - last_failure_message_shown: Option, + #[rust] last_failure_message_shown: Option, } impl LoginScreen { fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { self.signup_mode = signup_mode; - self.view - .view(cx, ids!(confirm_password_wrapper)) - .set_visible(cx, signup_mode); - self.view - .view(cx, ids!(login_only_view)) - .set_visible(cx, !signup_mode); - self.view.label(cx, ids!(title)).set_text( - cx, - if signup_mode { - "Create your Robrix account" - } else { - "Login to Robrix" - }, + self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); + self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); + self.view.label(cx, ids!(title)).set_text(cx, + if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } ); - self.view.button(cx, ids!(login_button)).set_text( - cx, - if signup_mode { - "Create account" - } else { - "Login" - }, + self.view.button(cx, ids!(login_button)).set_text(cx, + if signup_mode { "Create account" } else { "Login" } ); - self.view.label(cx, ids!(account_prompt_label)).set_text( - cx, - if signup_mode { - "Already have an account?" - } else { - "Don't have an account?" - }, + self.view.label(cx, ids!(account_prompt_label)).set_text(cx, + if signup_mode { "Already have an account?" } else { "Don't have an account?" } ); - self.view.button(cx, ids!(mode_toggle_button)).set_text( - cx, - if signup_mode { - "Back to login" - } else { - "Sign up here" - }, + self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, + if signup_mode { "Back to login" } else { "Sign up here" } ); if !signup_mode { - self.view - .text_input(cx, ids!(confirm_password_input)) - .set_text(cx, ""); + self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); } self.redraw(cx); } } + impl Widget for LoginScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -379,9 +346,7 @@ impl MatchEvent for LoginScreen { let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); - let login_status_modal_inner = self - .view - .login_status_modal(cx, ids!(login_status_modal_inner)); + let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); if mode_toggle_button.clicked(actions) { self.set_signup_mode(cx, !self.signup_mode); @@ -407,21 +372,15 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else if self.signup_mode && password != confirm_password { login_status_modal_inner.set_title(cx, "Passwords do not match"); - login_status_modal_inner.set_status( - cx, - "Please enter the same password in both password fields.", - ); + login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { self.last_failure_message_shown = None; - login_status_modal_inner.set_title( - cx, - if self.signup_mode { - "Creating account..." - } else { - "Logging in..." - }, - ); + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Creating account..." + } else { + "Logging in..." + }); login_status_modal_inner.set_status( cx, if self.signup_mode { @@ -430,9 +389,7 @@ impl MatchEvent for LoginScreen { "Waiting for a login response..." }, ); - login_status_modal_inner - .button_ref(cx) - .set_text(cx, "Cancel"); + login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); submit_async_request(MatrixRequest::Login(if self.signup_mode { LoginRequest::Register(RegisterAccount { user_id, @@ -450,14 +407,14 @@ impl MatchEvent for LoginScreen { login_status_modal.open(cx); self.redraw(cx); } - + let provider_brands = ["apple", "facebook", "github", "gitlab", "google", "twitter"]; let button_set: &[&[LiveId]] = ids_array!( - apple_button, - facebook_button, - github_button, - gitlab_button, - google_button, + apple_button, + facebook_button, + github_button, + gitlab_button, + google_button, twitter_button ); for action in actions { @@ -467,17 +424,16 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { - Some(LoginAction::CliAutoLogin { - user_id, - homeserver, - }) => { + Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); login_status_modal_inner.set_title(cx, "Logging in via CLI..."); - login_status_modal_inner - .set_status(cx, &format!("Auto-logging in as user {user_id}...")); + login_status_modal_inner.set_status( + cx, + &format!("Auto-logging in as user {user_id}...") + ); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Cancel"); login_status_modal_button.set_enabled(cx, false); // Login cancel not yet supported @@ -510,14 +466,11 @@ impl MatchEvent for LoginScreen { continue; } self.last_failure_message_shown = Some(error.clone()); - login_status_modal_inner.set_title( - cx, - if self.signup_mode { - "Account Creation Failed." - } else { - "Login Failed." - }, - ); + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Account Creation Failed." + } else { + "Login Failed." + }); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); @@ -527,15 +480,9 @@ impl MatchEvent for LoginScreen { } Some(LoginAction::SsoPending(pending)) => { let mask = if *pending { 1.0 } else { 0.0 }; - let cursor = if *pending { - MouseCursor::NotAllowed - } else { - MouseCursor::Hand - }; + let cursor = if *pending { MouseCursor::NotAllowed } else { MouseCursor::Hand }; for view_ref in self.view_set(cx, button_set).iter() { - let Some(mut view_mut) = view_ref.borrow_mut() else { - continue; - }; + let Some(mut view_mut) = view_ref.borrow_mut() else { continue }; let mut image = view_mut.image(cx, ids!(image)); script_apply_eval!(cx, image, { draw_bg.mask: #(mask) @@ -548,7 +495,7 @@ impl MatchEvent for LoginScreen { Some(LoginAction::SsoSetRedirectUrl(url)) => { self.sso_redirect_url = Some(url.to_string()); } - _ => {} + _ => { } } } @@ -557,10 +504,7 @@ impl MatchEvent for LoginScreen { let login_status_modal_button = login_status_modal_inner.button_ref(cx); if login_status_modal_button.clicked(actions) { let request_id = id!(SSO_CANCEL_BUTTON); - let request = HttpRequest::new( - format!("{}/?login_token=", sso_redirect_url), - HttpMethod::GET, - ); + let request = HttpRequest::new(format!("{}/?login_token=",sso_redirect_url), HttpMethod::GET); cx.http_request(request_id, request); self.sso_redirect_url = None; } @@ -569,14 +513,15 @@ impl MatchEvent for LoginScreen { // Handle any of the SSO login buttons being clicked for (view_ref, brand) in self.view_set(cx, button_set).iter().zip(&provider_brands) { if view_ref.finger_up(actions).is_some() && !self.sso_pending { - submit_async_request(MatrixRequest::SpawnSSOServer { - identity_provider_id: format!("oidc-{}", brand), + submit_async_request(MatrixRequest::SpawnSSOServer{ + identity_provider_id: format!("oidc-{}",brand), brand: brand.to_string(), - homeserver_url: homeserver_input.text(), + homeserver_url: homeserver_input.text() }); } } } + } /// Actions sent to or from the login screen. @@ -587,7 +532,10 @@ pub enum LoginAction { /// A negative response from the backend Matrix task to the login screen. LoginFailure(String), /// A login-related status message to display to the user. - Status { title: String, status: String }, + Status { + title: String, + status: String, + }, /// The given login info was specified on the command line (CLI), /// and the login process is underway. CliAutoLogin { @@ -598,9 +546,9 @@ pub enum LoginAction { /// informing it that the SSO login process is either still in flight (`true`) or has finished (`false`). /// /// Note that an inner value of `false` does *not* imply that the login request has - /// successfully finished. + /// successfully finished. /// The login screen can use this to prevent the user from submitting - /// additional SSO login requests while a previous request is in flight. + /// additional SSO login requests while a previous request is in flight. SsoPending(bool), /// Set the SSO redirect URL in the LoginScreen. /// diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index 7e51cb35a..f7d09bdf8 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -6,11 +6,15 @@ use makepad_widgets::{log, warning, Cx}; use matrix_sdk::{ authentication::matrix::MatrixSession, ruma::{OwnedUserId, UserId}, - sliding_sync, Client, + sliding_sync, + Client, }; use serde::{Deserialize, Serialize}; -use crate::{app_data_dir, login::login_screen::LoginAction}; +use crate::{ + app_data_dir, + login::login_screen::LoginAction, +}; /// The data needed to re-build a client. #[derive(Clone, Serialize, Deserialize)] @@ -53,11 +57,11 @@ pub struct FullSessionPersisted { pub sync_token: Option, /// The sliding sync version to use for this client session. - /// + /// /// This determines the sync protocol used by the Matrix client: /// - `Native`: Uses the server's native sliding sync implementation for efficient syncing /// - `None`: Falls back to standard Matrix sync (without sliding sync optimizations) - /// + /// /// The value is restored and applied to the client via `client.set_sliding_sync_version()` /// when rebuilding the session from persistent storage. #[serde(default)] @@ -89,7 +93,9 @@ impl From for SlidingSyncVersion { } fn user_id_to_file_name(user_id: &UserId) -> String { - user_id.as_str().replace(":", "_").replace("@", "") + user_id.as_str() + .replace(":", "_") + .replace("@", "") } /// Returns the path to the persistent state directory for the given user. @@ -108,12 +114,14 @@ const LATEST_USER_ID_FILE_NAME: &str = "latest_user_id.txt"; /// Returns the user ID of the most recently-logged in user session. pub async fn most_recent_user_id() -> Option { - tokio::fs::read_to_string(app_data_dir().join(LATEST_USER_ID_FILE_NAME)) - .await - .ok()? - .trim() - .try_into() - .ok() + tokio::fs::read_to_string( + app_data_dir().join(LATEST_USER_ID_FILE_NAME) + ) + .await + .ok()? + .trim() + .try_into() + .ok() } /// Save which user was the most recently logged in. @@ -121,17 +129,17 @@ async fn save_latest_user_id(user_id: &UserId) -> anyhow::Result<()> { tokio::fs::write( app_data_dir().join(LATEST_USER_ID_FILE_NAME), user_id.as_str(), - ) - .await?; + ).await?; Ok(()) } + /// Restores the given user's previous session from the filesystem. /// /// If no User ID is specified, the ID of the most recently-logged in user /// is retrieved from the filesystem. pub async fn restore_session( - user_id: Option, + user_id: Option ) -> anyhow::Result<(Client, Option)> { let user_id = if let Some(user_id) = user_id { Some(user_id) @@ -157,12 +165,8 @@ pub async fn restore_session( // The session was serialized as JSON in a file. let serialized_session = tokio::fs::read_to_string(session_file).await?; - let FullSessionPersisted { - client_session, - user_session, - sync_token, - sliding_sync_version, - } = serde_json::from_str(&serialized_session)?; + let FullSessionPersisted { client_session, user_session, sync_token, sliding_sync_version } = + serde_json::from_str(&serialized_session)?; let status_str = format!( "Loaded session file for:\n{user_id}\n\nTrying to connect to homeserver...\n{}", @@ -185,10 +189,7 @@ pub async fn restore_session( .await?; let sliding_sync_version = sliding_sync_version.into(); client.set_sliding_sync_version(sliding_sync_version); - let status_str = format!( - "Authenticating previous login session for {}...", - user_session.meta.user_id - ); + let status_str = format!("Authenticating previous login session for {}...", user_session.meta.user_id); log!("{status_str}"); Cx::post_action(LoginAction::Status { title: "Authenticating session".into(), @@ -225,7 +226,7 @@ pub async fn save_session( client_session, user_session, sync_token: None, - sliding_sync_version, + sliding_sync_version })?; if let Some(parent) = session_file.parent() { tokio::fs::create_dir_all(parent).await?; @@ -237,17 +238,16 @@ pub async fn save_session( } /// Remove the LATEST_USER_ID_FILE_NAME file if it exists -/// +/// /// Returns: /// - Ok(true) if file was found and deleted /// - Ok(false) if file didn't exist /// - Err if deletion failed pub async fn delete_latest_user_id() -> anyhow::Result { let last_login_path = app_data_dir().join(LATEST_USER_ID_FILE_NAME); - + if last_login_path.exists() { - tokio::fs::remove_file(&last_login_path) - .await + tokio::fs::remove_file(&last_login_path).await .map_err(|e| anyhow::anyhow!("Failed to remove latest user file: {e}")) .map(|_| true) } else { diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index eaeddca78..d50dd7842 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,110 +8,43 @@ 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::{ - account::register::v3::Request as RegistrationRequest, - error::ErrorKind, - profile::{AvatarUrl, DisplayName}, - receipt::create_receipt::v3::ReceiptType, - uiaa::{AuthData, AuthType, Dummy}, - }, - }, - events::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ + api::{Direction, client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }}, 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, + 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, - }, + 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, + 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::{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::{ + 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, + }, 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)] @@ -159,8 +92,7 @@ impl From for Cli { Self { user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login - .homeserver + homeserver: login.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -175,8 +107,7 @@ impl From for Cli { Self { user_id: registration.user_id.trim().to_owned(), password: registration.password, - homeserver: registration - .homeserver + homeserver: registration.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -197,8 +128,7 @@ async fn finalize_authenticated_client( fallback_user_id: &str, ) -> Result<(Client, Option)> { if client.matrix_auth().logged_in() { - let logged_in_user_id = client - .user_id() + let logged_in_user_id = client.user_id() .map(ToString::to_string) .unwrap_or_else(|| fallback_user_id.to_owned()); log!("Logged in successfully."); @@ -215,9 +145,7 @@ async fn finalize_authenticated_client( "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.clone(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } } @@ -233,8 +161,7 @@ fn registration_localpart(user_id: &str) -> Result { } let localpart = trimmed.trim_start_matches('@'); - if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) - { + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { bail!("Please enter a valid username or full Matrix user ID."); } @@ -341,14 +268,9 @@ async fn reset_runtime_state_for_relogin() { 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(), - }); + Cx::post_action(LogoutAction::ClearAppState { on_clear_appstate: on_clear_appstate.clone() }); - if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()) - .await - .is_err() - { + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); } } @@ -360,6 +282,7 @@ fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { ) } + /// Build a new client. async fn build_client( cli: &Cli, @@ -382,13 +305,11 @@ async fn build_client( }; let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); - let homeserver_url = cli - .homeserver - .as_deref() + let homeserver_url = cli.homeserver.as_deref() .filter(|homeserver| !homeserver.trim().is_empty()) .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); - // .unwrap_or("https://matrix.org/"); + // .unwrap_or("https://matrix.org/"); let mut builder = Client::builder() .server_name_or_homeserver_url(homeserver_url) @@ -416,11 +337,13 @@ async fn build_client( // 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))); + 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(); + let homeserver_url = client.homeserver().to_string(); Ok(( client, ClientSessionPersisted { @@ -436,7 +359,10 @@ async fn build_client( /// 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)> { +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 { @@ -459,9 +385,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option if !client.matrix_auth().logged_in() { 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(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } finalize_authenticated_client(client, client_session, &cli.user_id).await @@ -517,9 +441,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option register_result.user_id, ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.clone(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } @@ -539,6 +461,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option } } + /// Which direction to paginate in. /// /// * `Forwards` will retrieve later events (towards the end of the timeline), @@ -607,6 +530,7 @@ pub type OnLinkPreviewFetchedFn = fn( Option>, ); + /// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. #[derive(Clone, Debug)] pub enum MatrixLinkAction { @@ -637,7 +561,9 @@ pub enum DirectMessageRoomAction { 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 }, + DidNotExist { + user_profile: UserProfile, + }, /// A direct message room didn't exist, but we successfully created a new one. NewlyCreated { user_profile: UserProfile, @@ -672,10 +598,7 @@ impl TimelineKind { 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), + TimelineKind::Thread { thread_root_event_id, .. } => Some(thread_root_event_id), } } } @@ -683,10 +606,7 @@ 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, - } => { + TimelineKind::Thread { room_id, thread_root_event_id } => { write!(f, "Thread({}, {})", room_id, thread_root_event_id) } } @@ -699,7 +619,9 @@ pub enum MatrixRequest { /// Request from the login screen to log in with the given credentials. Login(LoginRequest), /// Request to logout. - Logout { is_desktop: bool }, + Logout { + is_desktop: bool, + }, /// Request to paginate the older (or newer) events of a room or thread timeline. PaginateTimeline { timeline_kind: TimelineKind, @@ -731,7 +653,9 @@ pub enum MatrixRequest { /// /// 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 }, + 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, @@ -750,9 +674,13 @@ pub enum MatrixRequest { user_id: OwnedUserId, }, /// Request to join the given room. - JoinRoom { room_id: OwnedRoomId }, + JoinRoom { + room_id: OwnedRoomId, + }, /// Request to leave the given room. - LeaveRoom { room_id: OwnedRoomId }, + 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. @@ -775,7 +703,9 @@ pub enum MatrixRequest { via: Vec, }, /// Request to fetch the full details (the room preview) of a tombstoned room. - GetSuccessorRoomDetails { tombstoned_room_id: OwnedRoomId }, + 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 @@ -800,7 +730,9 @@ pub enum MatrixRequest { local_only: bool, }, /// Request to fetch the number of unread messages in the given room. - GetNumberUnreadMessages { timeline_kind: TimelineKind }, + GetNumberUnreadMessages { + timeline_kind: TimelineKind, + }, /// Request to set the unread flag for the given room. SetUnreadFlag { room_id: OwnedRoomId, @@ -885,12 +817,15 @@ pub enum MatrixRequest { /// 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 }, + 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 { + SpawnSSOServer{ brand: String, homeserver_url: String, identity_provider_id: String, @@ -935,7 +870,9 @@ pub enum MatrixRequest { /// /// 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 }, + GetRoomPowerLevels { + timeline_kind: TimelineKind, + }, /// Toggles the given reaction to the given event in the given room. ToggleReaction { timeline_kind: TimelineKind, @@ -961,7 +898,7 @@ pub enum MatrixRequest { /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. GetMatrixRoomLinkPillInfo { matrix_id: MatrixId, - via: Vec, + via: Vec }, /// Request to fetch URL preview from the Matrix homeserver. GetUrlPreview { @@ -975,19 +912,19 @@ pub enum MatrixRequest { /// 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) + 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 { +pub enum LoginRequest{ LoginByPassword(LoginByPassword), Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), + } /// Information needed to log in to a Matrix homeserver. pub struct LoginByPassword { @@ -1004,6 +941,7 @@ pub struct RegisterAccount { 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 @@ -1014,8 +952,7 @@ async fn matrix_worker_task( ) -> 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(); + 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(); @@ -1025,7 +962,7 @@ async fn matrix_worker_task( 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.", + "BUG: failed to send login request to login worker task." ))); } } @@ -1038,7 +975,7 @@ async fn matrix_worker_task( match logout_with_state_machine(is_desktop).await { Ok(()) => { log!("Logout completed successfully via state machine"); - } + }, Err(e) => { error!("Logout failed: {e:?}"); } @@ -1046,11 +983,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::PaginateTimeline { - timeline_kind, - num_events, - direction, - } => { + 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; @@ -1092,11 +1025,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::EditMessage { - timeline_kind, - timeline_event_item_id, - edited_content, - } => { + 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; @@ -1118,10 +1047,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchDetailsForEvent { - timeline_kind, - event_id, - } => { + 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; @@ -1138,10 +1064,7 @@ async fn matrix_worker_task( // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); } } - if sender - .send(TimelineUpdate::EventDetailsFetched { event_id, result }) - .is_err() - { + 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(); @@ -1195,27 +1118,17 @@ async fn matrix_worker_task( }); } - MatrixRequest::CreateThreadTimeline { - room_id, - thread_root_event_id, - } => { + 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}" - ); + 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) - { + 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()); + let newly_pending = room_info.pending_thread_timelines.insert(thread_root_event_id.clone()); if !newly_pending { continue; } @@ -1287,18 +1200,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::Knock { - room_or_alias_id, - reason, - server_names, - } => { + 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 - { + 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 { @@ -1322,21 +1228,23 @@ async fn matrix_worker_task( 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 }), + Ok(_) => Cx::post_action(InviteResultAction::Sent { + room_id, + user_id, + }), Err(error) => Cx::post_action(InviteResultAction::Failed { room_id, user_id, error, }), } - } else { + } + 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(), - ), + error: matrix_sdk::Error::UnknownError("Room/Space not found in client's known list.".into()), }) } }); @@ -1357,7 +1265,8 @@ async fn matrix_worker_task( JoinRoomResultAction::Failed { room_id, error: e } } } - } else { + } + else { match client.join_room_by_id(&room_id).await { Ok(_room) => { log!("Successfully joined new unknown room {room_id}."); @@ -1392,20 +1301,14 @@ async fn matrix_worker_task( 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(), - ), + 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, - } => { + 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; @@ -1414,9 +1317,7 @@ async fn matrix_worker_task( 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(); + sender.send(TimelineUpdate::RoomMembersListFetched { members }).unwrap(); SignalToUI::set_ui_signal(); }; @@ -1433,10 +1334,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetRoomPreview { - room_or_alias_id, - via, - } => { + 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; @@ -1449,9 +1347,7 @@ async fn matrix_worker_task( 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" - ); + error!("BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request"); continue; }; ( @@ -1467,10 +1363,7 @@ async fn matrix_worker_task( ); } - MatrixRequest::OpenOrCreateDirectMessage { - user_profile, - allow_create, - } => { + 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) { @@ -1493,7 +1386,7 @@ async fn matrix_worker_task( 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 { @@ -1505,11 +1398,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUserProfile { - user_id, - room_id, - local_only, - } => { + 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}, \ @@ -1603,10 +1492,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetUnreadFlag { - room_id, - mark_as_unread, - } => { + 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; @@ -1615,64 +1501,35 @@ async fn matrix_worker_task( 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 - ), + Err(e) => error!("Failed to set unread flag to {} for room {}: {:?}", mark_as_unread, room_id, e), } }); } - MatrixRequest::SetIsFavorite { - room_id, - is_favorite, - } => { + 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}" - ); + 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; + 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 - ), + Err(e) => error!("Failed to set favorite to {} for room {}: {:?}", is_favorite, room_id, e), } }); } - MatrixRequest::SetIsLowPriority { - room_id, - is_low_priority, - } => { + 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}" - ); + 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; + 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 - ), + 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), } }); } @@ -1681,24 +1538,15 @@ async fn matrix_worker_task( 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" } - ); + 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" } - ); + 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" } - ); + let err_msg = format!("Failed to {} avatar: {e}", if is_removing { "remove" } else { "set" }); Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); } } @@ -1709,87 +1557,57 @@ async fn matrix_worker_task( 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{}...", + 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() + 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; + 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, - )); + 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" } - ); + 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, - } => { + 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 + room.matrix_event_permalink(event_id).await .map(MatrixLinkAction::MatrixUri) } else { - room.matrix_permalink(join_on_click) - .await + 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 + room.matrix_to_event_permalink(event_id).await .map(MatrixLinkAction::MatrixToUri) } else { - room.matrix_to_permalink() - .await + 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" - ))); + Cx::post_action(MatrixLinkAction::Error(format!("Room {room_id} not found"))); } }); } - MatrixRequest::IgnoreUser { - ignore, - room_member, - room_id, - } => { + 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(); @@ -1844,9 +1662,7 @@ async fn matrix_worker_task( 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}" - ); + log!("BUG: skipping send typing notice request for not-yet-known room {room_id}"); continue; }; let _typing_task = Handle::current().spawn(async move { @@ -1860,21 +1676,16 @@ async fn matrix_worker_task( 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}" - ); + 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." - ); + 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(); + let (drop_guard, receiver) = main_timeline.room().subscribe_to_typing_notifications(); jrd.typing_notice_subscriber = Some(drop_guard); (main_timeline, receiver) } @@ -1883,11 +1694,7 @@ async fn matrix_worker_task( 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, - ) + (main_timeline, jrd.main_timeline.timeline_update_sender.clone(), receiver) }; let _typing_notices_task = Handle::current().spawn(async move { @@ -1914,22 +1721,15 @@ async fn matrix_worker_task( }); } - MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { - timeline_kind, - subscribe, - } => { + MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { if !subscribe { - if let Some(task_handler) = - subscribers_own_user_read_receipts.remove(&timeline_kind) - { + 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}" - ); + log!("BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}"); continue; }; @@ -1971,8 +1771,7 @@ async fn matrix_worker_task( } } }); - subscribers_own_user_read_receipts - .insert(timeline_kind_clone, subscribe_own_read_receipt_task); + subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); } MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { @@ -1982,13 +1781,9 @@ async fn matrix_worker_task( } continue; } - let kind = TimelineKind::MainRoom { - room_id: room_id.clone(), - }; + 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}" - ); + log!("BUG: skipping subscribe to pinned events request for unknown room {room_id}"); continue; }; let subscribe_pinned_events_task = Handle::current().spawn(async move { @@ -2010,18 +1805,8 @@ async fn matrix_worker_task( 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::SpawnSSOServer { brand, homeserver_url, identity_provider_id} => { + spawn_sso_server(brand, homeserver_url, identity_provider_id, login_sender.clone()).await; } MatrixRequest::ResolveRoomAlias(room_alias) => { @@ -2034,10 +1819,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchAvatar { - mxc_uri, - on_fetched, - } => { + 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:?}..."); @@ -2047,21 +1829,13 @@ async fn matrix_worker_task( }; 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()), - }); + on_fetched(AvatarUpdate { mxc_uri, avatar_data: res.map(|v| v.into()) }); }); } - MatrixRequest::FetchMedia { - media_request, - on_fetched, - destination, - update_sender, - } => { + 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; @@ -2166,11 +1940,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::ReadReceipt { - timeline_kind, - event_id, - receipt_type, - } => { + 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; @@ -2191,7 +1961,7 @@ async fn matrix_worker_task( }); } }); - } + }, MatrixRequest::GetRoomPowerLevels { timeline_kind } => { let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { @@ -2199,21 +1969,15 @@ async fn matrix_worker_task( continue; }; - let Some(user_id) = current_user_id() else { - 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() - { + 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(); @@ -2223,13 +1987,9 @@ async fn matrix_worker_task( } } }); - } + }, - MatrixRequest::ToggleReaction { - timeline_kind, - timeline_event_id, - reaction, - } => { + 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; @@ -2237,26 +1997,17 @@ async fn matrix_worker_task( 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 - { + 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:?}" - ), + }, + Err(_e) => error!("Failed to send toggle reaction to {timeline_kind}; error: {_e:?}"), } }); - } + }, - MatrixRequest::RedactMessage { - timeline_kind, - timeline_event_id, - reason, - } => { + 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; @@ -2275,13 +2026,9 @@ async fn matrix_worker_task( } } }); - } + }, - MatrixRequest::PinEvent { - timeline_kind, - event_id, - pin, - } => { + 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; @@ -2293,11 +2040,7 @@ async fn matrix_worker_task( } else { timeline.unpin_event(&event_id).await }; - match sender.send(TimelineUpdate::PinResult { - event_id, - pin, - result, - }) { + match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { Ok(_) => SignalToUI::set_ui_signal(), Err(_) => log!("Failed to send UI update for pin event."), } @@ -2331,12 +2074,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUrlPreview { - url, - on_fetched, - destination, - update_sender, - } => { + 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 { @@ -2346,19 +2084,17 @@ async fn matrix_worker_task( // 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") + 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()) @@ -2371,20 +2107,20 @@ async fn matrix_worker_task( // 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]); @@ -2393,25 +2129,22 @@ async fn matrix_worker_task( // } // 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) - }); + 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 { + 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) => { @@ -2431,12 +2164,11 @@ async fn matrix_worker_task( // error!("Response body that failed to parse: {}", text); UrlPreviewError::Json(e) }) - } - .await; + }.await; // match &result { // Ok(preview_data) => { - // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", + // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", // url, preview_data.title, preview_data.site_name); // } // Err(e) => { @@ -2455,6 +2187,7 @@ async fn matrix_worker_task( 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); @@ -2467,8 +2200,7 @@ static REQUEST_SENDER: Mutex>> = Mutex::ne 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())); +static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = LazyLock::new(|| Arc::new(Notify::new())); /// Blocks the current thread until the given future completes. /// @@ -2479,45 +2211,36 @@ 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(); + 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 }) + 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(); + 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() + DEFAULT_SSO_CLIENT.lock().unwrap() .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2534,6 +2257,7 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2600,13 +2324,13 @@ impl Drop for JoinedRoomDetails { } } + /// 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())); +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>( @@ -2616,10 +2340,7 @@ fn get_per_timeline_details<'a>( 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), + TimelineKind::Thread { thread_root_event_id, .. } => room_info.thread_timelines.get_mut(thread_root_event_id), } } @@ -2630,22 +2351,14 @@ fn get_timeline(kind: &TimelineKind) -> Option> { } /// 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(), - ) - }) +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() + ALL_JOINED_ROOMS.lock().unwrap() .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2659,16 +2372,15 @@ pub fn get_client() -> Option { /// 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())) + 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() @@ -2677,8 +2389,7 @@ pub fn get_sync_service() -> Option> { /// 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())); +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 { @@ -2690,6 +2401,7 @@ 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. @@ -2703,10 +2415,7 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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)?, + 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 { @@ -2719,18 +2428,25 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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() - }) +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 @@ -2758,14 +2474,18 @@ struct RoomListServiceRoomInfo { 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 { + 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(), @@ -2807,26 +2527,26 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() + 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? {}", + 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")); + 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: Option<(Client, Option, bool)> = 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: {:?}", + 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.clone()).await { @@ -2843,10 +2563,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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 - ); + 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(), @@ -2855,9 +2572,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok((client, sync_token)) => Some((client, sync_token, false)), 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}" - ))); + 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:?}"), }); @@ -2882,30 +2599,34 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let (client, sync_service, logged_in_user_id) = 'login_loop: loop { let (client, _sync_token, validate_session) = 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, false), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); + 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, false), + 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: format!("Login failed: {e}"), + status: err, }); + return; } - }, - 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; } } - }, + } }; if validate_session { @@ -2913,8 +2634,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok(_) => {} Err(e) if is_invalid_token_http_error(&e) => { clear_persisted_session(client.user_id()).await; - let err_msg = - "Your login token is no longer valid.\n\nPlease log in again."; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.to_string(), @@ -2922,9 +2642,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { continue 'login_loop; } Err(e) => { - warning!( - "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" - ); + warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); } } } @@ -2934,8 +2652,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let _ = client_opt.take(); } - let logged_in_user_id: OwnedUserId = client - .user_id() + 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); @@ -2943,9 +2660,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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." - ); + error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); } // Listen for changes to our verification status and incoming verification requests. @@ -2969,9 +2684,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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}." - ) + format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") }; if is_invalid_token_error(&e) { clear_persisted_session(client.user_id()).await; @@ -3009,9 +2722,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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." - ); + 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)); @@ -3128,6 +2839,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } + /// 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?; @@ -3141,13 +2853,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()), - ]))); + 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(); @@ -3163,13 +2875,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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() - ); - } + 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); @@ -3180,35 +2886,20 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 - ); - } + 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 - ); + 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; + }) + ).await; // Send room order update with the new room IDs let (room_id_refs, room_ids) = { @@ -3222,57 +2913,43 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu }; if !room_ids.is_empty() { enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Append { values: room_ids }, + 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"); - } + 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; + 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 }, + 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; + 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 }, + 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 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, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopFront)); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3280,18 +2957,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).await?; } } remove_diff @ VectorDiff::PopBack => { - if LOG_ROOM_LIST_DIFFS { - log!("room_list: diff 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, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopBack)); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3299,61 +2971,38 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).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; + 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, - })); + 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; + 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(), - })); + 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 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 }, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Remove { index })); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3361,19 +3010,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).await?; } else { - error!( - "BUG: room_list: diff Remove index {index} out of bounds, len {}", - all_known_rooms.len() - ); + 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}"); - } + 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() { @@ -3382,7 +3025,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } all_known_rooms.truncate(length); // sanity check enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Truncate { length }, + VecDiff::Truncate { length } )); } } @@ -3392,6 +3035,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 @@ -3411,58 +3055,48 @@ async fn optimize_remove_then_add_into_update( ) -> 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() => { + 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 - ); + 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; + 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(), - })); + 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() => { + 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 - ); + 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; + 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(), - })); + 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() => { + 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 - ); + 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; + 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(), - })); + 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; } @@ -3476,6 +3110,7 @@ async fn optimize_remove_then_add_into_update( Ok(()) } + /// Invoked when the room list service has received an update that changes an existing room. async fn update_room( old_room: &RoomListServiceRoomInfo, @@ -3486,29 +3121,18 @@ async fn update_room( 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 - ); + 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 - ); + 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 - ); + 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 @@ -3518,17 +3142,11 @@ async fn update_room( return Ok(()); } RoomState::Joined => { - log!( - "update_room(): adding new Joined room: {:?} ({new_room_id})", - new_room.display_name - ); + 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 - ); + 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 => { @@ -3546,12 +3164,7 @@ async fn update_room( 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 - ); + 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(), @@ -3561,15 +3174,12 @@ async fn update_room( // 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) { + 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(), - ) { + 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, @@ -3578,13 +3188,9 @@ async fn update_room( 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 - ); + 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(), @@ -3595,15 +3201,11 @@ async fn update_room( || 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 {} --> {}", + 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, + 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(), @@ -3614,8 +3216,7 @@ async fn update_room( } if old_room.is_direct != new_room.is_direct { - log!( - "Updating room {} is_direct from {} to {}", + log!("Updating room {} is_direct from {} to {}", new_room_id, old_room.is_direct, new_room.is_direct, @@ -3630,8 +3231,7 @@ async fn update_room( 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 = Some(jrd.main_timeline.timeline_update_sender.clone()); } } __timeline_update_sender_opt.clone() @@ -3640,9 +3240,7 @@ async fn update_room( 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(), - }); + 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(), @@ -3651,9 +3249,7 @@ async fn update_room( timeline_update_sender, ); } else { - error!( - "BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}" - ); + error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); } } @@ -3664,38 +3260,37 @@ async fn update_room( 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}" - ), + 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." - ); + 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, + } + 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, - }); + 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, @@ -3704,39 +3299,26 @@ async fn add_new_room( ) -> Result<()> { match new_room.state { RoomState::Knocked => { - log!( - "Got new Knocked room: {:?} ({})", - new_room.display_name, - new_room.room_id - ); + 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 - ); + 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 - ); + 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())); + 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) { @@ -3753,20 +3335,18 @@ async fn add_new_room( } 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, - }, - )); + 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, @@ -3774,21 +3354,17 @@ async fn add_new_room( spawn_fetch_room_avatar(new_room); return Ok(()); } - RoomState::Joined => {} // Fall through to adding the joined room below. + 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; + room_list_service.subscribe_to_rooms(&[&new_room.room_id]).await; } let timeline = Arc::new( - new_room - .room - .timeline_builder() + new_room.room.timeline_builder() .with_focus(TimelineFocus::Live { // we show threads as separate timelines in their own RoomScreen hide_threaded_events: true, @@ -3796,12 +3372,7 @@ async fn add_new_room( .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 - ) - })?, + .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(); @@ -3817,11 +3388,7 @@ async fn add_new_room( // 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 - ); + 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 { @@ -3842,8 +3409,7 @@ async fn add_new_room( let latest = get_latest_event_details( &new_room.room.latest_event().await, room_list_service.client(), - ) - .await; + ).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()); @@ -3874,8 +3440,7 @@ async fn add_new_room( #[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() + let ignored_users = client.account() .account_data::() .await .ok()?? @@ -3939,9 +3504,7 @@ fn handle_load_app_state(user_id: OwnedUserId) { && !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, - )); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(app_state)); } } Err(_e) => { @@ -3960,12 +3523,12 @@ fn handle_load_app_state(user_id: OwnedUserId) { 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 - } + 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!( @@ -4047,12 +3610,14 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { 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); - + 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); + 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 { @@ -4065,10 +3630,7 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { } fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { - log!( - "Initial room list loading state is {:?}", - loading_state.get() - ); + 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:?}"); @@ -4076,12 +3638,8 @@ fn handle_room_list_service_loading_state(mut loading_state: Subscriber { enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); } - RoomListLoadingState::Loaded { - maximum_number_of_rooms, - } => { - enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { - max_rooms: maximum_number_of_rooms, - }); + 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; @@ -4104,12 +3662,12 @@ fn spawn_fetch_successor_room_preview( 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, - }, + 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 }) @@ -4143,18 +3701,12 @@ async fn fetch_room_preview_with_avatar( }; 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 - ); + 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 + 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()) } @@ -4174,10 +3726,7 @@ async fn fetch_room_preview_with_avatar( async fn fetch_thread_summary_details( room: &Room, thread_root_event_id: &EventId, -) -> ( - u32, - Option, -) { +) -> (u32, Option) { let mut num_replies = 0; let mut latest_reply_event = None; @@ -4235,7 +3784,10 @@ async fn fetch_latest_thread_reply_event( } /// 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 { +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; @@ -4248,10 +3800,7 @@ async fn count_thread_replies(room: &Room, thread_root_event_id: &EventId) -> Op ..Default::default() }; - let relations = room - .relations(thread_root_event_id.to_owned(), options) - .await - .ok()?; + let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; if relations.chunk.is_empty() { break; } @@ -4277,8 +3826,7 @@ async fn text_preview_of_latest_thread_reply( Ok(Some(rm)) => Some(rm), _ => room.get_member(&sender_id).await.ok().flatten(), }; - let sender_name = sender_room_member - .as_ref() + 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(|| { @@ -4295,6 +3843,7 @@ async fn text_preview_of_latest_thread_reply( } } + /// 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 @@ -4318,37 +3867,29 @@ async fn get_latest_event_details( match latest_event_value { LatestEventValue::None => None, - LatestEventValue::Remote { - timestamp, - sender, - is_own, - profile, - content, - } => { + 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); + 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: _, - } => { + 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); + 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. @@ -4356,9 +3897,10 @@ async fn get_latest_event_details( /// 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 - { + 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, @@ -4395,6 +3937,7 @@ async fn timeline_subscriber_handler( 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, @@ -4403,13 +3946,14 @@ async fn timeline_subscriber_handler( 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 + 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)) @@ -4418,13 +3962,11 @@ async fn timeline_subscriber_handler( } } + 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() - ); + 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(), @@ -4437,266 +3979,262 @@ async fn timeline_subscriber_handler( // 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)) - ); + 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()); + 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, + // 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, } - ).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, - }); - } + } 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(); + // 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)); } - 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; + 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. } - VectorDiff::PopFront => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopFront"); } + // 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; - 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()); + } else { + index_of_first_change = min(index_of_first_change, index); 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 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)); + 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; } - - 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}"); } + } else { + found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) + .map(|(i, ev)| (i + index, ev)); } - 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}"); } + + 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; } - 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; + 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.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; + 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 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; + } - 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:?}!") - ); - } + let changed_indices = index_of_first_change..index_of_last_change; - // Send a Makepad-level signal to update this room's timeline UI view. - SignalToUI::set_ui_signal(); + 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:?}!") + ); } - } - else => { - break; + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); } } - } - error!( - "Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}." - ); + 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. @@ -4721,13 +4259,8 @@ async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvata _ => { 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 - { + 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()); } } @@ -4756,8 +4289,7 @@ async fn spawn_sso_server( // 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(), + status: "Please wait while Matrix builds and configures the client object for login.".into(), }); // Wait for the notification that the client has been built @@ -4778,21 +4310,19 @@ async fn spawn_sso_server( // 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() + 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/")) - { + && 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 - { + ).await { Ok(success) => client_and_session = Some(success), Err(e) => build_client_error = Some(e), } @@ -4801,12 +4331,10 @@ async fn spawn_sso_server( 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}" - ) + 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. @@ -4818,8 +4346,7 @@ async fn spawn_sso_server( 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(), + status: "Please finish logging in using your browser, and then come back to Robrix.".into(), }); match client .matrix_auth() @@ -4829,15 +4356,12 @@ async fn spawn_sso_server( if key == "redirectUrl" { let redirect_url = Url::parse(&value)?; Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); - break; + break } } - Uri::new(&sso_url).open().map_err(|err| { - Error::Io(io::Error::other(format!( - "Unable to open SSO login url. Error: {:?}", - err - ))) - }) + 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}")) @@ -4852,13 +4376,10 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - if let Err(e) = login_sender - .send(LoginRequest::LoginBySSOSuccess(client, client_session)) - .await - { + 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.", + "BUG: failed to send login request to matrix worker thread." ))); } enqueue_rooms_list_update(RoomsListUpdate::Status { @@ -4884,6 +4405,7 @@ async fn spawn_sso_server( }); } + bitflags! { /// The powers that a user has in a given room. #[derive(Copy, Clone, PartialEq, Eq)] @@ -4961,38 +4483,14 @@ impl UserPowerLevels { 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.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 } @@ -5038,7 +4536,8 @@ impl UserPowerLevels { } pub fn can_send_message(self) -> bool { - self.contains(UserPowerLevels::RoomMessage) || self.contains(UserPowerLevels::Message) + self.contains(UserPowerLevels::RoomMessage) + || self.contains(UserPowerLevels::Message) } pub fn can_send_reaction(self) -> bool { @@ -5055,6 +4554,7 @@ impl UserPowerLevels { } } + /// 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() { @@ -5072,16 +4572,9 @@ pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { 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 - { + 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(()) From b9b0d780633dcc15ad32465ef57e0c9fafa4b219 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 25 Mar 2026 15:22:03 +0800 Subject: [PATCH 023/283] refactor: migrate streaming detection from heuristic to MSC4357 Replace prefix-match + recency + not-self heuristic with deterministic MSC4357 `org.matrix.msc4357.live` field detection. This simplifies the detection path and makes streaming animation reliable for any compliant server. Key changes: - StreamingAnimState: replace sender_user_id/detection/sender_stopped_typing with is_live bool; add restore() for timeline reset preservation - room_screen: add is_msc4357_live() helper, streaming_scan_range() for bounded detection, remove heuristic detection and typing-latch logic - sliding_sync: simplify TypingUsers back to Vec - Split timeouts: 30s for finished streams, 5min for live streams --- src/home/room_screen.rs | 219 ++++++++++++++++---------------- src/home/streaming_animation.rs | 159 +++++++++++++---------- src/sliding_sync.rs | 2 +- 3 files changed, 198 insertions(+), 182 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 8c0827e66..45cf67566 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -54,8 +54,6 @@ const MAX_ITEMS_TO_SEARCH_THROUGH: usize = 100; /// The max size (width or height) of a blurhash image to decode. const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; -const STREAMING_IDLE_TIMEOUT: Duration = Duration::from_secs(30); - static UNNAMED_ROOM: &str = "Unnamed Room"; /// #FFF4E5 @@ -63,14 +61,24 @@ const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); /// #FFEACC const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); -fn timeline_item_event_id(item: &Arc) -> Option<&EventId> { +fn item_event_id(item: &Arc) -> Option<&EventId> { let TimelineItemKind::Event(event) = item.kind() else { return None; }; event.event_id() } -fn bounded_changed_indices( +/// Check if an event carries the MSC4357 `org.matrix.msc4357.live` field, +/// indicating that the message content is still being streamed. +fn is_msc4357_live(event_tl_item: &EventTimelineItem) -> bool { + event_tl_item.latest_json() + .and_then(|raw| raw.get_field::("content").ok()) + .flatten() + .and_then(|content| content.get("org.matrix.msc4357.live").cloned()) + .is_some() +} + +fn clamp_indices( changed_indices: &Range, old_len: usize, new_len: usize, @@ -80,7 +88,20 @@ fn bounded_changed_indices( start..end } -fn refresh_streaming_message_indices<'a, I>( +fn streaming_scan_range( + clear_cache: bool, + changed_indices: &Range, + old_len: usize, + new_len: usize, +) -> Range { + if clear_cache { + 0..new_len + } else { + clamp_indices(changed_indices, old_len, new_len) + } +} + +fn refresh_stream_indices<'a, I>( event_ids: I, streaming_messages: &mut HashMap, ) @@ -101,13 +122,12 @@ where } } -fn next_streaming_timeout_duration<'a>( +fn next_stream_timeout<'a>( states: impl IntoIterator, - idle_timeout: Duration, ) -> Option { states .into_iter() - .map(|state| idle_timeout.saturating_sub(state.last_update_time.elapsed())) + .map(|state| state.timeout_after().saturating_sub(state.last_update_time.elapsed())) .min() } @@ -773,7 +793,7 @@ impl Widget for RoomScreen { } } - self.schedule_streaming_timeout_if_needed(cx); + self.schedule_stream_timeout(cx); } if self.streaming_timeout_timer.is_event(event).is_some() { @@ -799,7 +819,7 @@ impl Widget for RoomScreen { } } - self.schedule_streaming_timeout_if_needed(cx); + self.schedule_stream_timeout(cx); } // Handle actions here before processing timeline updates. @@ -1383,24 +1403,19 @@ impl RoomScreen { } /// Extract the text body from a timeline item, if it's a text message. - fn extract_message_text_from_item(item: &Arc) -> Option { + fn extract_message_text(item: &Arc) -> Option { let TimelineItemKind::Event(event) = item.kind() else { return None }; - let TimelineItemContent::MsgLike(msg_like) = event.content() else { return None }; - let MsgLikeKind::Message(msg) = &msg_like.kind else { return None }; - match msg.msgtype() { - MessageType::Text(text) => Some(text.body.clone()), - _ => None, - } + let TimelineItemContent::MsgLike(_) = event.content() else { return None }; + Some(plaintext_body_of_timeline_item(event)) } - fn schedule_streaming_timeout_if_needed(&mut self, cx: &mut Cx) { + fn schedule_stream_timeout(&mut self, cx: &mut Cx) { cx.stop_timer(self.streaming_timeout_timer); - self.streaming_timeout_timer = next_streaming_timeout_duration( + self.streaming_timeout_timer = next_stream_timeout( self.tl_state .as_ref() .into_iter() .flat_map(|tl| tl.streaming_messages.values()), - STREAMING_IDLE_TIMEOUT, ) .map(|duration| cx.start_timeout(duration.as_secs_f64())) .unwrap_or_else(Timer::empty); @@ -1433,8 +1448,8 @@ impl RoomScreen { jump_to_bottom_button.update_visibility(cx, true); tl.items = initial_items; - refresh_streaming_message_indices( - tl.items.iter().map(timeline_item_event_id), + refresh_stream_indices( + tl.items.iter().map(item_event_id), &mut tl.streaming_messages, ); done_loading = true; @@ -1555,77 +1570,64 @@ impl RoomScreen { // log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update); } - // --- Streaming detection --- - // Clear streaming state on timeline clear - if clear_cache { - tl.streaming_messages.clear(); - } + // --- MSC4357 streaming detection --- + let previous_streaming_messages = + clear_cache.then(|| std::mem::take(&mut tl.streaming_messages)); - // Compare old and new text for changed items to detect streaming - if !new_items.is_empty() && !changed_indices.is_empty() { - let current_uid = crate::sliding_sync::current_user_id(); - let changed_indices = - bounded_changed_indices(&changed_indices, tl.items.len(), new_items.len()); - - for idx in changed_indices { - let Some(old_item) = tl.items.get(idx) else { continue }; - let Some(new_item) = new_items.get(idx) else { continue }; - if timeline_item_event_id(old_item) != timeline_item_event_id(new_item) { - continue; - } + if !new_items.is_empty() { + use crate::home::streaming_animation::StreamingAnimState; - let Some(old_text) = Self::extract_message_text_from_item(old_item) else { continue }; - let Some(new_text) = Self::extract_message_text_from_item(new_item) else { continue }; - if old_text == new_text { continue; } + let mut should_schedule_frame = false; + let scan_range = streaming_scan_range( + clear_cache, + &changed_indices, + tl.items.len(), + new_items.len(), + ); - // Get event_id and sender from new item + for idx in scan_range { + let Some(new_item) = new_items.get(idx) else { continue }; let TimelineItemKind::Event(new_evt) = new_item.kind() else { continue }; let Some(event_id) = new_evt.event_id().map(|id| id.to_owned()) else { continue }; - let sender = new_evt.sender().to_owned(); + let live = is_msc4357_live(new_evt); + let Some(new_text) = Self::extract_message_text(new_item) else { continue }; - // If already tracking: just update target text if let Some(state) = tl.streaming_messages.get_mut(&event_id) { - state.update_target(&new_text); - self.streaming_next_frame = cx.new_next_frame(); + state.update_target(&new_text, live); + should_schedule_frame |= state.needs_frame(); continue; } - // Heuristic detection: prefix extension + recency + not self - let is_prefix_extension = new_text.len() > old_text.len() - && new_text.starts_with(&old_text); - - let is_recent = { - let ts = new_evt.timestamp(); - let now_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - now_ms.saturating_sub(ts.0.into()) < 60_000 - }; - - let is_not_self = current_uid.as_ref() - .is_some_and(|uid| *uid != sender); - - if is_prefix_extension && is_recent && is_not_self { - use crate::home::streaming_animation::*; - tl.streaming_messages.insert( - event_id, - StreamingAnimState::new_from_visible_prefix( - &old_text, - &new_text, - sender, - StreamDetection::Heuristic, - ), - ); - self.streaming_next_frame = cx.new_next_frame(); + if let Some(previous_state) = previous_streaming_messages + .as_ref() + .and_then(|states| states.get(&event_id)) + { + let restored = + StreamingAnimState::restore(previous_state, &new_text, live); + let should_track = live || restored.needs_frame(); + should_schedule_frame |= restored.needs_frame(); + if should_track { + tl.streaming_messages.insert(event_id, restored); + } + continue; } + + if live { + let state = StreamingAnimState::new(&new_text, true); + should_schedule_frame |= state.needs_frame(); + tl.streaming_messages.insert(event_id, state); + } + } + + if should_schedule_frame { + self.streaming_next_frame = cx.new_next_frame(); } } // --- End streaming detection --- tl.items = new_items; - refresh_streaming_message_indices( - tl.items.iter().map(timeline_item_event_id), + refresh_stream_indices( + tl.items.iter().map(item_event_id), &mut tl.streaming_messages, ); done_loading = true; @@ -1801,20 +1803,7 @@ impl RoomScreen { // update loop has completed, which avoids unnecessary expensive work // if the list of typing users gets updated many times in a row. - // Update streaming sender_stopped_typing latch - { - let typing_user_ids: HashSet<&OwnedUserId> = - users.iter().map(|(uid, _)| uid).collect(); - for state in tl.streaming_messages.values_mut() { - state.sender_stopped_typing = - !typing_user_ids.contains(&state.sender_user_id); - } - if !tl.streaming_messages.is_empty() { - self.streaming_next_frame = cx.new_next_frame(); - } - } - // Extract display names for the typing notice widget - typing_users = Some(users.iter().map(|(_, name)| name.clone()).collect::>()); + typing_users = Some(users); } TimelineUpdate::PinnedEvents(pinned_events) => { self.pinned_events = pinned_events; @@ -1870,7 +1859,7 @@ impl RoomScreen { } if num_updates > 0 { - self.schedule_streaming_timeout_if_needed(cx); + self.schedule_stream_timeout(cx); // log!("Applied {} timeline updates for room {}, redrawing with {} items...", num_updates, tl.kind.room_id(), tl.items.len()); self.redraw(cx); } @@ -2608,7 +2597,7 @@ impl RoomScreen { // Store the tl_state for this room into this RoomScreen widget, // such that it can be accessed in future functions like event/draw handlers. self.tl_state = Some(tl_state); - self.schedule_streaming_timeout_if_needed(cx); + self.schedule_stream_timeout(cx); // Now that we have restored the TimelineUiState into this RoomScreen widget, // we can proceed to processing pending background updates. @@ -2708,8 +2697,8 @@ impl RoomScreen { tl_state.tombstone_info.as_ref(), ); - refresh_streaming_message_indices( - tl_state.items.iter().map(timeline_item_event_id), + refresh_stream_indices( + tl_state.items.iter().map(item_event_id), &mut tl_state.streaming_messages, ); @@ -2995,8 +2984,8 @@ pub enum TimelineUpdate { MediaFetched(MediaRequestParameters), /// A notice that one or more members of a this room are currently typing. TypingUsers { - /// The list of users (user_id, display_name) who are currently typing in this room. - users: Vec<(OwnedUserId, String)>, + /// The list of display names of users who are currently typing in this room. + users: Vec, }, /// The result of a pin/unpin request ([`MatrixRequest::PinEvent`]). PinResult { @@ -5095,20 +5084,29 @@ pub fn clear_timeline_states(_cx: &mut Cx) { #[cfg(test)] mod tests { use super::*; - use crate::home::streaming_animation::{StreamDetection, StreamingAnimState}; + use crate::home::streaming_animation::StreamingAnimState; use std::time::{Duration, Instant}; fn make_state(text: &str) -> StreamingAnimState { - let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); - StreamingAnimState::new(text, user_id, StreamDetection::Heuristic) + StreamingAnimState::new(text, true) } #[test] fn test_bounded_indices_clamps_max() { - let bounded = bounded_changed_indices(&(1..usize::MAX), 3, 4); + let bounded = clamp_indices(&(1..usize::MAX), 3, 4); assert_eq!(bounded, 1..3); } + #[test] + fn test_scan_range_incremental() { + assert_eq!(streaming_scan_range(false, &(5..usize::MAX), 8, 9), 5..8); + } + + #[test] + fn test_scan_range_clear_cache() { + assert_eq!(streaming_scan_range(true, &(5..usize::MAX), 8, 9), 0..9); + } + #[test] fn test_refresh_stream_indices() { let event_id_a: OwnedEventId = "$event-a:example.com".try_into().unwrap(); @@ -5120,7 +5118,7 @@ mod tests { streaming_messages.insert(missing_event_id.clone(), make_state("missing")); let event_ids = vec![None, Some(event_id_a.as_ref()), Some(event_id_b.as_ref())]; - refresh_streaming_message_indices(event_ids.into_iter(), &mut streaming_messages); + refresh_stream_indices(event_ids.into_iter(), &mut streaming_messages); assert_eq!(streaming_messages[&event_id_a].timeline_index, Some(1)); assert_eq!(streaming_messages[&missing_event_id].timeline_index, None); @@ -5128,16 +5126,13 @@ mod tests { #[test] fn test_timeout_picks_earliest() { - let mut first = make_state("alpha"); - first.last_update_time = Instant::now() - Duration::from_secs(10); - let mut second = make_state("beta"); - second.last_update_time = Instant::now() - Duration::from_secs(29); - - let timeout = next_streaming_timeout_duration( - [&first, &second].into_iter(), - Duration::from_secs(30), - ) - .unwrap(); + let mut live = make_state("alpha"); + live.last_update_time = Instant::now() - Duration::from_secs(40); + let mut finished = make_state("beta"); + finished.is_live = false; + finished.last_update_time = Instant::now() - Duration::from_secs(29); + + let timeout = next_stream_timeout([&live, &finished].into_iter()).unwrap(); assert!(timeout <= Duration::from_secs(1)); } diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 4ac2fb0c1..21e992818 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -1,14 +1,10 @@ use std::time::{Duration, Instant}; -use matrix_sdk::ruma::OwnedUserId; -/// How a streaming session was detected. -#[derive(Debug, Clone, PartialEq)] -pub enum StreamDetection { - /// Detected by heuristic: prefix match + recency + not self. - Heuristic, -} +const FINISHED_STREAM_TIMEOUT: Duration = Duration::from_secs(30); +const LIVE_STREAM_STALL_TIMEOUT: Duration = Duration::from_secs(5 * 60); /// Animation state for a single streaming message. +/// Tracks an MSC4357 live message and drives character-by-character reveal. pub struct StreamingAnimState { pub target_text: String, pub target_char_count: usize, @@ -21,14 +17,13 @@ pub struct StreamingAnimState { pub animation_start_time: Instant, pub chars_at_last_update: usize, pub display_buffer: String, - pub sender_stopped_typing: bool, - pub sender_user_id: OwnedUserId, - pub detection: StreamDetection, + /// Whether the message currently carries the MSC4357 `live` field. + pub is_live: bool, pub timeline_index: Option, } impl StreamingAnimState { - pub fn new(initial_text: &str, sender_user_id: OwnedUserId, detection: StreamDetection) -> Self { + pub fn new(initial_text: &str, is_live: bool) -> Self { let char_count = initial_text.chars().count(); let now = Instant::now(); Self { @@ -43,39 +38,34 @@ impl StreamingAnimState { animation_start_time: now, chars_at_last_update: 0, display_buffer: String::with_capacity(initial_text.len() + 4), - sender_stopped_typing: false, - sender_user_id, - detection, + is_live, timeline_index: None, } } - pub fn new_from_visible_prefix( - visible_prefix: &str, - target_text: &str, - sender_user_id: OwnedUserId, - detection: StreamDetection, - ) -> Self { - let mut state = Self::new(target_text, sender_user_id, detection); - if target_text.starts_with(visible_prefix) { - state.displayed_char_count = visible_prefix.chars().count(); - state.displayed_byte_offset = visible_prefix.len(); - state.chars_at_last_update = state.displayed_char_count; - } - state.update_speed(); - state + pub fn restore(previous: &Self, new_text: &str, is_live: bool) -> Self { + let mut restored = Self::new(new_text, is_live); + let visible_prefix = &previous.target_text[..previous.displayed_byte_offset]; + let (common_chars, common_bytes) = common_prefix_len(visible_prefix, new_text); + + restored.displayed_char_count = common_chars; + restored.displayed_byte_offset = common_bytes; + restored.chars_at_last_update = common_chars; + restored.animation_start_time = previous.animation_start_time; + restored.timeline_index = previous.timeline_index; + restored.update_speed(); + restored } - pub fn update_target(&mut self, new_text: &str) { + pub fn update_target(&mut self, new_text: &str, is_live: bool) { self.target_text.clear(); self.target_text.push_str(new_text); self.target_char_count = new_text.chars().count(); - self.sender_stopped_typing = false; + self.is_live = is_live; // Clamp display pointers if the new text is shorter than what was already displayed. if self.displayed_char_count > self.target_char_count { self.displayed_char_count = self.target_char_count; - // Re-derive byte offset to stay on char boundary. self.displayed_byte_offset = self.target_text .char_indices() .nth(self.target_char_count) @@ -163,17 +153,41 @@ impl StreamingAnimState { self.displayed_char_count < self.target_char_count } - /// Check if streaming is complete. - /// Completes when the sender stops typing and all text has been revealed. + /// Streaming is complete when the live field is absent and all text has been revealed. pub fn is_complete(&self) -> bool { - if self.needs_frame() { return false; } - self.sender_stopped_typing + !self.needs_frame() && !self.is_live + } + + pub fn timeout_after(&self) -> Duration { + if self.is_live { + LIVE_STREAM_STALL_TIMEOUT + } else { + FINISHED_STREAM_TIMEOUT + } } pub fn is_timed_out(&self) -> bool { - self.last_update_time.elapsed().as_secs() > 30 + self.last_update_time.elapsed() > self.timeout_after() + } +} + +fn common_prefix_len(lhs: &str, rhs: &str) -> (usize, usize) { + let mut chars = 0; + let mut bytes = 0; + let mut lhs_chars = lhs.chars(); + + for (byte_idx, rhs_char) in rhs.char_indices() { + let Some(lhs_char) = lhs_chars.next() else { + break; + }; + if lhs_char != rhs_char { + break; + } + chars += 1; + bytes = byte_idx + rhs_char.len_utf8(); } + (chars, bytes) } #[cfg(test)] @@ -181,8 +195,7 @@ mod tests { use super::*; fn make_state(text: &str) -> StreamingAnimState { - let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); - StreamingAnimState::new(text, user_id, StreamDetection::Heuristic) + StreamingAnimState::new(text, true) } #[test] @@ -213,8 +226,7 @@ mod tests { fn test_update_target_extends() { let mut s = make_state("Hello"); s.advance_displayed(5); - assert_eq!(s.displayed_char_count, 5); - s.update_target("Hello, world!"); + s.update_target("Hello, world!", true); assert_eq!(s.target_char_count, 13); assert_eq!(s.displayed_char_count, 5); assert!(s.chars_per_second > 0.0); @@ -224,10 +236,9 @@ mod tests { fn test_update_target_shrinks_safely() { let mut s = make_state("Hello, world!"); s.advance_displayed(10); - s.update_target("Hi"); + s.update_target("Hi", true); assert_eq!(s.displayed_char_count, 2); assert_eq!(s.displayed_byte_offset, 2); - // Should not panic s.fill_display_buffer(); assert!(s.display_buffer.starts_with("Hi")); } @@ -241,13 +252,6 @@ mod tests { assert_eq!(s.displayed_char_count, 2); } - #[test] - fn test_tick_complete_noop() { - let mut s = make_state("Hi"); - s.advance_displayed(2); - assert!(!s.tick_with_elapsed(Duration::from_secs(1))); - } - #[test] fn test_tick_large_gap() { let mut s = make_state(&"a".repeat(1000)); @@ -266,30 +270,54 @@ mod tests { } #[test] - fn test_is_complete_heuristic() { + fn test_is_complete_msc4357() { let mut s = make_state("Hi"); s.advance_displayed(2); + // is_live=true → not complete even though all text revealed assert!(!s.is_complete()); - s.sender_stopped_typing = true; + // Simulate final edit without live field + s.is_live = false; assert!(s.is_complete()); } #[test] - fn test_visible_prefix_preserved() { - let user_id: OwnedUserId = "@bot:example.com".try_into().unwrap(); - let s = StreamingAnimState::new_from_visible_prefix( - "Hello", "Hello, world!", user_id, StreamDetection::Heuristic, - ); - assert_eq!(s.displayed_char_count, 5); - assert_eq!(&s.target_text[..s.displayed_byte_offset], "Hello"); + fn test_update_target_sets_live() { + let mut s = make_state("Hello"); + assert!(s.is_live); + s.update_target("Hello, world!", false); + assert!(!s.is_live); + } + + #[test] + fn test_restore_keeps_prefix() { + let mut prev = make_state("Hello, world!"); + prev.advance_displayed(5); + let restored = StreamingAnimState::restore(&prev, "Hello, world!!!", true); + assert_eq!(restored.displayed_char_count, 5); + assert_eq!(&restored.target_text[..restored.displayed_byte_offset], "Hello"); + } + + #[test] + fn test_restore_clamps_prefix() { + let mut prev = make_state("Hello, world!"); + prev.advance_displayed(12); + let restored = StreamingAnimState::restore(&prev, "Hello there", true); + assert_eq!(&restored.target_text[..restored.displayed_byte_offset], "Hello"); } #[test] - fn test_update_target_resets_typing() { + fn test_live_stream_survives_30s() { let mut s = make_state("Hello"); - s.sender_stopped_typing = true; - s.update_target("Hello, world!"); - assert!(!s.sender_stopped_typing); + s.last_update_time = Instant::now() - Duration::from_secs(31); + assert!(!s.is_timed_out()); + } + + #[test] + fn test_finished_stream_times_out() { + let mut s = make_state("Hello"); + s.is_live = false; + s.last_update_time = Instant::now() - Duration::from_secs(31); + assert!(s.is_timed_out()); } #[test] @@ -307,11 +335,4 @@ mod tests { assert_eq!(s.displayed_char_count, 0); } - #[test] - fn test_advance_zero_is_noop() { - let mut s = make_state("Hello"); - s.advance_displayed(0); - assert_eq!(s.displayed_char_count, 0); - assert_eq!(s.displayed_byte_offset, 0); - } } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index e2c1bb34b..84997adec 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1461,7 +1461,7 @@ async fn matrix_worker_task( .flatten() .and_then(|m| m.display_name().map(|d| d.to_owned())) .unwrap_or_else(|| user_id.to_string()); - users.push((user_id, display_name)); + users.push(display_name); } 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 c7465b8d428ccdf0dd0dbc8f8ebc39cf17b7a2d3 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 25 Mar 2026 17:04:28 +0800 Subject: [PATCH 024/283] fix: scan appended items for MSC4357 and clean up completed streams promptly Bug 1: streaming_scan_range reused clamp_indices which clamped end to min(old_len, new_len), making PushBack/Append (changed_indices=old_len..new_len) produce an empty range. New live messages were never detected. Fix: clamp directly to new_len so appended items are scanned. Bug 2: when the final live=false update arrived with text already fully revealed, needs_frame()=false meant no NextFrame was scheduled, so the completed state lingered with cursor until timeout. Fix: also schedule a frame when is_complete() becomes true. --- src/home/room_screen.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 45cf67566..4ca57a097 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -78,26 +78,18 @@ fn is_msc4357_live(event_tl_item: &EventTimelineItem) -> bool { .is_some() } -fn clamp_indices( - changed_indices: &Range, - old_len: usize, - new_len: usize, -) -> Range { - let end = changed_indices.end.min(old_len.min(new_len)); - let start = changed_indices.start.min(end); - start..end -} - fn streaming_scan_range( clear_cache: bool, changed_indices: &Range, - old_len: usize, + _old_len: usize, new_len: usize, ) -> Range { if clear_cache { 0..new_len } else { - clamp_indices(changed_indices, old_len, new_len) + let start = changed_indices.start.min(new_len); + let end = changed_indices.end.min(new_len); + start..end } } @@ -1594,7 +1586,8 @@ impl RoomScreen { if let Some(state) = tl.streaming_messages.get_mut(&event_id) { state.update_target(&new_text, live); - should_schedule_frame |= state.needs_frame(); + // Schedule frame for animation OR for cleanup of just-completed state + should_schedule_frame |= state.needs_frame() || state.is_complete(); continue; } @@ -5092,14 +5085,20 @@ mod tests { } #[test] - fn test_bounded_indices_clamps_max() { - let bounded = clamp_indices(&(1..usize::MAX), 3, 4); - assert_eq!(bounded, 1..3); + fn test_scan_range_incremental() { + // changed_indices 5..MAX clamped to new_len=9 → 5..9 (covers appended items) + assert_eq!(streaming_scan_range(false, &(5..usize::MAX), 8, 9), 5..9); } #[test] - fn test_scan_range_incremental() { - assert_eq!(streaming_scan_range(false, &(5..usize::MAX), 8, 9), 5..8); + fn test_scan_range_append() { + // PushBack: old_len=8, new_len=9, changed_indices=8..9 → 8..9 (new item scanned) + assert_eq!(streaming_scan_range(false, &(8..9), 8, 9), 8..9); + } + + #[test] + fn test_scan_range_empty_when_no_changes() { + assert_eq!(streaming_scan_range(false, &(8..8), 8, 8), 8..8); } #[test] From a8d7578b217b266829d3240648373d394c8ebf00 Mon Sep 17 00:00:00 2001 From: Alvin Date: Wed, 25 Mar 2026 18:55:13 +0800 Subject: [PATCH 025/283] test: consolidate streaming animation tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merge related test cases to reduce duplication: - 2 restore tests → 1 (test_restore_preserves_common_prefix) - 2 timeout tests → 1 (test_timeout_split_by_live_state) - 4 scan_range tests → 1 (test_streaming_scan_range) - Remove test_needs_frame_when_caught_up (covered by test_is_complete_msc4357) 85 → 79 tests, same coverage. --- src/home/room_screen.rs | 20 ++++----------- src/home/streaming_animation.rs | 45 +++++++++++++-------------------- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4ca57a097..0233b32b4 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -5085,24 +5085,14 @@ mod tests { } #[test] - fn test_scan_range_incremental() { - // changed_indices 5..MAX clamped to new_len=9 → 5..9 (covers appended items) + fn test_streaming_scan_range() { + // Incremental: clamp sentinel to new_len assert_eq!(streaming_scan_range(false, &(5..usize::MAX), 8, 9), 5..9); - } - - #[test] - fn test_scan_range_append() { - // PushBack: old_len=8, new_len=9, changed_indices=8..9 → 8..9 (new item scanned) + // Append: new item at end is scanned assert_eq!(streaming_scan_range(false, &(8..9), 8, 9), 8..9); - } - - #[test] - fn test_scan_range_empty_when_no_changes() { + // No changes: empty range assert_eq!(streaming_scan_range(false, &(8..8), 8, 8), 8..8); - } - - #[test] - fn test_scan_range_clear_cache() { + // Clear cache: full scan assert_eq!(streaming_scan_range(true, &(5..usize::MAX), 8, 9), 0..9); } diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 21e992818..886f0e1ed 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -289,42 +289,33 @@ mod tests { } #[test] - fn test_restore_keeps_prefix() { + fn test_restore_preserves_common_prefix() { + // Extension: keep what was already displayed let mut prev = make_state("Hello, world!"); prev.advance_displayed(5); let restored = StreamingAnimState::restore(&prev, "Hello, world!!!", true); assert_eq!(restored.displayed_char_count, 5); assert_eq!(&restored.target_text[..restored.displayed_byte_offset], "Hello"); - } - - #[test] - fn test_restore_clamps_prefix() { - let mut prev = make_state("Hello, world!"); - prev.advance_displayed(12); - let restored = StreamingAnimState::restore(&prev, "Hello there", true); - assert_eq!(&restored.target_text[..restored.displayed_byte_offset], "Hello"); - } - #[test] - fn test_live_stream_survives_30s() { - let mut s = make_state("Hello"); - s.last_update_time = Instant::now() - Duration::from_secs(31); - assert!(!s.is_timed_out()); + // Divergence: clamp to the common prefix + let mut prev2 = make_state("Hello, world!"); + prev2.advance_displayed(12); + let restored2 = StreamingAnimState::restore(&prev2, "Hello there", true); + assert_eq!(&restored2.target_text[..restored2.displayed_byte_offset], "Hello"); } #[test] - fn test_finished_stream_times_out() { - let mut s = make_state("Hello"); - s.is_live = false; - s.last_update_time = Instant::now() - Duration::from_secs(31); - assert!(s.is_timed_out()); - } - - #[test] - fn test_needs_frame_when_caught_up() { - let mut s = make_state("Hello"); - s.advance_displayed(5); - assert!(!s.needs_frame()); + fn test_timeout_split_by_live_state() { + // Live stream survives 31s idle (5min stall timeout) + let mut live = make_state("Hello"); + live.last_update_time = Instant::now() - Duration::from_secs(31); + assert!(!live.is_timed_out()); + + // Finished stream times out after 31s (30s cleanup timeout) + let mut finished = make_state("Hello"); + finished.is_live = false; + finished.last_update_time = Instant::now() - Duration::from_secs(31); + assert!(finished.is_timed_out()); } #[test] From 7cae0cceb9f90812a5efcb25a04959ab276b6b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 00:14:16 +0800 Subject: [PATCH 026/283] Finish create room space and users --- src/app.rs | 20 + src/home/add_room.rs | 860 +++++++++++++++++++++++++++++++++++++++- src/home/rooms_list.rs | 97 ++++- src/home/space_lobby.rs | 70 ++++ src/sliding_sync.rs | 118 +++++- 5 files changed, 1156 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index f04e177d5..433392239 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::clear_avatar_cache, home::{ + add_room::{CreateRoomModalAction, CreateRoomModalWidgetRefExt}, 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 }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt @@ -105,6 +106,12 @@ script_mod! { } } + create_room_modal := Modal { + content +: { + create_room_modal_inner := CreateRoomModal {} + } + } + // Show the logout confirmation modal. logout_confirm_modal := Modal { content +: { @@ -557,6 +564,19 @@ impl MatchEvent for App { _ => {} } + match action.downcast_ref() { + Some(CreateRoomModalAction::Open { parent_space_id }) => { + self.ui.create_room_modal(cx, ids!(create_room_modal_inner)).show(cx, parent_space_id.clone()); + self.ui.modal(cx, ids!(create_room_modal)).open(cx); + continue; + } + Some(CreateRoomModalAction::Close) => { + self.ui.modal(cx, ids!(create_room_modal)).close(cx); + continue; + } + _ => {} + } + // Handle EventSourceModalAction to open/close the event source modal. match action.downcast_ref() { Some(EventSourceModalAction::Open { room_id, event_id, original_json }) => { diff --git a/src/home/add_room.rs b/src/home/add_room.rs index 981369897..a6a32edc7 100644 --- a/src/home/add_room.rs +++ b/src/home/add_room.rs @@ -3,15 +3,138 @@ use makepad_widgets::*; use matrix_sdk::RoomState; -use ruma::{IdParseError, MatrixToUri, MatrixUri, OwnedRoomOrAliasId, OwnedServerName, matrix_uri::MatrixId, room::{JoinRuleSummary, RoomType}}; - -use crate::{app::AppStateAction, home::invite_screen::JoinRoomResultAction, room::{FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, shared::{avatar::AvatarWidgetRefExt, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{MatrixRequest, submit_async_request}, utils}; +use ruma::{IdParseError, MatrixToUri, MatrixUri, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, matrix_uri::MatrixId, room::{JoinRuleSummary, RoomType}}; + +use crate::{ + app::AppStateAction, + home::{invite_screen::JoinRoomResultAction, navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef}, + profile::user_profile::UserProfile, + room::{BasicRoomDetails, FetchedRoomAvatar, FetchedRoomPreview, RoomPreviewAction}, + shared::{ + avatar::{AvatarState, AvatarWidgetRefExt}, + popup_list::{PopupKind, enqueue_popup_notification}, + styles::COLOR_FG_DANGER_RED, + }, + sliding_sync::{DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, + space_service_sync::SpaceRequest, + utils::{self, RoomNameId}, +}; script_mod! { use mod.prelude.widgets.* use mod.widgets.* + mod.widgets.CreateRoomForm = set_type_default() do #(CreateRoomForm::register_widget(vm)) { + ..mod.widgets.View + + width: Fill + height: Fit + flow: Down + + create_room_help := Label { + width: Fill, height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "Create a standalone room, or attach it under a space where you can create child rooms." + } + + create_room_view := View { + width: Fill + height: Fit + margin: Inset{ top: 6, bottom: 10 } + spacing: 8 + flow: Down + + create_room_space_row := View { + width: Fill + height: Fit + margin: Inset{left: 5, right: 5} + spacing: 10 + align: Align{y: 0.5} + flow: Right + + create_room_space_dropdown := DropDownFlat { + width: Fill { max: 400 } + height: 40 + labels: ["Create without a space"] + } + + create_room_space_hint := Label { + width: Fill, height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "Choose a space where you have permission to create child rooms." + } + } + + create_room_name_input := RobrixTextInput { + margin: Inset{left: 5, right: 5} + padding: Inset{left: 12, right: 12, top: 11, bottom: 0} + width: Fill { max: 400 } + height: 40 + empty_text: "Enter the new room name..." + } + + create_room_feedback := View { + visible: false + width: Fill + height: Fit + margin: Inset{left: 5, right: 5, top: 6} + spacing: 8 + align: Align{y: 0.5} + flow: Right + + create_room_feedback_spinner_wrap := View { + width: Fit + height: Fit + + create_room_feedback_spinner := LoadingSpinner { + width: 16 + height: 16 + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + border_size: 2.0 + } + } + } + + create_room_feedback_label := Label { + width: Fill + height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + } + } + + create_room_button_row := View { + width: Fill + height: Fit + margin: Inset{top: 4} + padding: Inset{left: 5} + flow: Right + + create_room_button := RobrixPositiveIconButton { + width: Fit + padding: Inset{top: 10, bottom: 10, left: 12, right: 14} + height: 40 + draw_icon.svg: (ICON_ADD) + icon_walk: Walk{width: 16, height: 16} + text: "Create room" + } + } + } + } + // The main view that allows the user to add (join) or explore new rooms/spaces. mod.widgets.AddRoomScreen = #(AddRoomScreen::register_widget(vm)) { @@ -35,6 +158,58 @@ script_mod! { LineH { padding: 10, margin: Inset{top: 10, right: 2} } + SubsectionLabel { + margin: Inset{top: 8} + text: "Create a new room:" + } + + create_room_form := mod.widgets.CreateRoomForm {} + + LineH { padding: 10, margin: Inset{right: 2} } + + SubsectionLabel { + margin: Inset{top: 4} + text: "Add a friend:" + } + + add_friend_help := Label { + width: Fill, height: Fit + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "Enter a Matrix user ID to open or create a direct message room." + } + + add_friend_view := View { + width: Fill + height: Fit + margin: Inset{ top: 6, bottom: 10 } + align: Align{y: 0.5} + spacing: 5 + flow: Right + + friend_user_id_input := RobrixTextInput { + align: Align{y: 0.5} + margin: Inset{top: 0, left: 5, right: 5, bottom: 0} + padding: Inset{left: 12, right: 12, top: 11, bottom: 0} + width: Fill { max: 400 } + height: 40 + empty_text: "Enter a Matrix user ID, like @alice:matrix.org..." + } + + add_friend_button := RobrixIconButton { + padding: Inset{top: 10, bottom: 10, left: 12, right: 14} + height: 40 + draw_icon.svg: (ICON_ADD_USER) + icon_walk: Walk{width: 16, height: 16} + text: "Add friend" + } + } + + LineH { padding: 10, margin: Inset{right: 2} } + SubsectionLabel { text: "Join an existing room or space:" } @@ -250,6 +425,85 @@ script_mod! { } } + + mod.widgets.CreateRoomModal = #(CreateRoomModal::register_widget(vm)) { + width: Fit + height: Fit + + RoundedView { + width: 500 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 24, right: 24, bottom: 20, left: 24} + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 4.0 + } + + title_view := View { + width: Fill + height: Fit + padding: Inset{top: 0, bottom: 20} + align: Align{x: 0.5, y: 0.0} + + title := Label { + width: Fill + height: Fit + align: Align{x: 0.5} + flow: Flow.Right{wrap: true} + draw_text +: { + text_style: TITLE_TEXT {font_size: 13} + color: #000 + } + text: "Create New Room" + } + } + + subtitle := Label { + width: Fill + height: Fit + margin: Inset{bottom: 10} + flow: Flow.Right{wrap: true} + draw_text +: { + color: (MESSAGE_TEXT_COLOR) + text_style: MESSAGE_TEXT_STYLE { font_size: 11 } + } + text: "Create a new room directly inside the selected space." + } + + create_room_form := mod.widgets.CreateRoomForm {} + + buttons_view := View { + width: Fill + height: Fit + flow: Right + padding: Inset{top: 16, bottom: 5} + align: Align{x: 1.0, y: 0.5} + spacing: 12 + + create_button := RobrixPositiveIconButton { + width: 140 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_ADD) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Create room" + } + + cancel_button := RobrixNeutralIconButton { + width: 120 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "Cancel" + } + } + } + } } #[derive(Script, ScriptHook, Widget)] @@ -258,6 +512,13 @@ pub struct AddRoomScreen { #[rust] state: AddRoomState, /// The function to perform when the user clicks the `join_room_button`. #[rust(JoinButtonFunction::None)] join_function: JoinButtonFunction, + #[rust(false)] adding_friend: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CreateRoomContext { + AddRoomPage, + SpaceLobbyModal, } #[derive(Default)] @@ -345,6 +606,402 @@ impl AddRoomState { } } +#[derive(Script, ScriptHook, Widget)] +pub struct CreateRoomForm { + #[deref] view: View, + #[rust(CreateRoomContext::AddRoomPage)] context: CreateRoomContext, + #[rust(false)] creating_room: bool, + #[rust(None)] pending_created_room: Option, + #[rust(Vec::new())] creatable_spaces: Vec, + #[rust(None)] preferred_parent_space_id: Option, + #[rust(None)] fixed_parent_space_id: Option, +} + +impl Widget for CreateRoomForm { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let create_room_text_is_empty = self.view + .text_input(cx, ids!(create_room_name_input)) + .text() + .trim() + .is_empty(); + self.view.button(cx, ids!(create_room_button)) + .set_enabled(cx, !self.is_busy() && !create_room_text_is_empty); + + let selected_space_id = self.selected_parent_space_id( + self.view.drop_down(cx, ids!(create_room_space_dropdown)).selected_item(), + ); + let create_room_space_hint = self.view.label(cx, ids!(create_room_space_hint)); + update_space_hint( + cx, + &create_room_space_hint, + &self.creatable_spaces, + selected_space_id.as_ref(), + ); + + self.sync_mode_views(cx); + + self.view.draw_walk(cx, scope, walk) + } +} + +impl WidgetMatchEvent for CreateRoomForm { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let create_room_name_input = self.view.text_input(cx, ids!(create_room_name_input)); + let create_room_button = self.view.button(cx, ids!(create_room_button)); + let create_room_space_dropdown = self.view.drop_down(cx, ids!(create_room_space_dropdown)); + let create_room_space_hint = self.view.label(cx, ids!(create_room_space_hint)); + + if let Some(text) = create_room_name_input.changed(actions) { + if !self.is_busy() { + self.clear_feedback(cx); + } + create_room_button.set_enabled(cx, !self.is_busy() && !text.trim().is_empty()); + } + + if create_room_space_dropdown.changed(actions).is_some() { + self.preferred_parent_space_id = + selected_creatable_space(&self.creatable_spaces, create_room_space_dropdown.selected_item()); + update_space_hint( + cx, + &create_room_space_hint, + &self.creatable_spaces, + self.preferred_parent_space_id.as_ref(), + ); + self.view.redraw(cx); + } + + let create_room_request = create_room_button.clicked(actions) + || create_room_name_input.returned(actions).is_some(); + if create_room_request { + let _ = self.submit(cx); + } + + for action in actions { + if let Some(create_room_action) = action.downcast_ref() { + match create_room_action { + CreateRoomAction::Created { room_name_id, parent_space_id, space_link_error, context } + if context == &self.context => + { + self.creating_room = false; + create_room_name_input.set_text(cx, ""); + create_room_button.set_enabled(cx, false); + + if let Some(space_id) = parent_space_id { + refresh_space_children(cx, space_id); + } + + let mut popup_message = format!("Successfully created room \"{}\".", room_name_id); + let popup_kind = if let Some(link_error) = space_link_error { + popup_message.push_str(&format!( + "\n\nThe room was created, but it could not be linked into the selected space.\nError: {link_error}" + )); + PopupKind::Warning + } else { + PopupKind::Success + }; + enqueue_popup_notification(popup_message, popup_kind, Some(5.0)); + + if cx.has_global::() + && cx.get_global::().is_room_loaded(room_name_id.room_id()) + { + self.clear_feedback(cx); + if self.context == CreateRoomContext::SpaceLobbyModal { + cx.action(CreateRoomModalAction::Close); + } + cx.action(AppStateAction::NavigateToRoom { + room_to_close: None, + destination_room: BasicRoomDetails::Name(room_name_id.clone()), + }); + } else { + self.pending_created_room = Some(room_name_id.clone()); + let feedback_text = match (parent_space_id.as_ref(), space_link_error.as_ref()) { + (Some(_), None) => "Room created. Syncing it into the space...", + (Some(_), Some(_)) => "Room created, but linking it into the space failed. Opening the room...", + (None, _) => "Room created. Opening the room...", + }; + self.set_feedback(cx, feedback_text, true, false); + } + + self.view.redraw(cx); + } + CreateRoomAction::Failed { room_name, error, context } + if context == &self.context => + { + self.creating_room = false; + create_room_button.set_enabled(cx, !create_room_name_input.text().trim().is_empty()); + self.set_feedback( + cx, + &format!("Failed to create room: {error}"), + false, + true, + ); + enqueue_popup_notification( + format!("Failed to create room \"{room_name}\".\n\nError: {error}"), + PopupKind::Error, + None, + ); + self.view.redraw(cx); + } + _ => {} + } + } + + if let Some(CreatableSpacesAction::Loaded { spaces }) = action.downcast_ref() { + self.creatable_spaces = spaces.clone(); + sync_space_dropdown( + cx, + &create_room_space_dropdown, + &create_room_space_hint, + &self.creatable_spaces, + self.preferred_parent_space_id.as_ref(), + ); + self.sync_mode_views(cx); + self.view.redraw(cx); + } + + if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = action.downcast_ref() + && self.pending_created_room.as_ref().is_some_and(|pending| pending.room_id() == room_name_id.room_id()) + { + self.pending_created_room = None; + self.clear_feedback(cx); + if self.context == CreateRoomContext::SpaceLobbyModal { + cx.action(CreateRoomModalAction::Close); + } + cx.action(AppStateAction::NavigateToRoom { + room_to_close: None, + destination_room: BasicRoomDetails::Name(room_name_id.clone()), + }); + } + } + } +} + +impl CreateRoomForm { + fn can_submit(&self, cx: &mut Cx) -> bool { + !self.is_busy() + && !self.view + .text_input(cx, ids!(create_room_name_input)) + .text() + .trim() + .is_empty() + } + + fn is_busy(&self) -> bool { + self.creating_room || self.pending_created_room.is_some() + } + + fn set_feedback(&mut self, cx: &mut Cx, text: &str, show_spinner: bool, is_error: bool) { + self.view.view(cx, ids!(create_room_feedback)).set_visible(cx, true); + self.view.view(cx, ids!(create_room_feedback_spinner_wrap)) + .set_visible(cx, show_spinner); + let mut feedback_label = self.view.label(cx, ids!(create_room_feedback_label)); + feedback_label.set_text(cx, text); + script_apply_eval!(cx, feedback_label, { + draw_text +: { + color: #( + if is_error { + COLOR_FG_DANGER_RED + } else { + vec4(0.2, 0.2, 0.2, 1.0) + } + ) + } + }); + } + + fn clear_feedback(&mut self, cx: &mut Cx) { + self.view.view(cx, ids!(create_room_feedback)).set_visible(cx, false); + self.view.label(cx, ids!(create_room_feedback_label)).set_text(cx, ""); + } + + fn submit(&mut self, cx: &mut Cx) -> bool { + if !self.can_submit(cx) { + return false; + } + + let room_name = self.view.text_input(cx, ids!(create_room_name_input)).text(); + let room_name = room_name.trim(); + let parent_space_id = self.selected_parent_space_id( + self.view.drop_down(cx, ids!(create_room_space_dropdown)).selected_item(), + ); + + self.creating_room = true; + self.set_feedback(cx, "Creating room...", true, false); + submit_async_request(MatrixRequest::CreateRoom { + room_name: room_name.to_owned(), + parent_space_id, + context: self.context.clone(), + }); + self.view.redraw(cx); + true + } + + pub fn prepare( + &mut self, + cx: &mut Cx, + preferred_parent_space_id: Option, + context: CreateRoomContext, + clear_room_name: bool, + ) { + self.context = context; + self.creating_room = false; + self.pending_created_room = None; + self.preferred_parent_space_id = preferred_parent_space_id; + self.fixed_parent_space_id = (self.context == CreateRoomContext::SpaceLobbyModal) + .then_some(self.preferred_parent_space_id.clone()) + .flatten(); + + let create_room_name_input = self.view.text_input(cx, ids!(create_room_name_input)); + let create_room_button = self.view.button(cx, ids!(create_room_button)); + let create_room_space_dropdown = self.view.drop_down(cx, ids!(create_room_space_dropdown)); + let create_room_space_hint = self.view.label(cx, ids!(create_room_space_hint)); + + if clear_room_name { + create_room_name_input.set_text(cx, ""); + } + self.clear_feedback(cx); + create_room_button.set_enabled(cx, !create_room_name_input.text().trim().is_empty()); + create_room_button.set_text(cx, "Create room"); + create_room_button.reset_hover(cx); + + sync_space_dropdown( + cx, + &create_room_space_dropdown, + &create_room_space_hint, + &self.creatable_spaces, + self.preferred_parent_space_id.as_ref(), + ); + self.sync_mode_views(cx); + + if self.fixed_parent_space_id.is_none() { + submit_async_request(MatrixRequest::GetCreatableSpaces); + } + create_room_name_input.set_key_focus(cx); + self.view.redraw(cx); + } + + pub fn refresh_creatable_spaces(&mut self, _cx: &mut Cx) { + submit_async_request(MatrixRequest::GetCreatableSpaces); + } + + fn selected_parent_space_id(&self, dropdown_index: usize) -> Option { + self.fixed_parent_space_id.clone() + .or_else(|| selected_creatable_space(&self.creatable_spaces, dropdown_index)) + } + + fn sync_mode_views(&mut self, cx: &mut Cx) { + let show_fixed_parent = self.fixed_parent_space_id.is_some(); + self.view.view(cx, ids!(create_room_space_row)).set_visible(cx, !show_fixed_parent); + self.view.view(cx, ids!(create_room_button_row)).set_visible(cx, !show_fixed_parent); + + let help_text = if show_fixed_parent { + "Enter a room name. It will be created directly in this space." + } else { + "Create a standalone room, or attach it under a space where you can create child rooms." + }; + self.view.label(cx, ids!(create_room_help)).set_text(cx, help_text); + } +} + +impl CreateRoomFormRef { + pub fn can_submit(&self, cx: &mut Cx) -> bool { + self.borrow().is_some_and(|inner| inner.can_submit(cx)) + } + + pub fn is_busy(&self) -> bool { + self.borrow().is_some_and(|inner| inner.is_busy()) + } + + pub fn submit(&self, cx: &mut Cx) -> bool { + self.borrow_mut().is_some_and(|mut inner| inner.submit(cx)) + } + + pub fn prepare( + &self, + cx: &mut Cx, + preferred_parent_space_id: Option, + context: CreateRoomContext, + clear_room_name: bool, + ) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.prepare(cx, preferred_parent_space_id, context, clear_room_name); + } + + pub fn refresh_creatable_spaces(&self, cx: &mut Cx) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.refresh_creatable_spaces(cx); + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct CreateRoomModal { + #[deref] view: View, +} + +impl Widget for CreateRoomModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_event(cx, event, scope); + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let create_room_form = self.view.create_room_form(cx, ids!(create_room_form)); + let is_busy = create_room_form.is_busy(); + let create_button = self.view.button(cx, ids!(create_button)); + let can_submit = create_room_form.can_submit(cx); + create_button.set_enabled(cx, can_submit); + create_button.set_text(cx, if is_busy { "Syncing..." } else { "Create room" }); + self.view.button(cx, ids!(cancel_button)).set_enabled(cx, !is_busy); + self.view.draw_walk(cx, scope, walk) + } +} + +impl WidgetMatchEvent for CreateRoomModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let create_room_form = self.view.create_room_form(cx, ids!(create_room_form)); + let create_button = self.view.button(cx, ids!(create_button)); + let cancel_button = self.view.button(cx, ids!(cancel_button)); + if create_button.clicked(actions) { + let _ = create_room_form.submit(cx); + } + let cancel_clicked = cancel_button.clicked(actions); + if !create_room_form.is_busy() + && (cancel_clicked || actions.iter().any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed)))) + { + if cancel_clicked { + cx.action(CreateRoomModalAction::Close); + } + } + } +} + +impl CreateRoomModal { + pub fn show(&mut self, cx: &mut Cx, preferred_parent_space_id: Option) { + self.view.create_room_form(cx, ids!(create_room_form)).prepare( + cx, + preferred_parent_space_id, + CreateRoomContext::SpaceLobbyModal, + true, + ); + self.view.button(cx, ids!(create_button)).set_text(cx, "Create room"); + self.view.button(cx, ids!(create_button)).reset_hover(cx); + self.view.button(cx, ids!(cancel_button)).reset_hover(cx); + self.view.redraw(cx); + } +} + +impl CreateRoomModalRef { + pub fn show(&self, cx: &mut Cx, preferred_parent_space_id: Option) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.show(cx, preferred_parent_space_id); + } +} + impl Widget for AddRoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { self.view.handle_event(cx, event, scope); @@ -352,6 +1009,8 @@ impl Widget for AddRoomScreen { if let Event::Actions(actions) = event { let room_alias_id_input = self.view.text_input(cx, ids!(room_alias_id_input)); let search_for_room_button = self.view.button(cx, ids!(search_for_room_button)); + let friend_user_id_input = self.view.text_input(cx, ids!(friend_user_id_input)); + let add_friend_button = self.view.button(cx, ids!(add_friend_button)); let cancel_button = self.view.button(cx, ids!(fetched_room_summary.buttons_view.cancel_button)); let join_room_button = self.view.button(cx, ids!(fetched_room_summary.buttons_view.join_room_button)); @@ -359,6 +1018,48 @@ impl Widget for AddRoomScreen { if let Some(text) = room_alias_id_input.changed(actions) { search_for_room_button.set_enabled(cx, !text.trim().is_empty()); } + if let Some(text) = friend_user_id_input.changed(actions) { + add_friend_button.set_enabled(cx, !self.adding_friend && !text.trim().is_empty()); + } + + let add_friend_request = add_friend_button.clicked(actions) + .then(|| friend_user_id_input.text()) + .or_else(|| friend_user_id_input.returned(actions).map(|(t, _)| t)); + if let Some(user_id_str) = add_friend_request { + let user_id_str = user_id_str.trim(); + if !user_id_str.is_empty() { + match user_id_str.parse::() { + Ok(user_id) => { + if current_user_id().as_ref().is_some_and(|current| current == &user_id) { + enqueue_popup_notification( + "You cannot add yourself as a friend.".to_string(), + PopupKind::Warning, + Some(4.0), + ); + } else { + self.adding_friend = true; + add_friend_button.set_enabled(cx, false); + submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + user_profile: UserProfile { + user_id, + username: None, + avatar_state: AvatarState::Unknown, + }, + allow_create: false, + }); + } + } + Err(e) => { + enqueue_popup_notification( + format!("Invalid Matrix user ID.\n\nError: {e}"), + PopupKind::Error, + None, + ); + friend_user_id_input.set_key_focus(cx); + } + } + } + } // If the cancel button was clicked, hide the room preview and return to default state. if cancel_button.clicked(actions) { @@ -527,6 +1228,24 @@ impl Widget for AddRoomScreen { } for action in actions { + if matches!( + action.downcast_ref(), + Some( + DirectMessageRoomAction::FoundExisting { .. } + | DirectMessageRoomAction::DidNotExist { .. } + | DirectMessageRoomAction::NewlyCreated { .. } + | DirectMessageRoomAction::FailedToCreate { .. } + ) + ) { + self.adding_friend = false; + add_friend_button.set_enabled(cx, !friend_user_id_input.text().trim().is_empty()); + } + + if let Some(NavigationBarAction::TabSelected(SelectedTab::AddRoom)) = action.downcast_ref() { + self.view.create_room_form(cx, ids!(create_room_form)) + .prepare(cx, None, CreateRoomContext::AddRoomPage, false); + } + // If the room/space the user is searching for has been loaded from the homeserver // (e.g., by getting invited to it, or joining it in another client), // then update the state of @@ -542,6 +1261,14 @@ impl Widget for AddRoomScreen { fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + let add_friend_text_is_empty = self.view + .text_input(cx, ids!(friend_user_id_input)) + .text() + .trim() + .is_empty(); + self.view.button(cx, ids!(add_friend_button)) + .set_enabled(cx, !self.adding_friend && !add_friend_text_is_empty); + let loading_room_view = self.view.view(cx, ids!(loading_room_view)); let fetched_room_summary = self.view.view(cx, ids!(fetched_room_summary)); let error_view = self.view.view(cx, ids!(error_view)); @@ -752,6 +1479,96 @@ impl Widget for AddRoomScreen { } } +fn refresh_space_children(cx: &mut Cx, space_id: &OwnedRoomId) { + let Some(rooms_list_ref) = cx.has_global::().then(|| cx.get_global::()) else { + return; + }; + let Some(space_request_sender) = rooms_list_ref.get_space_request_sender() else { + return; + }; + let parent_chain = rooms_list_ref.get_space_parent_chain(space_id).unwrap_or_default(); + if let Err(e) = space_request_sender.send(SpaceRequest::SubscribeToSpaceRoomList { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) { + error!("Failed to subscribe to space room list for {space_id}: {e}"); + return; + } + if let Err(e) = space_request_sender.send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) { + error!("Failed to paginate children for space {space_id}: {e}"); + } + if let Err(e) = space_request_sender.send(SpaceRequest::GetChildren { + space_id: space_id.clone(), + parent_chain, + }) { + error!("Failed to refresh children for space {space_id}: {e}"); + } +} + +fn creatable_space_labels(creatable_spaces: &[RoomNameId]) -> Vec { + let mut labels = Vec::with_capacity(creatable_spaces.len() + 1); + labels.push("Create without a space".to_string()); + labels.extend(creatable_spaces.iter().map(ToString::to_string)); + labels +} + +fn selected_creatable_space(creatable_spaces: &[RoomNameId], dropdown_index: usize) -> Option { + dropdown_index.checked_sub(1) + .and_then(|index| creatable_spaces.get(index)) + .map(|space| space.room_id().clone()) +} + +fn apply_space_dropdown_selection( + cx: &mut Cx, + dropdown: &DropDownRef, + creatable_spaces: &[RoomNameId], + preferred_parent_space_id: Option<&OwnedRoomId>, +) { + let selected_index = preferred_parent_space_id + .and_then(|space_id| + creatable_spaces.iter().position(|space| space.room_id() == space_id) + ) + .map(|index| index + 1) + .unwrap_or_else(|| dropdown.selected_item().min(creatable_spaces.len())); + dropdown.set_selected_item(cx, selected_index); +} + +fn update_space_hint( + cx: &mut Cx, + hint_label: &LabelRef, + creatable_spaces: &[RoomNameId], + selected_space_id: Option<&OwnedRoomId>, +) { + if creatable_spaces.is_empty() { + hint_label.set_text(cx, "No joined space currently allows you to create child rooms."); + } else if let Some(space_id) = selected_space_id { + let selected_name = creatable_spaces + .iter() + .find(|space| space.room_id() == space_id) + .map(ToString::to_string) + .unwrap_or_else(|| space_id.to_string()); + hint_label.set_text(cx, &format!("New room will be added under: {selected_name}")); + } else { + hint_label.set_text(cx, "Create a standalone room, or choose a space from the dropdown."); + } +} + +fn sync_space_dropdown( + cx: &mut Cx, + dropdown: &DropDownRef, + hint_label: &LabelRef, + creatable_spaces: &[RoomNameId], + preferred_parent_space_id: Option<&OwnedRoomId>, +) { + dropdown.set_labels(cx, creatable_space_labels(creatable_spaces)); + apply_space_dropdown_selection(cx, dropdown, creatable_spaces, preferred_parent_space_id); + let selected_space_id = selected_creatable_space(creatable_spaces, dropdown.selected_item()); + update_space_hint(cx, hint_label, creatable_spaces, selected_space_id.as_ref()); +} + /// The function to perform when the user clicks the join button in the fetched room preview. enum JoinButtonFunction { @@ -781,6 +1598,43 @@ pub enum KnockResultAction { } } +/// Actions sent from the backend task as a result of a [`MatrixRequest::CreateRoom`]. +#[derive(Debug)] +pub enum CreateRoomAction { + /// A new room was created. + Created { + room_name_id: RoomNameId, + parent_space_id: Option, + /// If set, the room was created but couldn't be linked into the requested space. + space_link_error: Option, + context: CreateRoomContext, + }, + /// There was an error creating the room. + Failed { + room_name: String, + error: matrix_sdk::Error, + context: CreateRoomContext, + }, +} + +/// Actions emitted by other widgets to show or hide the create-room modal. +#[derive(Debug)] +pub enum CreateRoomModalAction { + Open { + parent_space_id: Option, + }, + Close, +} + +/// Actions sent from the backend task containing the spaces where the current user +/// can create child rooms. +#[derive(Debug)] +pub enum CreatableSpacesAction { + Loaded { + spaces: Vec, + }, +} + /// Tries to extract a room address (Alias or ID) from the given text. /// diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 7cf7d5106..8c8baf93d 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -26,7 +26,11 @@ use matrix_sdk::{RoomState, ruma::{events::tag::Tags, MilliSecondsSinceUnixEpoch use crate::{ app::{AppState, SelectedRoom}, home::{ - navigation_tab_bar::{NavigationBarAction, SelectedTab}, room_context_menu::RoomContextMenuDetails, rooms_list_entry::RoomsListEntryAction, space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt} + add_room::CreateRoomAction, + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room_context_menu::RoomContextMenuDetails, + rooms_list_entry::RoomsListEntryAction, + space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt}, }, room::{ FetchedRoomAvatar, @@ -512,6 +516,66 @@ impl RoomsList { None } + fn upsert_created_room_placeholder( + &mut self, + cx: &mut Cx, + room_name_id: &RoomNameId, + parent_space_id: Option<&OwnedRoomId>, + should_link_into_space: bool, + ) { + let room_id = room_name_id.room_id().clone(); + let room_avatar = FetchedRoomAvatar::Text( + room_name_id.name_for_avatar().unwrap_or("?").to_owned(), + ); + + match self.all_joined_rooms.entry(room_id.clone()) { + Entry::Occupied(mut occ) => { + occ.get_mut().room_name_id = room_name_id.clone(); + occ.get_mut().room_avatar = room_avatar; + } + Entry::Vacant(vac) => { + vac.insert(JoinedRoomInfo { + room_name_id: room_name_id.clone(), + num_unread_messages: 0, + num_unread_mentions: 0, + is_marked_unread: false, + canonical_alias: None, + alt_aliases: Vec::new(), + tags: Tags::default(), + latest: None, + room_avatar, + has_been_paginated: false, + is_selected: false, + is_direct: false, + is_tombstoned: false, + }); + } + } + + if should_link_into_space { + if let Some(parent_space_id) = parent_space_id { + match self.space_map.entry(parent_space_id.clone()) { + Entry::Occupied(mut occ) => { + let value = occ.get_mut(); + let mut direct_child_rooms = (*value.direct_child_rooms).clone(); + direct_child_rooms.insert(room_id.clone()); + value.direct_child_rooms = Arc::new(direct_child_rooms); + } + Entry::Vacant(vac) => { + let mut direct_child_rooms = HashSet::new(); + direct_child_rooms.insert(room_id.clone()); + vac.insert(SpaceMapValue { + direct_child_rooms: Arc::new(direct_child_rooms), + ..Default::default() + }); + } + } + } + } + + self.update_displayed_rooms(cx, false); + } + /// Handle all pending updates to the list of all rooms. fn handle_rooms_list_updates(&mut self, cx: &mut Cx, _event: &Event, _scope: &mut Scope) { let mut num_updates: usize = 0; @@ -536,8 +600,10 @@ impl RoomsList { let _replaced = self.all_joined_rooms.insert(room_id.clone(), joined_room); if should_display { if is_direct { - self.displayed_direct_rooms.push(room_id.clone()); - } else { + if !self.displayed_direct_rooms.contains(&room_id) { + self.displayed_direct_rooms.push(room_id.clone()); + } + } else if !self.displayed_regular_rooms.contains(&room_id) { self.displayed_regular_rooms.push(room_id.clone()); } } @@ -970,17 +1036,32 @@ impl RoomsList { } // Otherwise, if no sort function was provided (default), use the `all_known_rooms_order`. else { + let mut seen_joined = HashSet::new(); + let mut seen_invited = HashSet::new(); for room_id in &self.all_known_rooms_order { if let Some(jr) = self.all_joined_rooms.get(room_id) { if should_display_room!(self, room_id, jr) { + seen_joined.insert(room_id.clone()); push_joined_room(room_id, jr); } } else if let Some(ir) = invited_rooms_ref.get(room_id) { if should_display_room!(self, room_id, ir) { + seen_invited.insert(room_id.clone()); new_displayed_invited_rooms.push(room_id.clone()); } } } + + for (room_id, jr) in &self.all_joined_rooms { + if !seen_joined.contains(room_id) && should_display_room!(self, room_id, jr) { + push_joined_room(room_id, jr); + } + } + for (room_id, ir) in invited_rooms_ref.iter() { + if !seen_invited.contains(room_id) && should_display_room!(self, room_id, ir) { + new_displayed_invited_rooms.push(room_id.clone()); + } + } } (new_displayed_invited_rooms, new_displayed_regular_rooms, new_displayed_direct_rooms) @@ -1350,6 +1431,16 @@ impl Widget for RoomsList { _ => {} } + if let Some(CreateRoomAction::Created { room_name_id, parent_space_id, space_link_error, .. }) = action.downcast_ref() { + self.upsert_created_room_placeholder( + cx, + room_name_id, + parent_space_id.as_ref(), + space_link_error.is_none(), + ); + continue; + } + if let Some(space_room_list_action) = action.downcast_ref() { self.handle_space_room_list_action(cx, space_room_list_action); continue; diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index 42bca8635..eb9b8277c 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -22,6 +22,7 @@ use crate::{ app::AppStateAction, avatar_cache::{self, AvatarCacheEntry}, home::{ + add_room::{CreateRoomAction, CreateRoomModalAction}, invite_modal::InviteModalAction, rooms_list::RoomsListRef, }, @@ -482,6 +483,16 @@ script_mod! { text: "" } + create_room_button := RobrixPositiveIconButton { + width: Fit + align: Align{x: 0.5, y: 0.5} + margin: Inset{left: 6} + padding: 12, + draw_icon.svg: (ICON_ADD) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1} } + text: "New Room" + } + invite_button := RobrixPositiveIconButton { width: Fit align: Align{x: 0.5, y: 0.5} @@ -921,6 +932,14 @@ impl Widget for SpaceLobbyScreen { _ => { } } + if let Some(CreateRoomAction::Created { room_name_id, parent_space_id, space_link_error, .. }) = action.downcast_ref() { + if space_link_error.is_none() + && parent_space_id.as_ref() == self.space_name_id.as_ref().map(RoomNameId::room_id) + { + self.insert_created_room_placeholder(cx, room_name_id); + } + } + // Handle SubspaceEntry clicks match action.as_widget_action().cast_ref() { SubspaceEntryAction::SpaceClicked { space_id } => { @@ -969,6 +988,14 @@ impl Widget for SpaceLobbyScreen { } } + if self.view.button(cx, ids!(header.parent_space_row.create_room_button)).clicked(actions) { + if let Some(space_name_id) = self.space_name_id.as_ref() { + cx.action(CreateRoomModalAction::Open { + parent_space_id: Some(space_name_id.room_id().clone()), + }); + } + } + // Handle the invite button being clicked in the header. if self.view.button(cx, ids!(header.parent_space_row.invite_button)).clicked(actions) { if let Some(space_name_id) = self.space_name_id.as_ref() { @@ -1202,6 +1229,49 @@ impl SpaceLobbyScreen { BasicRoomDetails::Name(room_name_id) } + fn insert_created_room_placeholder(&mut self, cx: &mut Cx, room_name_id: &RoomNameId) { + let Some(space_id) = self.space_name_id.as_ref().map(|space| space.room_id().clone()) else { + return; + }; + let room_id = room_name_id.room_id().clone(); + let display_name = room_name_id.to_string(); + let mut children = self.children_cache.get(&space_id).cloned().unwrap_or_default(); + + if let Some(existing_index) = children.iter().position(|child| child.room_id == room_id) { + if let Some(existing_child) = children.get_mut(existing_index) { + existing_child.name = Some(display_name.clone()); + existing_child.display_name = display_name; + existing_child.state = Some(RoomState::Joined); + existing_child.num_joined_members = existing_child.num_joined_members.max(1); + } + } else { + children.push_back(SpaceRoom { + room_id, + canonical_alias: None, + name: Some(display_name.clone()), + display_name, + topic: None, + avatar_url: None, + room_type: None, + num_joined_members: 1, + join_rule: None, + world_readable: None, + guest_can_join: false, + is_direct: Some(false), + children_count: 0, + state: Some(RoomState::Joined), + heroes: None, + via: Vec::new(), + }); + } + + self.children_cache.insert(space_id.clone(), children); + self.is_loading = false; + self.expanded_spaces.insert(space_id); + self.rebuild_tree_entries(); + self.redraw(cx); + } + /// Handle receiving detailed children for a space. fn update_children_in_space(&mut self, cx: &mut Cx, space_id: &OwnedRoomId, children: &Vector) { self.children_cache.insert(space_id.clone(), children.clone()); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index d50dd7842..b428d37a7 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -11,6 +11,7 @@ use matrix_sdk::{ config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ api::{Direction, client::{ account::register::v3::Request as RegistrationRequest, + room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, error::ErrorKind, profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType, @@ -18,8 +19,10 @@ use matrix_sdk::{ }}, events::{ relation::RelationType, room::{ - message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource - }, MessageLikeEventType, StateEventType + encryption::RoomEncryptionEventContent, message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource + }, + space::{child::SpaceChildEventContent, parent::SpaceParentEventContent}, + InitialStateEvent, 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 }; @@ -38,7 +41,7 @@ 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 + add_room::{CreatableSpacesAction, CreateRoomAction, CreateRoomContext, 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}, @@ -717,6 +720,14 @@ pub enum MatrixRequest { user_profile: UserProfile, allow_create: bool, }, + /// Request to create a new room, optionally underneath a selected parent space. + CreateRoom { + room_name: String, + parent_space_id: Option, + context: CreateRoomContext, + }, + /// Request the list of joined spaces where the current user may create child rooms. + GetCreatableSpaces, /// Request to fetch profile information for the given user ID. GetUserProfile { user_id: OwnedUserId, @@ -1398,6 +1409,74 @@ async fn matrix_worker_task( }); } + MatrixRequest::CreateRoom { room_name, parent_space_id, context } => { + let Some(client) = get_client() else { continue }; + let _create_room_task = Handle::current().spawn(async move { + let mut request = CreateRoomRequest::new(); + request.name = Some(room_name.clone()); + request.preset = Some(RoomPreset::PrivateChat); + request.initial_state.push( + InitialStateEvent::with_empty_state_key( + RoomEncryptionEventContent::with_recommended_defaults(), + ).to_raw_any() + ); + + log!("Creating new room \"{room_name}\"..."); + match client.create_room(request).await { + Ok(room) => { + let mut space_link_error = None; + if let Some(space_id) = parent_space_id.as_ref() + && let Err(error) = attach_room_to_space(&client, &room, space_id).await + { + error!("Created room {} but failed to add it to space {space_id}: {error}", room.room_id()); + space_link_error = Some(error.to_string()); + } + + let room_name_id = RoomNameId::from_room(&room).await; + Cx::post_action(CreateRoomAction::Created { + room_name_id, + parent_space_id, + space_link_error, + context, + }); + } + Err(error) => { + error!("Failed to create room \"{room_name}\": {error}"); + Cx::post_action(CreateRoomAction::Failed { room_name, error, context }); + } + } + }); + } + + MatrixRequest::GetCreatableSpaces => { + let Some(client) = get_client() else { continue }; + let _creatable_spaces_task = Handle::current().spawn(async move { + let Some(user_id) = client.user_id().map(ToOwned::to_owned) else { + Cx::post_action(CreatableSpacesAction::Loaded { spaces: Vec::new() }); + return; + }; + + let mut spaces = Vec::new(); + for room in client.joined_rooms() { + if room.room_type() != Some(ruma::room::RoomType::Space) { + continue; + } + + let Ok(power_levels) = room.power_levels().await else { + continue; + }; + if !power_levels.user_can_send_state(&user_id, StateEventType::SpaceChild) { + continue; + } + + spaces.push(RoomNameId::from_room(&room).await); + } + + spaces.sort_by_cached_key(|space| space.to_string().to_lowercase()); + Cx::post_action(CreatableSpacesAction::Loaded { spaces }); + }); + } + MatrixRequest::GetUserProfile { user_id, room_id, local_only } => { let Some(client) = get_client() else { continue }; let _fetch_task = Handle::current().spawn(async move { @@ -2187,6 +2266,39 @@ async fn matrix_worker_task( bail!("matrix_worker_task task ended unexpectedly") } +async fn attach_room_to_space(client: &Client, child_room: &Room, space_id: &OwnedRoomId) -> Result<()> { + let user_id = client.user_id().ok_or_else(|| anyhow!("Current user ID not found"))?; + let space_room = client.get_room(space_id) + .ok_or_else(|| anyhow!("Selected space {space_id} was not found"))?; + let child_power_levels = child_room.power_levels().await?; + + let child_route = room_route_with_fallback(child_room).await; + space_room + .send_state_event_for_key(child_room.room_id(), SpaceChildEventContent::new(child_route)) + .await?; + + if child_power_levels.user_can_send_state(user_id, StateEventType::SpaceParent) { + let mut parent_content = SpaceParentEventContent::new(room_route_with_fallback(&space_room).await); + parent_content.canonical = true; + child_room + .send_state_event_for_key(space_room.room_id(), parent_content) + .await?; + } + + Ok(()) +} + +async fn room_route_with_fallback(room: &Room) -> Vec { + match room.route().await { + Ok(route) if !route.is_empty() => route, + Ok(_) | Err(_) => room.room_id() + .server_name() + .map(ToOwned::to_owned) + .into_iter() + .collect(), + } +} + /// The single global Tokio runtime that is used by all async tasks. static TOKIO_RUNTIME: Mutex> = Mutex::new(None); From 271ad5fafb87fd4dff829c3f10a965cd0f7e1bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 00:56:36 +0800 Subject: [PATCH 027/283] Migrate app service and BotFather management to robrix2 --- src/app.rs | 558 +++++-- src/home/create_bot_modal.rs | 309 ++++ src/home/delete_bot_modal.rs | 249 +++ src/home/home_screen.rs | 636 ++++---- src/home/mod.rs | 4 + src/home/room_context_menu.rs | 160 +- src/home/room_screen.rs | 2639 +++++++++++++++++++++++-------- src/home/rooms_list.rs | 685 +++++--- src/room/room_input_bar.rs | 351 ++-- src/settings/bot_settings.rs | 187 +++ src/settings/mod.rs | 2 + src/settings/settings_screen.rs | 72 +- src/sliding_sync.rs | 2047 +++++++++++++++--------- 13 files changed, 5628 insertions(+), 2271 deletions(-) create mode 100644 src/home/create_bot_modal.rs create mode 100644 src/home/delete_bot_modal.rs create mode 100644 src/settings/bot_settings.rs diff --git a/src/app.rs b/src/app.rs index f04e177d5..0ed4de033 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,17 +4,47 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, RoomId}}; +use matrix_sdk::{ + RoomState, + ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}, +}; 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 - }, 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::{ - VerificationModalAction, - VerificationModalWidgetRefExt, - } + 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, + }, + 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::{VerificationModalAction, VerificationModalWidgetRefExt}, }; script_mod! { @@ -51,7 +81,7 @@ script_mod! { close +: { draw_bg +: {color: #0, color_hover: #E81123, color_down: #FF0015} } } } - + body +: { padding: 0, @@ -80,7 +110,7 @@ script_mod! { image_viewer_modal_inner := ImageViewer {} } } - + // Context menus should be shown in front of other UI elements, // but behind verification modals. new_message_context_menu := NewMessageContextMenu { } @@ -164,16 +194,20 @@ app_main!(App); #[derive(Script)] pub struct App { - #[live] ui: WidgetRef, + #[live] + ui: WidgetRef, /// The top-level app state, shared across various parts of the app. - #[rust] app_state: AppState, + #[rust] + app_state: AppState, /// The details of a room we're waiting on to be loaded so that we can navigate to it. /// 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)>, + #[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, + #[rust] + mobile_room_nav_stack: Vec, } impl ScriptHook for App { @@ -198,15 +232,27 @@ impl MatchEvent for App { let _ = tracing_subscriber::fmt::try_init(); // Override Makepad's new default-JSON logger. We just want regular formatting. - fn regular_log(file_name: &str, line_start: u32, column_start: u32, _line_end: u32, _column_end: u32, message: String, level: LogLevel) { + fn regular_log( + file_name: &str, + line_start: u32, + column_start: u32, + _line_end: u32, + _column_end: u32, + message: String, + level: LogLevel, + ) { let l = match level { - LogLevel::Panic => "[!]", - LogLevel::Error => "[E]", + LogLevel::Panic => "[!]", + LogLevel::Error => "[E]", LogLevel::Warning => "[W]", - LogLevel::Log => "[I]", - LogLevel::Wait => "[.]", + LogLevel::Log => "[I]", + LogLevel::Wait => "[.]", }; - println!("{l} {file_name}:{}:{}: {message}", line_start + 1, column_start + 1); + println!( + "{l} {file_name}:{}:{}: {message}", + line_start + 1, + column_start + 1 + ); } *LOG_WITH_LEVEL.write().unwrap() = regular_log; @@ -221,7 +267,10 @@ impl MatchEvent for App { // Hide the caption bar on macOS and Linux, which use native window chrome. // On Windows (with custom chrome), the caption bar is needed. - if matches!(cx.os_type(), OsType::Macos | OsType::LinuxWindow(_) | OsType::LinuxDirect) { + if matches!( + cx.os_type(), + OsType::Macos | OsType::LinuxWindow(_) | OsType::LinuxDirect + ) { let mut window = self.ui.window(cx, ids!(main_window)); script_apply_eval!(cx, window, { show_caption_bar: false @@ -233,41 +282,52 @@ impl MatchEvent for App { log!("App::Startup: starting matrix sdk loop"); let _tokio_rt_handle = crate::sliding_sync::start_matrix_tokio().unwrap(); - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { log!("App::Startup: initializing TSP (Trust Spanning Protocol) module."); crate::tsp::tsp_init(_tokio_rt_handle).unwrap(); } } fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - let invite_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); + let invite_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); if let Some(_accepted) = invite_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(invite_confirmation_modal)).close(cx); } - let delete_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); + let delete_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); if let Some(_accepted) = delete_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(delete_confirmation_modal)).close(cx); } - let positive_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); + let positive_confirmation_modal_inner = self + .ui + .confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); if let Some(_accepted) = positive_confirmation_modal_inner.closed(actions) { - self.ui.modal(cx, ids!(positive_confirmation_modal)).close(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .close(cx); } for action in actions { match action.downcast_ref() { Some(LogoutConfirmModalAction::Open) => { - self.ui.logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)).reset_state(cx); + self.ui + .logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)) + .reset_state(cx); self.ui.modal(cx, ids!(logout_confirm_modal)).open(cx); continue; - }, + } Some(LogoutConfirmModalAction::Close { was_internal, .. }) => { if *was_internal { self.ui.modal(cx, ids!(logout_confirm_modal)).close(cx); } continue; - }, + } _ => {} } @@ -279,8 +339,8 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } - Some(LogoutAction::ClearAppState { on_clear_appstate }) => { - // Clear user profile cache, invited_rooms timeline states + Some(LogoutAction::ClearAppState { on_clear_appstate }) => { + // Clear user profile cache, invited_rooms timeline states clear_all_app_state(cx); // Reset all app state to its default. self.app_state = Default::default(); @@ -303,7 +363,9 @@ impl MatchEvent for App { // When not yet logged in, the login_screen widget handles displaying the failure modal. if let Some(LoginAction::LoginFailure(_)) = action.downcast_ref() { if self.app_state.logged_in { - log!("Received LoginAction::LoginFailure while logged in; showing login screen."); + log!( + "Received LoginAction::LoginFailure while logged in; showing login screen." + ); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); @@ -312,9 +374,13 @@ impl MatchEvent for App { } // Handle an action requesting to open the new message context menu. - if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { + if let MessageAction::OpenMessageContextMenu { details, abs_pos } = + action.as_widget_action().cast() + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); + let new_message_context_menu = self + .ui + .new_message_context_menu(cx, ids!(new_message_context_menu)); let expected_dimensions = new_message_context_menu.show(cx, details); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); @@ -335,7 +401,9 @@ impl MatchEvent for App { } // Handle an action requesting to open the room context menu. - if let RoomsListAction::OpenRoomContextMenu { details, pos } = action.as_widget_action().cast() { + if let RoomsListAction::OpenRoomContextMenu { details, pos } = + action.as_widget_action().cast() + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); let expected_dimensions = room_context_menu.show(cx, details); @@ -369,7 +437,9 @@ impl MatchEvent for App { // 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())); + cx.action(AppStateAction::UpgradedInviteToJoinedRoom( + room_name_id.room_id().clone(), + )); continue; } _ => {} @@ -413,18 +483,77 @@ impl MatchEvent for App { cx.action(MainDesktopUiAction::LoadDockFromAppState); continue; } - Some(AppStateAction::NavigateToRoom { room_to_close, destination_room }) => { + Some(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id, + warning, + }) => { + self.app_state + .bot_settings + .set_room_bound(room_id.clone(), *bound); + let kind = if warning.is_some() { + PopupKind::Warning + } else { + PopupKind::Success + }; + let message = match (*bound, bot_user_id.as_ref(), warning.as_deref()) { + (true, Some(bot_user_id), Some(warning)) => { + format!( + "BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}" + ) + } + (true, Some(bot_user_id), None) => { + format!("Bound room {room_id} to BotFather {bot_user_id}.") + } + (false, Some(bot_user_id), Some(warning)) => { + format!( + "Unbound BotFather {bot_user_id} from room {room_id}, with warning: {warning}" + ) + } + (false, Some(bot_user_id), None) => { + format!("Unbound BotFather {bot_user_id} from room {room_id}.") + } + (false, None, Some(warning)) => { + format!( + "Unbound room {room_id} from BotFather, with warning: {warning}" + ) + } + (false, None, None) => { + format!("Unbound room {room_id} from BotFather.") + } + (true, None, Some(warning)) => { + format!( + "BotFather is available for room {room_id}, with warning: {warning}" + ) + } + (true, None, None) => { + format!("Bound room {room_id} to BotFather.") + } + }; + enqueue_popup_notification(message, kind, Some(5.0)); + self.ui.redraw(cx); + continue; + } + Some(AppStateAction::NavigateToRoom { + room_to_close, + destination_room, + }) => { self.navigate_to_room(cx, room_to_close.as_ref(), destination_room); continue; } // If we successfully loaded a room that we were waiting on, // we can now navigate to it and optionally close a previous room. - Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) if - self.waiting_to_navigate_to_room.as_ref() + Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) + if self + .waiting_to_navigate_to_room + .as_ref() .is_some_and(|(dr, _)| dr.room_id() == room_name_id.room_id()) => { log!("Loaded awaited room {room_name_id:?}, navigating to it now..."); - if let Some((dest_room, room_to_close)) = self.waiting_to_navigate_to_room.take() { + if let Some((dest_room, room_to_close)) = + self.waiting_to_navigate_to_room.take() + { self.navigate_to_room(cx, room_to_close.as_ref(), &dest_room); } continue; @@ -434,18 +563,22 @@ impl MatchEvent for App { // Handle actions for showing or hiding the tooltip. match action.as_widget_action().cast() { - TooltipAction::HoverIn { text, widget_rect, options } => { + TooltipAction::HoverIn { + text, + widget_rect, + options, + } => { // Don't show any tooltips if the message context menu is currently shown. - if self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)).is_currently_shown(cx) { + if self + .ui + .new_message_context_menu(cx, ids!(new_message_context_menu)) + .is_currently_shown(cx) + { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - } - else { - self.ui.callout_tooltip(cx, ids!(app_tooltip)).show_with_options( - cx, - &text, - widget_rect, - options, - ); + } else { + self.ui + .callout_tooltip(cx, ids!(app_tooltip)) + .show_with_options(cx, &text, widget_rect, options); } continue; } @@ -479,7 +612,8 @@ impl MatchEvent for App { // // Note: other verification actions are handled by the verification modal itself. if let Some(VerificationAction::RequestReceived(state)) = action.downcast_ref() { - self.ui.verification_modal(cx, ids!(verification_modal_inner)) + self.ui + .verification_modal(cx, ids!(verification_modal_inner)) .initialize_with_data(cx, state.clone()); self.ui.modal(cx, ids!(verification_modal)).open(cx); continue; @@ -500,12 +634,23 @@ impl MatchEvent for App { _ => {} } // Handle actions to open/close the TSP verification modal. - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { use std::ops::Deref; - use crate::tsp::{tsp_verification_modal::{TspVerificationModalAction, TspVerificationModalWidgetRefExt}, TspIdentityAction}; + use crate::tsp::{ + tsp_verification_modal::{ + TspVerificationModalAction, TspVerificationModalWidgetRefExt, + }, + TspIdentityAction, + }; - if let Some(TspIdentityAction::ReceivedDidAssociationRequest { details, wallet_db }) = action.downcast_ref() { - self.ui.tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) + if let Some(TspIdentityAction::ReceivedDidAssociationRequest { + details, + wallet_db, + }) = action.downcast_ref() + { + self.ui + .tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) .initialize_with_details(cx, details.clone(), wallet_db.deref().clone()); self.ui.modal(cx, ids!(tsp_verification_modal)).open(cx); continue; @@ -517,7 +662,9 @@ impl MatchEvent for App { } // Handle a request to show the invite confirmation modal. - if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = action.downcast_ref() { + if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = + action.downcast_ref() + { if let Some(content) = content_opt.borrow_mut().take() { invite_confirmation_modal_inner.show(cx, content); self.ui.modal(cx, ids!(invite_confirmation_modal)).open(cx); @@ -526,10 +673,13 @@ impl MatchEvent for App { } // Handle a request to show the generic positive confirmation modal. - if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() { + if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() + { if let Some(content) = content_opt.borrow_mut().take() { positive_confirmation_modal_inner.show(cx, content); - self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .open(cx); } continue; } @@ -537,7 +687,9 @@ impl MatchEvent for App { // Handle a request to show the delete confirmation modal. if let Some(ConfirmDeleteAction::Show(content_opt)) = action.downcast_ref() { if let Some(content) = content_opt.borrow_mut().take() { - self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)).show(cx, content); + self.ui + .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)) + .show(cx, content); self.ui.modal(cx, ids!(delete_confirmation_modal)).open(cx); } continue; @@ -546,8 +698,10 @@ impl MatchEvent for App { // Handle InviteModalAction to open/close the invite modal. match action.downcast_ref() { Some(InviteModalAction::Open(room_name_id)) => { - self.ui.invite_modal(cx, ids!(invite_modal_inner)).show(cx, room_name_id.clone()); - self.ui.modal(cx, ids!(invite_modal)).open(cx); + self.ui + .invite_modal(cx, ids!(invite_modal_inner)) + .show(cx, room_name_id.clone()); + self.ui.modal(cx, ids!(invite_modal)).open(cx); continue; } Some(InviteModalAction::Close) => { @@ -559,8 +713,13 @@ impl MatchEvent for App { // Handle EventSourceModalAction to open/close the event source modal. match action.downcast_ref() { - Some(EventSourceModalAction::Open { room_id, event_id, original_json }) => { - self.ui.event_source_modal(cx, ids!(event_source_modal_inner)) + Some(EventSourceModalAction::Open { + room_id, + event_id, + original_json, + }) => { + self.ui + .event_source_modal(cx, ids!(event_source_modal_inner)) .show(cx, room_id.clone(), event_id.clone(), original_json.clone()); self.ui.modal(cx, ids!(event_source_modal)).open(cx); continue; @@ -575,7 +734,11 @@ impl MatchEvent for App { // Handle DirectMessageRoomActions match action.downcast_ref() { Some(DirectMessageRoomAction::FoundExisting { room_name_id, .. }) => { - self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); + self.navigate_to_room( + cx, + None, + &BasicRoomDetails::RoomId(room_name_id.clone()), + ); } Some(DirectMessageRoomAction::DidNotExist { user_profile }) => { let user_profile = user_profile.clone(); @@ -583,8 +746,7 @@ impl MatchEvent for App { Some(un) if !un.is_empty() => format!( "You don't have an existing direct message room with {} ({}).\n\n\ Would you like to create one now?", - un, - user_profile.user_id, + un, user_profile.user_id, ), _ => format!( "You don't have an existing direct message room with {}.\n\n\ @@ -612,17 +774,29 @@ impl MatchEvent for App { ..Default::default() }, ); - self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); + self.ui + .modal(cx, ids!(positive_confirmation_modal)) + .open(cx); } - Some(DirectMessageRoomAction::FailedToCreate { user_profile, error }) => { + Some(DirectMessageRoomAction::FailedToCreate { + user_profile, + error, + }) => { enqueue_popup_notification( - format!("Failed to create a new DM room with {}.\n\nError: {error}", user_profile.displayable_name()), + format!( + "Failed to create a new DM room with {}.\n\nError: {error}", + user_profile.displayable_name() + ), PopupKind::Error, None, ); } Some(DirectMessageRoomAction::NewlyCreated { room_name_id, .. }) => { - self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); + self.navigate_to_room( + cx, + None, + &BasicRoomDetails::RoomId(room_name_id.clone()), + ); } _ => {} } @@ -631,7 +805,7 @@ impl MatchEvent for App { } /// Clears all thread-local UI caches (user profiles, invited rooms, and timeline states). -/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, +/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, fn clear_all_app_state(cx: &mut Cx) { clear_user_profile_cache(cx); clear_all_invited_rooms(cx); @@ -683,27 +857,34 @@ impl AppMain for App { error!("Failed to save app state. Error: {e}"); } } - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { // Save the TSP wallet state, if it exists, with a 3-second timeout. let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap()); let res = crate::sliding_sync::block_on_async_with_timeout( Some(std::time::Duration::from_secs(3)), async move { match tsp_state.close_and_serialize().await { - Ok(saved_state) => match persistence::save_tsp_state_async(saved_state).await { - Ok(_) => { } - Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), + Ok(saved_state) => { + match persistence::save_tsp_state_async(saved_state).await { + Ok(_) => {} + Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), + } + } + Err(e) => { + error!("Failed to close and serialize TSP wallet state. Error: {e}") } - Err(e) => error!("Failed to close and serialize TSP wallet state. Error: {e}"), } }, ); if let Err(_e) = res { - error!("Failed to save TSP wallet state before app shutdown. Error: Timed Out."); + error!( + "Failed to save TSP wallet state before app shutdown. Error: Timed Out." + ); } } } - + // Forward events to the MatchEvent trait implementation. self.match_event(cx, event); let scope = &mut Scope::with_data(&mut self.app_state); @@ -751,8 +932,12 @@ impl App { .modal(cx, ids!(login_screen_view.login_screen.login_status_modal)) .close(cx); } - self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, show_login); - self.ui.view(cx, ids!(home_screen_view)).set_visible(cx, !show_login); + self.ui + .view(cx, ids!(login_screen_view)) + .set_visible(cx, show_login); + self.ui + .view(cx, ids!(home_screen_view)) + .set_visible(cx, !show_login); } /// Navigates to the given `destination_room`, optionally closing the `room_to_close`. @@ -767,16 +952,17 @@ impl App { let tab_id = LiveId::from_str(to_close.as_str()); let widget_uid = self.ui.widget_uid(); move |cx: &mut Cx| { - cx.widget_action( - widget_uid, - DockAction::TabCloseWasPressed(tab_id), - ); - enqueue_rooms_list_update(RoomsListUpdate::HideRoom { room_id: to_close.clone() }); + cx.widget_action(widget_uid, DockAction::TabCloseWasPressed(tab_id)); + enqueue_rooms_list_update(RoomsListUpdate::HideRoom { + room_id: to_close.clone(), + }); } }); let destination_room_id = destination_room.room_id(); - let room_state = cx.get_global::().get_room_state(destination_room_id); + let room_state = cx + .get_global::() + .get_room_state(destination_room_id); let new_selected_room = match room_state { Some(RoomState::Joined) => SelectedRoom::JoinedRoom { room_name_id: destination_room.room_name_id().clone(), @@ -786,11 +972,12 @@ impl App { }, // If the destination room is not yet loaded, show a join modal. _ => { - log!("Destination room {:?} not loaded, showing join modal...", destination_room.room_name_id()); - self.waiting_to_navigate_to_room = Some(( - destination_room.clone(), - room_to_close.cloned(), - )); + log!( + "Destination room {:?} not loaded, showing join modal...", + destination_room.room_name_id() + ); + self.waiting_to_navigate_to_room = + Some((destination_room.clone(), room_to_close.cloned())); cx.action(JoinLeaveRoomModalAction::Open { kind: JoinLeaveModalKind::JoinRoom { details: destination_room.clone(), @@ -802,8 +989,8 @@ impl App { } }; - - log!("Navigating to destination room {:?}, closing room {:?}", + log!( + "Navigating to destination room {:?}, closing room {:?}", destination_room.room_name_id(), room_to_close, ); @@ -814,7 +1001,7 @@ impl App { cx.action(NavigationBarAction::GoToHome); } cx.widget_action( - self.ui.widget_uid(), + self.ui.widget_uid(), RoomsListAction::Selected(new_selected_room), ); // Select and scroll to the destination room in the rooms list. @@ -830,27 +1017,43 @@ impl App { /// 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), + 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), + 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. @@ -884,7 +1087,11 @@ impl App { | 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 { + let thread_root = if let SelectedRoom::Thread { + thread_root_event_id, + .. + } = &selected_room + { Some(thread_root_event_id.clone()) } else { None @@ -910,8 +1117,16 @@ impl App { }; // 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()); + 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() { @@ -921,10 +1136,11 @@ impl App { 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 + .stack_navigation(cx, ids!(view_stack)) + .push(cx, view_id); self.ui.redraw(cx); } - } /// App-wide state that is stored persistently across multiple app runs @@ -950,6 +1166,91 @@ pub struct AppState { pub saved_dock_state_per_space: HashMap, /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, + /// Local configuration and UI state for bot-assisted room binding. + #[serde(default)] + pub bot_settings: BotSettingsState, +} + +/// Local bot integration settings persisted per Matrix account. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct BotSettingsState { + /// Whether bot-assisted room binding is enabled in the UI. + pub enabled: bool, + /// The configured botfather user, either as a full MXID or localpart. + pub botfather_user_id: String, + /// Rooms that Robrix currently considers bound to BotFather. + pub bound_rooms: Vec, +} + +impl Default for BotSettingsState { + fn default() -> Self { + Self { + enabled: false, + botfather_user_id: Self::DEFAULT_BOTFATHER_LOCALPART.to_string(), + bound_rooms: Vec::new(), + } + } +} + +impl BotSettingsState { + pub const DEFAULT_BOTFATHER_LOCALPART: &'static str = "bot"; + + /// Returns `true` if the given room is currently marked as bound locally. + pub fn is_room_bound(&self, room_id: &RoomId) -> bool { + self.bound_rooms + .iter() + .any(|bound_room_id| bound_room_id == room_id) + } + + /// Updates the local bound/unbound state for the given room. + pub fn set_room_bound(&mut self, room_id: OwnedRoomId, bound: bool) { + if bound { + if !self.is_room_bound(&room_id) { + self.bound_rooms.push(room_id); + self.bound_rooms + .sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + } + } else { + self.bound_rooms + .retain(|existing_room_id| existing_room_id != &room_id); + } + } + + /// Returns the configured botfather user ID, resolving a localpart against + /// the current user's homeserver when needed. + pub fn resolved_bot_user_id( + &self, + current_user_id: Option<&UserId>, + ) -> Result { + let raw = self.botfather_user_id.trim(); + if raw.starts_with('@') || raw.contains(':') { + let full_user_id = if raw.starts_with('@') { + raw.to_string() + } else { + format!("@{raw}") + }; + return UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid bot user ID: {full_user_id}")); + } + + let Some(current_user_id) = current_user_id else { + return Err( + "Current user ID is unavailable, so the bot homeserver cannot be resolved.".into(), + ); + }; + + let localpart = if raw.is_empty() { + Self::DEFAULT_BOTFATHER_LOCALPART + } else { + raw + }; + let full_user_id = format!("@{localpart}:{}", current_user_id.server_name()); + UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid bot user ID: {full_user_id}")) + } } /// A snapshot of the main dock: all state needed to restore the dock tabs/layout. @@ -966,7 +1267,6 @@ pub struct SavedDockState { pub selected_room: Option, } - /// Represents a room currently or previously selected by the user. /// /// ## PartialEq/Eq equality comparison behavior @@ -1023,9 +1323,7 @@ impl SelectedRoom { match self { SelectedRoom::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => { let name = room_name_id.clone(); - *self = SelectedRoom::JoinedRoom { - room_name_id: name, - }; + *self = SelectedRoom::JoinedRoom { room_name_id: name }; true } _ => false, @@ -1035,11 +1333,14 @@ impl SelectedRoom { /// Returns the `LiveId` of the room tab corresponding to this `SelectedRoom`. pub fn tab_id(&self) -> LiveId { match self { - SelectedRoom::Thread { room_name_id, thread_root_event_id } => { - LiveId::from_str( - &format!("{}##{}", room_name_id.room_id(), thread_root_event_id) - ) - } + SelectedRoom::Thread { + room_name_id, + thread_root_event_id, + } => LiveId::from_str(&format!( + "{}##{}", + room_name_id.room_id(), + thread_root_event_id + )), other => LiveId::from_str(other.room_id().as_str()), } } @@ -1093,6 +1394,13 @@ pub enum AppStateAction { /// The given app state was loaded from persistent storage /// and is ready to be restored. RestoreAppStateFromPersistentState(AppState), + /// A room-level BotFather bind or unbind action completed. + BotRoomBindingUpdated { + room_id: OwnedRoomId, + bound: bool, + bot_user_id: Option, + warning: Option, + }, /// The given room was successfully loaded from the homeserver /// and is now known to our client. /// diff --git a/src/home/create_bot_modal.rs b/src/home/create_bot_modal.rs new file mode 100644 index 000000000..bafb822e6 --- /dev/null +++ b/src/home/create_bot_modal.rs @@ -0,0 +1,309 @@ +//! A modal dialog for creating a Matrix child bot through BotFather slash commands. + +use makepad_widgets::*; + +use crate::utils::RoomNameId; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.CreateBotModalLabel = Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #333 + wrap: Word + } + text: "" + } + + mod.widgets.CreateBotModal = #(CreateBotModal::register_widget(vm)) { + width: Fit + height: Fit + + RoundedView { + width: 448 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 28, right: 24, bottom: 20, left: 24} + spacing: 16 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT { font_size: 13 } + color: #000 + wrap: Word + } + text: "Create Bot" + } + + body := mod.widgets.CreateBotModalLabel { + text: "" + } + + form := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 12 + padding: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + } + + username_label := mod.widgets.CreateBotModalLabel { + text: "Username" + } + + username_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "weather" + } + + username_hint := mod.widgets.CreateBotModalLabel { + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9.5 } + color: #666 + } + text: "Lowercase letters, digits, and underscores only. BotFather will create @bot_:server." + } + + display_name_label := mod.widgets.CreateBotModalLabel { + text: "Display Name" + } + + display_name_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "Weather Bot" + } + + prompt_label := mod.widgets.CreateBotModalLabel { + text: "System Prompt (Optional)" + } + + prompt_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "You are a weather assistant." + } + } + + status_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #000 + wrap: Word + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 16 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + create_button := RobrixPositiveIconButton { + width: 130 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Create Bot" + } + } + } + } +} + +fn is_valid_bot_username(username: &str) -> bool { + !username.is_empty() + && username + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CreateBotRequest { + pub username: String, + pub display_name: String, + pub system_prompt: Option, +} + +#[derive(Clone, Debug)] +pub enum CreateBotModalAction { + Close, + Submit(CreateBotRequest), +} + +#[derive(Script, ScriptHook, Widget)] +pub struct CreateBotModal { + #[deref] + view: View, + #[rust] + room_name_id: Option, + #[rust] + is_showing_error: bool, +} + +impl Widget for CreateBotModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_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 WidgetMatchEvent for CreateBotModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let cancel_button = self.view.button(cx, ids!(buttons.cancel_button)); + let create_button = self.view.button(cx, ids!(buttons.create_button)); + let username_input = self.view.text_input(cx, ids!(form.username_input)); + let display_name_input = self.view.text_input(cx, ids!(form.display_name_input)); + let prompt_input = self.view.text_input(cx, ids!(form.prompt_input)); + let mut status_label = self.view.label(cx, ids!(status_label)); + + if cancel_button.clicked(actions) + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + { + cx.action(CreateBotModalAction::Close); + return; + } + + if self.is_showing_error + && (username_input.changed(actions).is_some() + || display_name_input.changed(actions).is_some() + || prompt_input.changed(actions).is_some()) + { + self.is_showing_error = false; + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if create_button.clicked(actions) || prompt_input.returned(actions).is_some() { + let username = username_input.text().trim().to_string(); + if !is_valid_bot_username(&username) { + self.is_showing_error = true; + script_apply_eval!(cx, status_label, { + text: "Username must use lowercase letters, digits, or underscores." + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + } + + let display_name = display_name_input.text().trim().to_string(); + let system_prompt = prompt_input.text().trim().to_string(); + + cx.action(CreateBotModalAction::Submit(CreateBotRequest { + username: username.clone(), + display_name: if display_name.is_empty() { + username + } else { + display_name + }, + system_prompt: (!system_prompt.is_empty()).then_some(system_prompt), + })); + } + } +} + +impl CreateBotModal { + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { + self.room_name_id = Some(room_name_id.clone()); + self.is_showing_error = false; + + self.view + .label(cx, ids!(title)) + .set_text(cx, "Create Room Bot"); + self.view.label(cx, ids!(body)).set_text( + cx, + &format!( + "Robrix will send `/createbot` to BotFather in {}. The bot becomes available immediately after octos creates it.", + room_name_id + ), + ); + self.view + .text_input(cx, ids!(form.username_input)) + .set_text(cx, ""); + self.view + .text_input(cx, ids!(form.display_name_input)) + .set_text(cx, ""); + self.view + .text_input(cx, ids!(form.prompt_input)) + .set_text(cx, ""); + self.view.label(cx, ids!(status_label)).set_text(cx, ""); + self.view + .button(cx, ids!(buttons.create_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.cancel_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.create_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(buttons.cancel_button)) + .reset_hover(cx); + self.view.redraw(cx); + } +} + +impl CreateBotModalRef { + pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.show(cx, room_name_id); + } +} diff --git a/src/home/delete_bot_modal.rs b/src/home/delete_bot_modal.rs new file mode 100644 index 000000000..caab2bd49 --- /dev/null +++ b/src/home/delete_bot_modal.rs @@ -0,0 +1,249 @@ +//! A modal dialog for deleting a Matrix bot through BotFather slash commands. + +use makepad_widgets::*; + +use crate::utils::RoomNameId; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.DeleteBotModalLabel = Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #333 + wrap: Word + } + text: "" + } + + mod.widgets.DeleteBotModal = #(DeleteBotModal::register_widget(vm)) { + width: Fit + height: Fit + + RoundedView { + width: 448 + height: Fit + align: Align{x: 0.5} + flow: Down + padding: Inset{top: 28, right: 24, bottom: 20, left: 24} + spacing: 16 + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 6.0 + } + + title := Label { + width: Fill + height: Fit + draw_text +: { + text_style: TITLE_TEXT { font_size: 13 } + color: #000 + wrap: Word + } + text: "Delete Bot" + } + + body := mod.widgets.DeleteBotModalLabel { + text: "" + } + + form := RoundedView { + width: Fill + height: Fit + flow: Down + spacing: 12 + padding: 14 + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + } + + user_id_label := mod.widgets.DeleteBotModalLabel { + text: "Bot Matrix User ID" + } + + user_id_input := RobrixTextInput { + width: Fill + height: Fit + padding: 10 + draw_text +: { + text_style: REGULAR_TEXT { font_size: 11.5 } + color: #000 + } + empty_text: "@bot_weather:server or bot_weather" + } + + user_id_hint := mod.widgets.DeleteBotModalLabel { + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9.5 } + color: #666 + } + text: "Use the full Matrix user ID when possible. A plain localpart like `bot_weather` will be resolved on your current homeserver." + } + } + + status_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: #000 + wrap: Word + } + text: "" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + spacing: 16 + + cancel_button := RobrixNeutralIconButton { + width: 110 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_FORBIDDEN) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Cancel" + } + + delete_button := RobrixNegativeIconButton { + width: 130 + align: Align{x: 0.5, y: 0.5} + padding: 12 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Delete Bot" + } + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeleteBotRequest { + pub user_id_or_localpart: String, +} + +#[derive(Clone, Debug)] +pub enum DeleteBotModalAction { + Close, + Submit(DeleteBotRequest), +} + +#[derive(Script, ScriptHook, Widget)] +pub struct DeleteBotModal { + #[deref] + view: View, + #[rust] + room_name_id: Option, + #[rust] + is_showing_error: bool, +} + +impl Widget for DeleteBotModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_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 WidgetMatchEvent for DeleteBotModal { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let cancel_button = self.view.button(cx, ids!(buttons.cancel_button)); + let delete_button = self.view.button(cx, ids!(buttons.delete_button)); + let user_id_input = self.view.text_input(cx, ids!(form.user_id_input)); + let mut status_label = self.view.label(cx, ids!(status_label)); + + if cancel_button.clicked(actions) + || actions + .iter() + .any(|a| matches!(a.downcast_ref(), Some(ModalAction::Dismissed))) + { + cx.action(DeleteBotModalAction::Close); + return; + } + + if self.is_showing_error && user_id_input.changed(actions).is_some() { + self.is_showing_error = false; + status_label.set_text(cx, ""); + self.view.redraw(cx); + } + + if delete_button.clicked(actions) || user_id_input.returned(actions).is_some() { + let user_id_or_localpart = user_id_input.text().trim().to_string(); + if user_id_or_localpart.is_empty() { + self.is_showing_error = true; + script_apply_eval!(cx, status_label, { + text: "Enter the bot Matrix user ID or localpart to delete." + draw_text +: { + color: mod.widgets.COLOR_FG_DANGER_RED + } + }); + self.view.redraw(cx); + return; + } + + cx.action(DeleteBotModalAction::Submit(DeleteBotRequest { + user_id_or_localpart, + })); + } + } +} + +impl DeleteBotModal { + pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { + self.room_name_id = Some(room_name_id.clone()); + self.is_showing_error = false; + + self.view + .label(cx, ids!(title)) + .set_text(cx, "Delete Room Bot"); + self.view.label(cx, ids!(body)).set_text( + cx, + &format!( + "Robrix will send `/deletebot` to BotFather in {}. This only removes bots already managed by octos.", + room_name_id + ), + ); + self.view + .text_input(cx, ids!(form.user_id_input)) + .set_text(cx, ""); + self.view.label(cx, ids!(status_label)).set_text(cx, ""); + self.view + .button(cx, ids!(buttons.delete_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.cancel_button)) + .set_enabled(cx, true); + self.view + .button(cx, ids!(buttons.delete_button)) + .reset_hover(cx); + self.view + .button(cx, ids!(buttons.cancel_button)) + .reset_hover(cx); + self.view.redraw(cx); + } +} + +impl DeleteBotModalRef { + pub fn show(&self, cx: &mut Cx, room_name_id: RoomNameId) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.show(cx, room_name_id); + } +} diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 910f817e1..c45c7309f 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,365 +1,371 @@ use makepad_widgets::*; -use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; +use crate::{ + app::AppState, + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, + settings::settings_screen::SettingsScreenWidgetRefExt, +}; script_mod! { - use mod.prelude.widgets.* - use mod.widgets.* - - - // Defines the total height of the StackNavigationView's header. - // This has to be set in multiple places because of how StackNavigation - // uses an Overlay view internally. - mod.widgets.STACK_VIEW_HEADER_HEIGHT = 75 - - // A reusable base for StackNavigationView children in the mobile layout. - // Each specific content view (room, invite, space lobby) extends this - // and places its own screen widget inside the body. - mod.widgets.RobrixContentView = StackNavigationView { - width: Fill, height: Fill - draw_bg.color: (COLOR_PRIMARY) - header +: { - clip_x: false, - clip_y: false, - show_bg: true, - draw_bg +: { - color: instance((COLOR_PRIMARY_DARKER)) - color_dither: uniform(1.0) - gradient_border_horizontal: uniform(0.0) - gradient_fill_horizontal: uniform(0.0) - color_2: instance(vec4(-1)) - - border_radius: uniform(4.0) - border_size: uniform(0.0) - border_color: instance(#0000) - border_color_2: instance(vec4(-1)) - - shadow_color: instance(#0005) - shadow_radius: uniform(9.0) - shadow_offset: uniform(vec2(1.0, 0.0)) - - rect_size2: varying(vec2(0)) - rect_size3: varying(vec2(0)) - rect_pos2: varying(vec2(0)) - rect_shift: varying(vec2(0)) - sdf_rect_pos: varying(vec2(0)) - sdf_rect_size: varying(vec2(0)) - - vertex: fn() { - let min_offset = min(self.shadow_offset vec2(0)) - self.rect_size2 = self.rect_size + 2.0*vec2(self.shadow_radius) - self.rect_size3 = self.rect_size2 + abs(self.shadow_offset) - self.rect_pos2 = self.rect_pos - vec2(self.shadow_radius) + min_offset - self.sdf_rect_size = self.rect_size2 - vec2(self.shadow_radius * 2.0 + self.border_size * 2.0) - self.sdf_rect_pos = -min_offset + vec2(self.border_size + self.shadow_radius) - self.rect_shift = -min_offset - - return self.clip_and_transform_vertex(self.rect_pos2 self.rect_size3) - } +use mod.prelude.widgets.* +use mod.widgets.* + + +// Defines the total height of the StackNavigationView's header. +// This has to be set in multiple places because of how StackNavigation +// uses an Overlay view internally. +mod.widgets.STACK_VIEW_HEADER_HEIGHT = 75 + +// A reusable base for StackNavigationView children in the mobile layout. +// Each specific content view (room, invite, space lobby) extends this +// and places its own screen widget inside the body. +mod.widgets.RobrixContentView = StackNavigationView { + width: Fill, height: Fill + draw_bg.color: (COLOR_PRIMARY) + header +: { + clip_x: false, + clip_y: false, + show_bg: true, + draw_bg +: { + color: instance((COLOR_PRIMARY_DARKER)) + color_dither: uniform(1.0) + gradient_border_horizontal: uniform(0.0) + gradient_fill_horizontal: uniform(0.0) + color_2: instance(vec4(-1)) + + border_radius: uniform(4.0) + border_size: uniform(0.0) + border_color: instance(#0000) + border_color_2: instance(vec4(-1)) + + shadow_color: instance(#0005) + shadow_radius: uniform(9.0) + shadow_offset: uniform(vec2(1.0, 0.0)) + + rect_size2: varying(vec2(0)) + rect_size3: varying(vec2(0)) + rect_pos2: varying(vec2(0)) + rect_shift: varying(vec2(0)) + sdf_rect_pos: varying(vec2(0)) + sdf_rect_size: varying(vec2(0)) + + vertex: fn() { + let min_offset = min(self.shadow_offset vec2(0)) + self.rect_size2 = self.rect_size + 2.0*vec2(self.shadow_radius) + self.rect_size3 = self.rect_size2 + abs(self.shadow_offset) + self.rect_pos2 = self.rect_pos - vec2(self.shadow_radius) + min_offset + self.sdf_rect_size = self.rect_size2 - vec2(self.shadow_radius * 2.0 + self.border_size * 2.0) + self.sdf_rect_pos = -min_offset + vec2(self.border_size + self.shadow_radius) + self.rect_shift = -min_offset + + return self.clip_and_transform_vertex(self.rect_pos2 self.rect_size3) + } - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size3) + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size3) - let mut fill_color = self.color - if self.color_2.x > -0.5 { - let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither - let dir = if self.gradient_fill_horizontal > 0.5 self.pos.x else self.pos.y - fill_color = mix(self.color self.color_2 dir + dither) - } + let mut fill_color = self.color + if self.color_2.x > -0.5 { + let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither + let dir = if self.gradient_fill_horizontal > 0.5 self.pos.x else self.pos.y + fill_color = mix(self.color self.color_2 dir + dither) + } - let mut stroke_color = self.border_color - if self.border_color_2.x > -0.5 { - let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither - let dir = if self.gradient_border_horizontal > 0.5 self.pos.x else self.pos.y - stroke_color = mix(self.border_color self.border_color_2 dir + dither) - } + let mut stroke_color = self.border_color + if self.border_color_2.x > -0.5 { + let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither + let dir = if self.gradient_border_horizontal > 0.5 self.pos.x else self.pos.y + stroke_color = mix(self.border_color self.border_color_2 dir + dither) + } - sdf.box( - self.sdf_rect_pos.x - self.sdf_rect_pos.y - self.sdf_rect_size.x - self.sdf_rect_size.y - max(1.0 self.border_radius) - ) - if sdf.shape > -1.0 { - let m = self.shadow_radius - let o = self.shadow_offset + self.rect_shift - let v = GaussShadow.rounded_box_shadow(vec2(m) + o self.rect_size2+o self.pos * (self.rect_size3+vec2(m)) self.shadow_radius*0.5 self.border_radius*2.0) - sdf.clear(self.shadow_color*v) - } + sdf.box( + self.sdf_rect_pos.x + self.sdf_rect_pos.y + self.sdf_rect_size.x + self.sdf_rect_size.y + max(1.0 self.border_radius) + ) + if sdf.shape > -1.0 { + let m = self.shadow_radius + let o = self.shadow_offset + self.rect_shift + let v = GaussShadow.rounded_box_shadow(vec2(m) + o self.rect_size2+o self.pos * (self.rect_size3+vec2(m)) self.shadow_radius*0.5 self.border_radius*2.0) + sdf.clear(self.shadow_color*v) + } - sdf.fill_keep(fill_color) + sdf.fill_keep(fill_color) - if self.border_size > 0.0 { - sdf.stroke(stroke_color self.border_size) - } - return sdf.result + if self.border_size > 0.0 { + sdf.stroke(stroke_color self.border_size) } + return sdf.result } + } - padding: Inset{top: 30, bottom: 0} - height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT), - - content +: { - height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) - button_container +: { - padding: 0, - margin: 0 - left_button +: { - width: Fit, height: Fit, - padding: Inset{left: 20, right: 23, top: 10, bottom: 10} - margin: Inset{left: 8, right: 0, top: 0, bottom: 0} - draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } - icon_walk: Walk{width: 13, height: Fit} - spacing: 0 - text: "" - } + padding: Inset{top: 30, bottom: 0} + height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT), + + content +: { + height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) + button_container +: { + padding: 0, + margin: 0 + left_button +: { + width: Fit, height: Fit, + padding: Inset{left: 20, right: 23, top: 10, bottom: 10} + margin: Inset{left: 8, right: 0, top: 0, bottom: 0} + draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } + icon_walk: Walk{width: 13, height: Fit} + spacing: 0 + text: "" } - title_container +: { - padding: Inset{top: 8} - title +: { - draw_text +: { - color: (ROOM_NAME_TEXT_COLOR) - } + } + title_container +: { + padding: Inset{top: 8} + title +: { + draw_text +: { + color: (ROOM_NAME_TEXT_COLOR) } } } } - body +: { - margin: Inset{top: (mod.widgets.STACK_VIEW_HEADER_HEIGHT)} - } } + body +: { + margin: Inset{top: (mod.widgets.STACK_VIEW_HEADER_HEIGHT)} + } +} - // A wrapper view around the SpacesBar that lets us show/hide it via animation. - mod.widgets.SpacesBarWrapper = set_type_default() do #(SpacesBarWrapper::register_widget(vm)) { - ..mod.widgets.RoundedShadowView - - width: Fill, - height: (NAVIGATION_TAB_BAR_SIZE) - margin: Inset{left: 4, right: 4} - show_bg: true - draw_bg +: { - color: (COLOR_PRIMARY_DARKER) - border_radius: 4.0 - border_size: 0.0 - shadow_color: #0005 - shadow_radius: 15.0 - shadow_offset: vec2(1.0, 0.0) - } +// A wrapper view around the SpacesBar that lets us show/hide it via animation. +mod.widgets.SpacesBarWrapper = set_type_default() do #(SpacesBarWrapper::register_widget(vm)) { + ..mod.widgets.RoundedShadowView + + width: Fill, + height: (NAVIGATION_TAB_BAR_SIZE) + margin: Inset{left: 4, right: 4} + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY_DARKER) + border_radius: 4.0 + border_size: 0.0 + shadow_color: #0005 + shadow_radius: 15.0 + shadow_offset: vec2(1.0, 0.0) + } - CachedWidget { - root_spaces_bar := mod.widgets.SpacesBar {} - } + CachedWidget { + root_spaces_bar := mod.widgets.SpacesBar {} + } - animator: Animator{ - spaces_bar_animator: { - default: @hide - show: AnimatorState{ - redraw: true - from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } - apply: { height: (NAVIGATION_TAB_BAR_SIZE), draw_bg: { shadow_color: #x00000055 } } - } - hide: AnimatorState{ - redraw: true - from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } - apply: { height: 0, draw_bg: { shadow_color: (COLOR_TRANSPARENT) } } - } + animator: Animator{ + spaces_bar_animator: { + default: @hide + show: AnimatorState{ + redraw: true + from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } + apply: { height: (NAVIGATION_TAB_BAR_SIZE), draw_bg: { shadow_color: #x00000055 } } + } + hide: AnimatorState{ + redraw: true + from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } + apply: { height: 0, draw_bg: { shadow_color: (COLOR_TRANSPARENT) } } } } } +} - // The home screen widget contains the main content: - // rooms list, room screens, and the settings screen as an overlay. - // It adapts to both desktop and mobile layouts. - mod.widgets.HomeScreen = #(HomeScreen::register_widget(vm)) { - AdaptiveView { - // NOTE: within each of these sub views, we used `CachedWidget` wrappers - // to ensure that there is only a single global instance of each - // of those widgets, which means they maintain their state - // across transitions between the Desktop and Mobile variant. - Desktop := SolidView { - width: Fill, height: Fill - flow: Right - align: Align{x: 0.0, y: 0.0} - padding: 0, - margin: 0, +// The home screen widget contains the main content: +// rooms list, room screens, and the settings screen as an overlay. +// It adapts to both desktop and mobile layouts. +mod.widgets.HomeScreen = #(HomeScreen::register_widget(vm)) { + AdaptiveView { + // NOTE: within each of these sub views, we used `CachedWidget` wrappers + // to ensure that there is only a single global instance of each + // of those widgets, which means they maintain their state + // across transitions between the Desktop and Mobile variant. + Desktop := SolidView { + width: Fill, height: Fill + flow: Right + align: Align{x: 0.0, y: 0.0} + padding: 0, + margin: 0, + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + } - show_bg: true - draw_bg +: { - color: (COLOR_SECONDARY) - } + // On the left, show the navigation tab bar vertically. + CachedWidget { + navigation_tab_bar := mod.widgets.NavigationTabBar {} + } - // On the left, show the navigation tab bar vertically. - CachedWidget { - navigation_tab_bar := mod.widgets.NavigationTabBar {} - } + // To the right of that, we use the PageFlip widget to show either + // the main desktop UI or the settings screen. + home_screen_page_flip := PageFlip { + width: Fill, height: Fill - // To the right of that, we use the PageFlip widget to show either - // the main desktop UI or the settings screen. - home_screen_page_flip := PageFlip { + lazy_init: true, + active_page: @home_page + + home_page := View { width: Fill, height: Fill + flow: Down - lazy_init: true, - active_page: @home_page + View { + width: Fill, + height: 39, + flow: Right + padding: Inset{top: 2, bottom: 2} + margin: Inset{right: 2} + spacing: 2 + align: Align{y: 0.5} - home_page := View { - width: Fill, height: Fill - flow: Down + CachedWidget { + room_filter_input_bar := RoomFilterInputBar {} + } - View { - width: Fill, - height: 39, - flow: Right - padding: Inset{top: 2, bottom: 2} + search_messages_button := SearchMessagesButton { + // make this button match/align with the RoomFilterInputBar + height: 32.5, margin: Inset{right: 2} - spacing: 2 - align: Align{y: 0.5} - - CachedWidget { - room_filter_input_bar := RoomFilterInputBar {} - } - - search_messages_button := SearchMessagesButton { - // make this button match/align with the RoomFilterInputBar - height: 32.5, - margin: Inset{right: 2} - } } - - mod.widgets.MainDesktopUI {} } - settings_page := SolidView { - width: Fill, height: Fill - show_bg: true, - draw_bg.color: (COLOR_PRIMARY) + mod.widgets.MainDesktopUI {} + } - CachedWidget { - settings_screen := mod.widgets.SettingsScreen {} - } + settings_page := SolidView { + width: Fill, height: Fill + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) + + CachedWidget { + settings_screen := mod.widgets.SettingsScreen {} } + } - add_room_page := SolidView { - width: Fill, height: Fill - show_bg: true, - draw_bg.color: (COLOR_PRIMARY) + add_room_page := SolidView { + width: Fill, height: Fill + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) - CachedWidget { - add_room_screen := mod.widgets.AddRoomScreen {} - } + CachedWidget { + add_room_screen := mod.widgets.AddRoomScreen {} } } } + } - Mobile := SolidView { - width: Fill, height: Fill - flow: Down + Mobile := SolidView { + width: Fill, height: Fill + flow: Down - show_bg: true - draw_bg.color: (COLOR_PRIMARY) + show_bg: true + draw_bg.color: (COLOR_PRIMARY) - view_stack := StackNavigation { - root_view +: { - flow: Down - width: Fill, height: Fill + view_stack := StackNavigation { + root_view +: { + flow: Down + width: Fill, height: Fill - // At the top of the root view, we use the PageFlip widget to show either - // the main list of rooms or the settings screen. - home_screen_page_flip := PageFlip { - width: Fill, height: Fill + // At the top of the root view, we use the PageFlip widget to show either + // the main list of rooms or the settings screen. + home_screen_page_flip := PageFlip { + width: Fill, height: Fill - lazy_init: true, - active_page: @home_page + lazy_init: true, + active_page: @home_page - home_page := View { - width: Fill, height: Fill - // Note: while the other page views have top padding, we do NOT add that here - // because it is added in the `RoomsSideBar`'s `RoundedShadowView` itself. - flow: Down + home_page := View { + width: Fill, height: Fill + // Note: while the other page views have top padding, we do NOT add that here + // because it is added in the `RoomsSideBar`'s `RoundedShadowView` itself. + flow: Down - mod.widgets.RoomsSideBar {} - } + mod.widgets.RoomsSideBar {} + } - settings_page := View { - width: Fill, height: Fill - padding: Inset{top: 20} + settings_page := View { + width: Fill, height: Fill + padding: Inset{top: 20} - CachedWidget { - settings_screen := mod.widgets.SettingsScreen {} - } + CachedWidget { + settings_screen := mod.widgets.SettingsScreen {} } + } - add_room_page := View { - width: Fill, height: Fill - padding: Inset{top: 20} + add_room_page := View { + width: Fill, height: Fill + padding: Inset{top: 20} - CachedWidget { - add_room_screen := mod.widgets.AddRoomScreen {} - } + CachedWidget { + add_room_screen := mod.widgets.AddRoomScreen {} } } + } - // Show the SpacesBar right above the navigation tab bar. - // We wrap it in the SpacesBarWrapper in order to animate it in or out, - // and wrap *that* in a CachedWidget in order to maintain its shown/hidden state - // across AdaptiveView transitions between Mobile view mode and Desktop view mode. - // - // ... Then we wrap *that* in a ... - CachedWidget { - spaces_bar_wrapper := mod.widgets.SpacesBarWrapper {} - } + // Show the SpacesBar right above the navigation tab bar. + // We wrap it in the SpacesBarWrapper in order to animate it in or out, + // and wrap *that* in a CachedWidget in order to maintain its shown/hidden state + // across AdaptiveView transitions between Mobile view mode and Desktop view mode. + // + // ... Then we wrap *that* in a ... + CachedWidget { + spaces_bar_wrapper := mod.widgets.SpacesBarWrapper {} + } - // At the bottom of the root view, show the navigation tab bar horizontally. - CachedWidget { - navigation_tab_bar := mod.widgets.NavigationTabBar {} - } + // At the bottom of the root view, show the navigation tab bar horizontally. + CachedWidget { + navigation_tab_bar := mod.widgets.NavigationTabBar {} } + } - // Room views: multiple instances to support deep stacking - // (e.g., room -> thread -> room -> thread -> ...). - // Each stack depth gets its own dedicated view widget, - // avoiding complex state save/restore when views are reused. - room_view_0 := mod.widgets.RobrixContentView { body +: { room_screen_0 := mod.widgets.RoomScreen {} } } - room_view_1 := mod.widgets.RobrixContentView { body +: { room_screen_1 := mod.widgets.RoomScreen {} } } - room_view_2 := mod.widgets.RobrixContentView { body +: { room_screen_2 := mod.widgets.RoomScreen {} } } - room_view_3 := mod.widgets.RobrixContentView { body +: { room_screen_3 := mod.widgets.RoomScreen {} } } - room_view_4 := mod.widgets.RobrixContentView { body +: { room_screen_4 := mod.widgets.RoomScreen {} } } - room_view_5 := mod.widgets.RobrixContentView { body +: { room_screen_5 := mod.widgets.RoomScreen {} } } - room_view_6 := mod.widgets.RobrixContentView { body +: { room_screen_6 := mod.widgets.RoomScreen {} } } - room_view_7 := mod.widgets.RobrixContentView { body +: { room_screen_7 := mod.widgets.RoomScreen {} } } - room_view_8 := mod.widgets.RobrixContentView { body +: { room_screen_8 := mod.widgets.RoomScreen {} } } - room_view_9 := mod.widgets.RobrixContentView { body +: { room_screen_9 := mod.widgets.RoomScreen {} } } - room_view_10 := mod.widgets.RobrixContentView { body +: { room_screen_10 := mod.widgets.RoomScreen {} } } - room_view_11 := mod.widgets.RobrixContentView { body +: { room_screen_11 := mod.widgets.RoomScreen {} } } - room_view_12 := mod.widgets.RobrixContentView { body +: { room_screen_12 := mod.widgets.RoomScreen {} } } - room_view_13 := mod.widgets.RobrixContentView { body +: { room_screen_13 := mod.widgets.RoomScreen {} } } - room_view_14 := mod.widgets.RobrixContentView { body +: { room_screen_14 := mod.widgets.RoomScreen {} } } - room_view_15 := mod.widgets.RobrixContentView { body +: { room_screen_15 := mod.widgets.RoomScreen {} } } - - invite_view := mod.widgets.RobrixContentView { - body +: { - invite_screen := mod.widgets.InviteScreen {} - } + // Room views: multiple instances to support deep stacking + // (e.g., room -> thread -> room -> thread -> ...). + // Each stack depth gets its own dedicated view widget, + // avoiding complex state save/restore when views are reused. + room_view_0 := mod.widgets.RobrixContentView { body +: { room_screen_0 := mod.widgets.RoomScreen {} } } + room_view_1 := mod.widgets.RobrixContentView { body +: { room_screen_1 := mod.widgets.RoomScreen {} } } + room_view_2 := mod.widgets.RobrixContentView { body +: { room_screen_2 := mod.widgets.RoomScreen {} } } + room_view_3 := mod.widgets.RobrixContentView { body +: { room_screen_3 := mod.widgets.RoomScreen {} } } + room_view_4 := mod.widgets.RobrixContentView { body +: { room_screen_4 := mod.widgets.RoomScreen {} } } + room_view_5 := mod.widgets.RobrixContentView { body +: { room_screen_5 := mod.widgets.RoomScreen {} } } + room_view_6 := mod.widgets.RobrixContentView { body +: { room_screen_6 := mod.widgets.RoomScreen {} } } + room_view_7 := mod.widgets.RobrixContentView { body +: { room_screen_7 := mod.widgets.RoomScreen {} } } + room_view_8 := mod.widgets.RobrixContentView { body +: { room_screen_8 := mod.widgets.RoomScreen {} } } + room_view_9 := mod.widgets.RobrixContentView { body +: { room_screen_9 := mod.widgets.RoomScreen {} } } + room_view_10 := mod.widgets.RobrixContentView { body +: { room_screen_10 := mod.widgets.RoomScreen {} } } + room_view_11 := mod.widgets.RobrixContentView { body +: { room_screen_11 := mod.widgets.RoomScreen {} } } + room_view_12 := mod.widgets.RobrixContentView { body +: { room_screen_12 := mod.widgets.RoomScreen {} } } + room_view_13 := mod.widgets.RobrixContentView { body +: { room_screen_13 := mod.widgets.RoomScreen {} } } + room_view_14 := mod.widgets.RobrixContentView { body +: { room_screen_14 := mod.widgets.RoomScreen {} } } + room_view_15 := mod.widgets.RobrixContentView { body +: { room_screen_15 := mod.widgets.RoomScreen {} } } + + invite_view := mod.widgets.RobrixContentView { + body +: { + invite_screen := mod.widgets.InviteScreen {} } + } - space_lobby_view := mod.widgets.RobrixContentView { - body +: { - space_lobby_screen := mod.widgets.SpaceLobbyScreen {} - } + space_lobby_view := mod.widgets.RobrixContentView { + body +: { + space_lobby_screen := mod.widgets.SpaceLobbyScreen {} } } } } } } - +} /// A simple wrapper around the SpacesBar that allows us to animate showing or hiding it. #[derive(Script, ScriptHook, Widget, Animator)] pub struct SpacesBarWrapper { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, } impl Widget for SpacesBarWrapper { @@ -384,7 +390,9 @@ impl Widget for SpacesBarWrapper { impl SpacesBarWrapperRef { /// Shows or hides the spaces bar by animating it in or out. fn show_or_hide(&self, cx: &mut Cx, show: bool) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; if show { inner.animator_play(cx, ids!(spaces_bar_animator.show)); } else { @@ -394,18 +402,20 @@ impl SpacesBarWrapperRef { } } - #[derive(Script, ScriptHook, Widget)] pub struct HomeScreen { - #[deref] view: View, + #[deref] + view: View, /// The previously-selected navigation tab, used to determine which tab /// and top-level view we return to after closing the settings screen. /// /// Note that the current selected tap is stored in `AppState` so that /// other widgets can easily access it. - #[rust] previous_selection: SelectedTab, - #[rust] is_spaces_bar_shown: bool, + #[rust] + previous_selection: SelectedTab, + #[rust] + is_spaces_bar_shown: bool, } impl Widget for HomeScreen { @@ -418,7 +428,9 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Home) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Home; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -427,17 +439,23 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::AddRoom) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::AddRoom; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::GoToSpace { space_name_id }) => { - let new_space_selection = SelectedTab::Space { space_name_id: space_name_id.clone() }; + let new_space_selection = SelectedTab::Space { + space_name_id: space_name_id.clone(), + }; if app_state.selected_tab != new_space_selection { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = new_space_selection; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -447,11 +465,15 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Settings) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Settings; - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); - if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); + if let Some(settings_page) = + self.update_active_page_from_selection(cx, app_state) + { settings_page .settings_screen(cx, ids!(settings_screen)) - .populate(cx, None); + .populate(cx, None, &app_state.bot_settings); self.view.redraw(cx); } else { error!("BUG: failed to set active page to show settings screen."); @@ -461,19 +483,21 @@ impl Widget for HomeScreen { Some(NavigationBarAction::CloseSettings) => { if matches!(app_state.selected_tab, SelectedTab::Settings) { app_state.selected_tab = self.previous_selection.clone(); - cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + cx.action(NavigationBarAction::TabSelected( + app_state.selected_tab.clone(), + )); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::ToggleSpacesBar) => { self.is_spaces_bar_shown = !self.is_spaces_bar_shown; - self.view.spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) + self.view + .spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) .show_or_hide(cx, self.is_spaces_bar_shown); } // We're the ones who emitted this action, so we don't need to handle it again. - Some(NavigationBarAction::TabSelected(_)) - | None => { } + Some(NavigationBarAction::TabSelected(_)) | None => {} } } } @@ -504,12 +528,10 @@ impl HomeScreen { .set_active_page( cx, match app_state.selected_tab { - SelectedTab::Space { .. } - | SelectedTab::Home => id!(home_page), + SelectedTab::Space { .. } | SelectedTab::Home => id!(home_page), SelectedTab::Settings => id!(settings_page), SelectedTab::AddRoom => id!(add_room_page), }, ) } } - diff --git a/src/home/mod.rs b/src/home/mod.rs index 23a1de96d..8dae34f82 100644 --- a/src/home/mod.rs +++ b/src/home/mod.rs @@ -1,6 +1,8 @@ use makepad_widgets::ScriptVm; pub mod add_room; +pub mod create_bot_modal; +pub mod delete_bot_modal; pub mod edited_indicator; pub mod editing_pane; pub mod event_source_modal; @@ -35,6 +37,8 @@ pub fn script_mod(vm: &mut ScriptVm) { loading_pane::script_mod(vm); location_preview::script_mod(vm); add_room::script_mod(vm); + create_bot_modal::script_mod(vm); + delete_bot_modal::script_mod(vm); space_lobby::script_mod(vm); link_preview::script_mod(vm); event_reaction_list::script_mod(vm); diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index c55b7fa54..9a048b91f 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,7 +3,13 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{home::invite_modal::InviteModalAction, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, submit_async_request}, utils::RoomNameId}; +use crate::{ + app::AppState, + home::invite_modal::InviteModalAction, + shared::popup_list::{PopupKind, enqueue_popup_notification}, + sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, + utils::RoomNameId, +}; const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; @@ -69,7 +75,7 @@ script_mod! { } priority_button := mod.widgets.RoomContextMenuButton { - draw_icon +: { svg: (ICON_TOMBSTONE) } + draw_icon +: { svg: (ICON_TOMBSTONE) } text: "Set Low Priority" } @@ -77,7 +83,7 @@ script_mod! { draw_icon +: { svg: (ICON_LINK) } text: "Copy Link to Room" } - + divider1 := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -99,6 +105,11 @@ script_mod! { text: "Invite" } + bot_binding_button := mod.widgets.RoomContextMenuButton { + draw_icon +: { svg: (ICON_HIERARCHY) } + text: "Bind BotFather" + } + divider2 := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -123,6 +134,8 @@ pub struct RoomContextMenuDetails { pub is_favorite: bool, pub is_low_priority: bool, pub is_marked_unread: bool, + pub app_service_enabled: bool, + pub is_bot_bound: bool, } /// Actions emitted from the RoomContextMenu widget, as they must be handled @@ -137,9 +150,12 @@ pub enum RoomContextMenuAction { #[derive(Script, ScriptHook, Widget)] pub struct RoomContextMenu { - #[deref] view: View, - #[source] source: ScriptObjectRef, - #[rust] details: Option, + #[deref] + view: View, + #[source] + source: ScriptObjectRef, + #[rust] + details: Option, } impl Widget for RoomContextMenu { @@ -151,21 +167,25 @@ impl Widget for RoomContextMenu { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - if !self.visible { return; } + if !self.visible { + return; + } self.view.handle_event(cx, event, scope); // Close logic similar to NewMessageContextMenu let area = self.view.area(); let close_menu = { event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerUp(fue) if fue.is_over => { - !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerUp(fue) if fue.is_over => !self + .view(cx, ids!(main_content)) + .area() + .rect(cx) + .contains(fue.abs), + Hit::FingerScroll(_) => true, + _ => false, } - Hit::FingerScroll(_) => true, - _ => false, - } }; if close_menu { @@ -178,32 +198,31 @@ impl Widget for RoomContextMenu { } impl WidgetMatchEvent for RoomContextMenu { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { - let Some(details) = self.details.as_ref() else { return }; + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { + let Some(details) = self.details.as_ref() else { + return; + }; let mut close_menu = false; - + if self.button(cx, ids!(mark_unread_button)).clicked(actions) { submit_async_request(MatrixRequest::SetUnreadFlag { room_id: details.room_name_id.room_id().clone(), mark_as_unread: !details.is_marked_unread, }); close_menu = true; - } - else if self.button(cx, ids!(favorite_button)).clicked(actions) { + } else if self.button(cx, ids!(favorite_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsFavorite { room_id: details.room_name_id.room_id().clone(), is_favorite: !details.is_favorite, }); close_menu = true; - } - else if self.button(cx, ids!(priority_button)).clicked(actions) { + } else if self.button(cx, ids!(priority_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsLowPriority { room_id: details.room_name_id.room_id().clone(), is_low_priority: !details.is_low_priority, }); close_menu = true; - } - else if self.button(cx, ids!(copy_link_button)).clicked(actions) { + } else if self.button(cx, ids!(copy_link_button)).clicked(actions) { submit_async_request(MatrixRequest::GenerateMatrixLink { room_id: details.room_name_id.room_id().clone(), event_id: None, @@ -211,8 +230,7 @@ impl WidgetMatchEvent for RoomContextMenu { join_on_click: false, }); close_menu = true; - } - else if self.button(cx, ids!(room_settings_button)).clicked(actions) { + } else if self.button(cx, ids!(room_settings_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room settings page is not yet implemented.", @@ -220,8 +238,7 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } - else if self.button(cx, ids!(notifications_button)).clicked(actions) { + } else if self.button(cx, ids!(notifications_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room notifications page is not yet implemented.", @@ -229,12 +246,54 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } - else if self.button(cx, ids!(invite_button)).clicked(actions) { + } else if self.button(cx, ids!(invite_button)).clicked(actions) { cx.action(InviteModalAction::Open(details.room_name_id.clone())); close_menu = true; - } - else if self.button(cx, ids!(leave_button)).clicked(actions) { + } else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { + if let Some(app_state) = scope.data.get::() { + let room_id = details.room_name_id.room_id().clone(); + match app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + { + Ok(bot_user_id) => { + if details.is_bot_bound { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id, + bound: false, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + format!("Removing BotFather {bot_user_id} from this room..."), + PopupKind::Info, + Some(4.0), + ); + } else { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id, + bound: true, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + format!("Inviting BotFather {bot_user_id} into this room..."), + PopupKind::Info, + Some(5.0), + ); + } + } + Err(error) => { + enqueue_popup_notification(error, PopupKind::Error, Some(5.0)); + } + } + } else { + enqueue_popup_notification( + "Bot settings are unavailable right now.", + PopupKind::Error, + Some(5.0), + ); + } + close_menu = true; + } else if self.button(cx, ids!(leave_button)).clicked(actions) { use crate::join_leave_room_modal::{JoinLeaveRoomModalAction, JoinLeaveModalKind}; use crate::room::BasicRoomDetails; let room_details = BasicRoomDetails::Name(details.room_name_id.clone()); @@ -263,7 +322,7 @@ impl RoomContextMenu { cx.set_key_focus(self.view.area()); dvec2(MENU_WIDTH, height) } - + fn update_buttons(&mut self, cx: &mut Cx, details: &RoomContextMenuDetails) -> f64 { let mark_unread_button = self.button(cx, ids!(mark_unread_button)); if details.is_marked_unread { @@ -271,12 +330,12 @@ impl RoomContextMenu { } else { mark_unread_button.set_text(cx, "Mark as Unread"); } - + let favorite_button = self.button(cx, ids!(favorite_button)); if details.is_favorite { favorite_button.set_text(cx, "Un-favorite"); } else { - favorite_button.set_text(cx, "Favorite"); + favorite_button.set_text(cx, "Favorite"); } let priority_button = self.button(cx, ids!(priority_button)); @@ -285,7 +344,15 @@ impl RoomContextMenu { } else { priority_button.set_text(cx, "Set Low Priority"); } - + + let bot_binding_button = self.button(cx, ids!(bot_binding_button)); + bot_binding_button.set_visible(cx, details.app_service_enabled); + if details.is_bot_bound { + bot_binding_button.set_text(cx, "Unbind BotFather"); + } else { + bot_binding_button.set_text(cx, "Bind BotFather"); + } + // Reset hover states mark_unread_button.reset_hover(cx); favorite_button.reset_hover(cx); @@ -294,13 +361,18 @@ impl RoomContextMenu { self.button(cx, ids!(room_settings_button)).reset_hover(cx); self.button(cx, ids!(notifications_button)).reset_hover(cx); self.button(cx, ids!(invite_button)).reset_hover(cx); + bot_binding_button.reset_hover(cx); self.button(cx, ids!(leave_button)).reset_hover(cx); - + self.redraw(cx); - - // Calculate height (rudimentary) - sum of visible buttons + padding - // 8 buttons * 35.0 + 2 dividers * ~10.0 + padding - (8.0 * BUTTON_HEIGHT) + 20.0 + 10.0 // approx + + // Calculate height (rudimentary) - sum of visible buttons + padding. + let button_count = if details.app_service_enabled { + 9.0 + } else { + 8.0 + }; + (button_count * BUTTON_HEIGHT) + 20.0 + 10.0 // approx } fn close(&mut self, cx: &mut Cx) { @@ -313,12 +385,16 @@ impl RoomContextMenu { impl RoomContextMenuRef { pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { return false }; + let Some(inner) = self.borrow() else { + return false; + }; inner.is_currently_shown(cx) } pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 { - let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; + let Some(mut inner) = self.borrow_mut() else { + return DVec2::default(); + }; inner.show(cx, details) } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced9..1b4ddc171 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,40 +1,106 @@ //! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline //! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc}; +use std::{ + borrow::Cow, + cell::RefCell, + ops::{DerefMut, Range}, + sync::Arc, +}; use bytesize::ByteSize; use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ - EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ + OwnedServerName, RoomDisplayName, + media::{MediaFormat, MediaRequestParameters}, + room::RoomMember, + ruma::{ + EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, + events::{ receipt::Receipt, room::{ - ImageInfo, MediaSource, message::{ - AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, TextMessageEventContent, VideoMessageEventContent - } + ImageInfo, MediaSource, + message::{ + AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, + FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, + LocationMessageEventContent, MessageFormat, MessageType, + NoticeMessageEventContent, RoomMessageEventContent, TextMessageEventContent, + VideoMessageEventContent, + }, }, sticker::{StickerEventContent, StickerMediaSource}, - }, matrix_uri::MatrixId, uint - } + }, + matrix_uri::MatrixId, + uint, + }, }; use matrix_sdk_ui::timeline::{ - self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem + self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, + MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, + PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, + TimelineItemContent, TimelineItemKind, VirtualTimelineItem, +}; +use ruma::{ + OwnedUserId, + api::client::receipt::create_receipt::v3::ReceiptType, + events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, + owned_room_id, }; -use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ - user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, + avatar_cache, + event_preview::{ + plaintext_body_of_timeline_item, text_preview_of_encrypted_message, + text_preview_of_member_profile_change, text_preview_of_other_message_like, + text_preview_of_other_state, text_preview_of_room_membership_change, + text_preview_of_timeline_item, + }, + home::{ + create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, + delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, + edited_indicator::EditedIndicatorWidgetRefExt, + link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, + loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, + room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, + rooms_list::{RoomsListAction, RoomsListRef}, + tombstone_footer::SuccessorRoomDetails, + }, + media_cache::{MediaCache, MediaCacheEntry}, + profile::{ + user_profile::{ + ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, + UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt, + }, user_profile_cache, }, - room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, typing_notice::TypingNoticeWidgetExt}, + room::{ + BasicRoomDetails, + room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, + typing_notice::TypingNoticeWidgetExt, + }, shared::{ - avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt + avatar::{AvatarState, AvatarWidgetRefExt}, + confirmation_modal::ConfirmationModalContent, + html_or_plaintext::{ + HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction, + }, + image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, + jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, + popup_list::{PopupKind, enqueue_popup_notification}, + restore_status_view::RestoreStatusViewWidgetExt, + styles::*, + text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, + timestamp::TimestampWidgetRefExt, + }, + sliding_sync::{ + BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, + TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, + submit_async_request, take_timeline_endpoints, }, - sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} + utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime}, }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -43,7 +109,12 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction; use rangemap::RangeSet; -use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; +use super::{ + event_reaction_list::ReactionData, + loading_pane::LoadingPaneRef, + new_message_context_menu::{MessageAbilities, MessageDetails}, + room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}, +}; /// The maximum number of timeline items to search through /// when looking for a particular event. @@ -62,6 +133,62 @@ const COLOR_THREAD_SUMMARY_BG: Vec4 = vec4(1.0, 0.957, 0.898, 1.0); /// #FFEACC const COLOR_THREAD_SUMMARY_BG_HOVER: Vec4 = vec4(1.0, 0.918, 0.8, 1.0); +fn escape_slash_command_arg(value: &str) -> String { + value.trim().replace('\\', "\\\\").replace('"', "\\\"") +} + +fn format_create_bot_command( + username: &str, + display_name: &str, + system_prompt: Option<&str>, +) -> String { + let mut command = format!("/createbot {} {}", username.trim(), display_name.trim()); + if let Some(system_prompt) = system_prompt + .map(str::trim) + .filter(|value| !value.is_empty()) + { + command.push_str(" --prompt \""); + command.push_str(&escape_slash_command_arg(system_prompt)); + command.push('"'); + } + command +} + +fn format_delete_bot_command(matrix_user_id: &UserId) -> String { + format!("/deletebot {matrix_user_id}") +} + +fn resolve_delete_bot_user_id( + user_id_or_localpart: &str, + current_user_id: Option<&UserId>, +) -> Result { + let raw = user_id_or_localpart.trim(); + if raw.is_empty() { + return Err("Please enter the bot Matrix user ID to delete.".into()); + } + + if raw.starts_with('@') || raw.contains(':') { + let full_user_id = if raw.starts_with('@') { + raw.to_string() + } else { + format!("@{raw}") + }; + return UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")); + } + + let Some(current_user_id) = current_user_id else { + return Err( + "Current user ID is unavailable, so the bot homeserver cannot be resolved.".into(), + ); + }; + + let full_user_id = format!("@{raw}:{}", current_user_id.server_name()); + UserId::parse(&full_user_id) + .map(|user_id| user_id.to_owned()) + .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")) +} script_mod! { use mod.prelude.widgets.* @@ -504,6 +631,192 @@ script_mod! { } } + mod.widgets.AppServicePanel = #(AppServicePanel::register_widget(vm)) { + width: Fill + height: Fit + margin: Inset{left: 14, right: 54, top: 10, bottom: 16} + flow: Down + align: Align{x: 0.0, y: 0.0} + spacing: 8 + + sender_row := View { + width: Fit + height: Fit + flow: Right + spacing: 6 + + sender_name := Label { + width: Fit + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 10.8 } + color: (COLOR_ACTIVE_PRIMARY) + } + text: "BotFather" + } + + sender_tag := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 9.5 } + color: #8A8A8A + } + text: "bot" + } + } + + bubble := RoundedView { + width: 408 + height: Fit + flow: Down + spacing: 8 + padding: Inset{top: 14, right: 14, bottom: 12, left: 14} + + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + border_radius: 0.0 + border_size: 1.0 + border_color: (COLOR_SECONDARY_DARKER) + } + + header := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + + title := Label { + width: Fit + height: Fit + draw_text +: { + text_style: USERNAME_TEXT_STYLE { font_size: 11.2 } + color: #1F1F1F + } + text: "App Service Actions" + } + + spacer := View { + width: Fill + height: Fit + } + + dismiss_button := RobrixNeutralIconButton { + width: 28 + height: 24 + align: Align{x: 0.5, y: 0.5} + spacing: 0 + padding: 0 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 12, height: 12} + text: "" + } + } + + subtitle := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 10.5 } + color: (COLOR_TEXT) + wrap: Word + } + text: "Create a bot through BotFather. Robrix only sends the matching slash command." + } + + footer := View { + width: Fill + height: Fit + flow: Right + align: Align{x: 1.0, y: 0.5} + + timestamp := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT { font_size: 8.8 } + color: #9A9A9A + } + text: "now" + } + } + } + + keyboard := View { + width: Fit + height: Fit + flow: Down + spacing: 8 + + first_row := View { + width: Fit + height: Fit + flow: Right + spacing: 8 + + create_button := RobrixPositiveIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16, margin: Inset{left: -2, right: -1}} + text: "Create Bot" + } + + list_button := RobrixNeutralIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_SEARCH) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "List Bots" + } + } + + second_row := View { + width: Fit + height: Fit + flow: Right + spacing: 8 + + delete_button := RobrixNegativeIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Delete Bot" + } + + help_button := RobrixNeutralIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_INFO) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Bot Help" + } + } + + third_row := View { + width: Fit + height: Fit + flow: Right + spacing: 8 + + unbind_button := RobrixNeutralIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_CLOSE) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "Unbind" + } + } + } + } + mod.widgets.Timeline = View { width: Fill, height: Fill, @@ -527,6 +840,7 @@ script_mod! { Empty := mod.widgets.Empty {} DateDivider := mod.widgets.DateDivider {} ReadMarker := mod.widgets.ReadMarker {} + AppServicePanel := mod.widgets.AppServicePanel {} } // A jump to bottom button (with an unread message badge) that is shown @@ -582,6 +896,18 @@ script_mod! { // to finish loading, e.g., when loading an older replied-to message. loading_pane := LoadingPane { } + create_bot_modal := Modal { + content +: { + create_bot_modal_inner := mod.widgets.CreateBotModal {} + } + } + + delete_bot_modal := Modal { + content +: { + delete_bot_modal_inner := mod.widgets.DeleteBotModal {} + } + } + /* * TODO: add the action bar back in as a series of floating buttons. @@ -608,20 +934,30 @@ script_mod! { /// The main widget that displays a single Matrix room. #[derive(Script, Widget)] pub struct RoomScreen { - #[deref] view: View, + #[deref] + view: View, /// The name and ID of the currently-shown room, if any. - #[rust] room_name_id: Option, + #[rust] + room_name_id: Option, /// The timeline currently displayed by this RoomScreen, if any. - #[rust] timeline_kind: Option, + #[rust] + timeline_kind: Option, /// The persistent UI-relevant states for the room that this widget is currently displaying. - #[rust] tl_state: Option, + #[rust] + tl_state: Option, /// The set of pinned events in this room. - #[rust] pinned_events: Vec, + #[rust] + pinned_events: Vec, /// Whether this room has been successfully loaded (received from the homeserver). - #[rust] is_loaded: bool, + #[rust] + is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). - #[rust] all_rooms_loaded: bool, + #[rust] + all_rooms_loaded: bool, + /// Whether the in-room app service quick actions card is currently visible. + #[rust] + show_app_service_actions: bool, } impl Drop for RoomScreen { @@ -653,7 +989,8 @@ impl Widget for RoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { let room_screen_widget_uid = self.widget_uid(); let portal_list = self.portal_list(cx, ids!(timeline.list)); - let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); + let user_profile_sliding_pane = + self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); // Handle actions here before processing timeline updates. @@ -668,9 +1005,13 @@ impl Widget for RoomScreen { if let RoomScreenTooltipActions::HoverInReactionButton { widget_rect, reaction_data, - } = reaction_list.hovered_in(actions) { - let Some(_tl_state) = self.tl_state.as_ref() else { continue }; - let tooltip_text_arr: Vec = reaction_data.reaction_senders + } = reaction_list.hovered_in(actions) + { + let Some(_tl_state) = self.tl_state.as_ref() else { + continue; + }; + let tooltip_text_arr: Vec = reaction_data + .reaction_senders .iter() .map(|(sender, _react_info)| { user_profile_cache::get_user_display_name_for_room( @@ -684,10 +1025,13 @@ impl Widget for RoomScreen { }) .collect(); - let mut tooltip_text = utils::human_readable_list(&tooltip_text_arr, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT); + let mut tooltip_text = utils::human_readable_list( + &tooltip_text_arr, + MAX_VISIBLE_AVATARS_IN_READ_RECEIPT, + ); tooltip_text.push_str(&format!(" reacted with: {}", reaction_data.reaction)); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -701,24 +1045,23 @@ impl Widget for RoomScreen { // Handle a hover-out action on the reaction list or avatar row. let avatar_row_ref = wr.avatar_row(cx, ids!(avatar_row)); - if reaction_list.hovered_out(actions) - || avatar_row_ref.hover_out(actions) - { - cx.widget_action( - room_screen_widget_uid, - TooltipAction::HoverOut, - ); + if reaction_list.hovered_out(actions) || avatar_row_ref.hover_out(actions) { + cx.widget_action(room_screen_widget_uid, TooltipAction::HoverOut); } // Handle a hover-in action on the avatar row: show a read receipts summary. if let RoomScreenTooltipActions::HoverInReadReceipt { widget_rect, - read_receipts - } = avatar_row_ref.hover_in(actions) { - let Some(room_id) = self.room_id() else { return; }; - let tooltip_text= room_read_receipt::populate_tooltip(cx, read_receipts, room_id); + read_receipts, + } = avatar_row_ref.hover_in(actions) + { + let Some(room_id) = self.room_id() else { + return; + }; + let tooltip_text = + room_read_receipt::populate_tooltip(cx, read_receipts, room_id); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -732,23 +1075,27 @@ impl Widget for RoomScreen { // Handle an image within the message being clicked. let content_message = wr.text_or_image(cx, ids!(content.message)); - if let TextOrImageAction::Clicked(mxc_uri) = actions.find_widget_action(content_message.widget_uid()).cast() { + if let TextOrImageAction::Clicked(mxc_uri) = actions + .find_widget_action(content_message.widget_uid()) + .cast() + { let texture = content_message.get_texture(cx); - self.handle_image_click( - cx, - mxc_uri, - texture, - index, - ); + self.handle_image_click(cx, mxc_uri, texture, index); continue; } // Handle the invite_user_button (in a SmallStateEvent) being clicked. if wr.button(cx, ids!(invite_user_button)).clicked(actions) { - let Some(tl) = self.tl_state.as_ref() else { continue }; - if let Some(event_tl_item) = tl.items.get(index).and_then(|item| item.as_event()) { + let Some(tl) = self.tl_state.as_ref() else { + continue; + }; + if let Some(event_tl_item) = + tl.items.get(index).and_then(|item| item.as_event()) + { let user_id = event_tl_item.sender().to_owned(); - let username = if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { + let username = if let TimelineDetails::Ready(profile) = + event_tl_item.sender_profile() + { profile.display_name.as_deref().unwrap_or(user_id.as_str()) } else { user_id.as_str() @@ -756,14 +1103,22 @@ impl Widget for RoomScreen { let room_id = tl.kind.room_id().clone(); let content = ConfirmationModalContent { title_text: "Send Invitation".into(), - body_text: format!("Are you sure you want to invite {username} to this room?").into(), + body_text: format!( + "Are you sure you want to invite {username} to this room?" + ) + .into(), accept_button_text: Some("Invite".into()), on_accept_clicked: Some(Box::new(move |_cx| { - submit_async_request(MatrixRequest::InviteUser { room_id, user_id }); + submit_async_request(MatrixRequest::InviteUser { + room_id, + user_id, + }); })), ..Default::default() }; - cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new(Some(content)))); + cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new( + Some(content), + ))); } } } @@ -772,11 +1127,19 @@ impl Widget for RoomScreen { for action in actions { // Handle actions related to restoring the previously-saved state of rooms. - if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, ..}) = action.downcast_ref() { - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_name_id.room_id()) { + if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = + action.downcast_ref() + { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_name_id.room_id()) + { // `set_displayed_room()` does nothing if the room_name_id is unchanged, so we clear it first. self.room_name_id = None; - let thread_root_event_id = self.timeline_kind.as_ref() + let thread_root_event_id = self + .timeline_kind + .as_ref() .and_then(|k| k.thread_root_event_id().cloned()); self.set_displayed_room(cx, room_name_id, thread_root_event_id); return; @@ -786,7 +1149,11 @@ impl Widget for RoomScreen { // Handle InviteResultAction to show popup notifications. if let Some(InviteResultAction::Sent { room_id, .. }) = action.downcast_ref() { // Only handle if this is for the current room. - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_id) + { enqueue_popup_notification( "Sent invite successfully.", PopupKind::Success, @@ -794,9 +1161,15 @@ impl Widget for RoomScreen { ); } } - if let Some(InviteResultAction::Failed { room_id, error, .. }) = action.downcast_ref() { + if let Some(InviteResultAction::Failed { room_id, error, .. }) = + action.downcast_ref() + { // Only handle if this is for the current room. - if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|rn| rn.room_id() == room_id) + { enqueue_popup_notification( format!("Failed to send invite.\n\nError: {error}"), PopupKind::Error, @@ -806,11 +1179,15 @@ impl Widget for RoomScreen { } // Handle the highlight animation for a message. - let Some(tl) = self.tl_state.as_mut() else { continue }; - if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { + let Some(tl) = self.tl_state.as_mut() else { + continue; + }; + if let MessageHighlightAnimationState::Pending { item_id } = + tl.message_highlight_animation_state + { if portal_list.smooth_scroll_reached(actions) { cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, MessageAction::HighlightMessage(item_id), ); tl.message_highlight_animation_state = MessageHighlightAnimationState::Off; @@ -834,22 +1211,25 @@ impl Widget for RoomScreen { self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list); // Handle the jump to bottom button: update its visibility, and handle clicks. - self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)).update_from_actions( - cx, - &portal_list, - actions, - ); + self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)) + .update_from_actions(cx, &portal_list, actions); } // Currently, a Signal event is only used to tell this widget: // 1. to check if the room has been loaded from the homeserver yet, or // 2. that its timeline events have been updated in the background. if let Event::Signal = event { - if let (false, Some(room_name_id), true) = (self.is_loaded, self.room_name_id.as_ref(), cx.has_global::()) { + if let (false, Some(room_name_id), true) = ( + self.is_loaded, + self.room_name_id.as_ref(), + cx.has_global::(), + ) { let rooms_list_ref = cx.get_global::(); if rooms_list_ref.is_room_loaded(room_name_id.room_id()) { let room_name_clone = room_name_id.clone(); - let thread_root_event_id = self.timeline_kind.as_ref() + let thread_root_event_id = self + .timeline_kind + .as_ref() .and_then(|k| k.thread_root_event_id().cloned()); // This room has been loaded now, so we call `set_displayed_room()`. // We first clear the `room_name_id`, otherwise that function will do nothing. @@ -891,14 +1271,12 @@ impl Widget for RoomScreen { if is_interactive_hit { loading_pane.handle_event(cx, event, scope); } - } - else if user_profile_sliding_pane.is_currently_shown(cx) { + } else if user_profile_sliding_pane.is_currently_shown(cx) { is_pane_shown = true; if is_interactive_hit { user_profile_sliding_pane.handle_event(cx, event, scope); } - } - else { + } else { is_pane_shown = false; } @@ -913,14 +1291,26 @@ impl Widget for RoomScreen { let room_props = if let Some(tl) = self.tl_state.as_ref() { let room_id = tl.kind.room_id().clone(); let room_members = tl.room_members.clone(); + let (app_service_enabled, app_service_room_bound) = scope + .data + .get::() + .map(|app_state| { + ( + app_state.bot_settings.enabled, + app_state.bot_settings.is_room_bound(&room_id), + ) + }) + .unwrap_or((false, false)); // Fetch room data once to avoid duplicate expensive lookups let (room_display_name, room_avatar_url) = get_client() .and_then(|client| client.get_room(&room_id)) - .map(|room| ( - room.cached_display_name().unwrap_or(RoomDisplayName::Empty), - room.avatar_url() - )) + .map(|room| { + ( + room.cached_display_name().unwrap_or(RoomDisplayName::Empty), + room.avatar_url(), + ) + }) .unwrap_or((RoomDisplayName::Empty, None)); RoomScreenProps { @@ -929,23 +1319,31 @@ impl Widget for RoomScreen { timeline_kind: tl.kind.clone(), room_members, room_avatar_url, + app_service_enabled, + app_service_room_bound, } } else if let Some(room_name) = &self.room_name_id { // Fallback case: we have a room_name but no tl_state yet RoomScreenProps { room_screen_widget_uid, room_name_id: room_name.clone(), - timeline_kind: self.timeline_kind.clone() + timeline_kind: self + .timeline_kind + .clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, room_avatar_url: None, + app_service_enabled: false, + app_service_room_bound: false, } } else { // No room selected yet, skip event handling that requires room context if !is_pane_shown || !is_interactive_hit { return; } - log!("RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling"); + log!( + "RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling" + ); // Use a dummy room props for non-room-specific events let room_id = owned_room_id!("!dummy:matrix.org"); RoomScreenProps { @@ -954,17 +1352,17 @@ impl Widget for RoomScreen { timeline_kind: TimelineKind::MainRoom { room_id }, room_members: None, room_avatar_url: None, + app_service_enabled: false, + app_service_room_bound: false, } }; let mut room_scope = Scope::with_props(&room_props); - // Forward the event to the inner timeline view, but capture any actions it produces // such that we can handle the ones relevant to only THIS RoomScreen widget right here and now, // ensuring they are not mistakenly handled by other RoomScreen widget instances. - let mut actions_generated_within_this_room_screen = cx.capture_actions(|cx| - self.view.handle_event(cx, event, &mut room_scope) - ); + let mut actions_generated_within_this_room_screen = + cx.capture_actions(|cx| self.view.handle_event(cx, event, &mut room_scope)); // Here, we handle and remove any general actions that are relevant to only this RoomScreen. // Removing the handled actions ensures they are not mistakenly handled by other RoomScreen widget instances. actions_generated_within_this_room_screen.retain(|action| { @@ -972,6 +1370,224 @@ impl Widget for RoomScreen { return false; } + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast() + { + AppServicePanelAction::Dismiss => { + self.set_app_service_actions_visible(cx, false); + return false; + } + AppServicePanelAction::OpenCreateBotModal => { + if let Some(app_state) = scope.data.get::() { + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before creating bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else if !room_props.app_service_room_bound { + enqueue_popup_notification( + "Bind BotFather to this room before creating a bot.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else { + self.open_create_bot_modal(cx); + } + } else { + enqueue_popup_notification( + "App state is unavailable, so bot creation is temporarily unavailable.", + PopupKind::Error, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } + return false; + } + AppServicePanelAction::OpenDeleteBotModal => { + if let Some(app_state) = scope.data.get::() { + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before deleting bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else if !room_props.app_service_room_bound { + enqueue_popup_notification( + "Bind BotFather to this room before deleting a bot.", + PopupKind::Warning, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } else { + self.open_delete_bot_modal(cx); + } + } else { + enqueue_popup_notification( + "App state is unavailable, so bot deletion is temporarily unavailable.", + PopupKind::Error, + Some(4.0), + ); + self.set_app_service_actions_visible(cx, false); + } + return false; + } + AppServicePanelAction::SendListBots => { + if let Some(app_state) = scope.data.get::() { + self.send_botfather_command( + cx, + app_state, + "/listbots", + "Sent `/listbots` to BotFather.", + ); + } + return false; + } + AppServicePanelAction::SendBotHelp => { + if let Some(app_state) = scope.data.get::() { + self.send_botfather_command( + cx, + app_state, + "/bothelp", + "Sent `/bothelp` to BotFather.", + ); + } + return false; + } + AppServicePanelAction::Unbind => { + if let Some(app_state) = scope.data.get::() { + if !room_props.app_service_room_bound { + enqueue_popup_notification( + "This room is not currently bound to BotFather.", + PopupKind::Warning, + Some(4.0), + ); + } else { + match app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + { + Ok(bot_user_id) => { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id: room_props.room_name_id.room_id().clone(), + bound: false, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + format!( + "Removing BotFather {bot_user_id} from this room..." + ), + PopupKind::Info, + Some(4.0), + ); + } + Err(error) => { + enqueue_popup_notification( + error, + PopupKind::Error, + Some(4.0), + ); + } + } + } + } else { + enqueue_popup_notification( + "App state is unavailable, so BotFather could not be removed from this room.", + PopupKind::Error, + Some(4.0), + ); + } + self.set_app_service_actions_visible(cx, false); + return false; + } + _ => {} + } + + match action.downcast_ref::() { + Some(CreateBotModalAction::Close) => { + self.close_create_bot_modal(cx); + return false; + } + Some(CreateBotModalAction::Submit(request)) => { + let Some(app_state) = scope.data.get::() else { + enqueue_popup_notification( + "App state is unavailable, so the create-bot command was not sent.", + PopupKind::Error, + Some(4.0), + ); + self.close_create_bot_modal(cx); + return false; + }; + self.send_create_bot_command( + cx, + app_state, + &request.username, + &request.display_name, + request.system_prompt.as_deref(), + ); + return false; + } + None => {} + } + + match action.downcast_ref::() { + Some(DeleteBotModalAction::Close) => { + self.close_delete_bot_modal(cx); + return false; + } + Some(DeleteBotModalAction::Submit(request)) => { + let Some(app_state) = scope.data.get::() else { + enqueue_popup_notification( + "App state is unavailable, so the delete-bot command was not sent.", + PopupKind::Error, + Some(4.0), + ); + self.close_delete_bot_modal(cx); + return false; + }; + self.send_delete_bot_command(cx, app_state, &request.user_id_or_localpart); + return false; + } + None => {} + } + + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast() + { + MessageAction::ToggleAppServiceActions => { + if room_props.timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + } else if !room_props.app_service_enabled { + enqueue_popup_notification( + "Enable App Service in Settings before using /bot.", + PopupKind::Warning, + Some(4.0), + ); + } else if !room_props.app_service_room_bound { + enqueue_popup_notification( + "Bind BotFather to this room before using /bot.", + PopupKind::Warning, + Some(4.0), + ); + } else { + self.toggle_app_service_actions(cx); + } + return false; + } + _ => {} + } + // Handle the action that requests to show the user profile sliding pane. if let ShowUserProfileAction::ShowUserProfile(profile_and_room_id) = action.as_widget_action().cast() { self.show_user_profile( @@ -1033,7 +1649,6 @@ impl Widget for RoomScreen { } } - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // If the room isn't loaded yet, we show the restore status label only. if !self.is_loaded { @@ -1041,7 +1656,8 @@ impl Widget for RoomScreen { // No room selected yet, nothing to show. return DrawStep::done(); }; - let mut restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); + let mut restore_status_view = + self.view.restore_status_view(cx, ids!(restore_status_view)); restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); return restore_status_view.draw(cx, scope); } @@ -1051,13 +1667,14 @@ impl Widget for RoomScreen { return DrawStep::done(); } - let room_screen_widget_uid = self.widget_uid(); while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // Here, we only need to handle drawing the portal list. let portal_list_ref = subview.as_portal_list(); let Some(mut list_ref) = portal_list_ref.borrow_mut() else { - error!("!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else"); + error!( + "!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else" + ); continue; }; let Some(tl_state) = self.tl_state.as_mut() else { @@ -1066,7 +1683,7 @@ impl Widget for RoomScreen { // Set the portal list's range based on the number of timeline items. let tl_items = &tl_state.items; - let last_item_id = tl_items.len(); + let last_item_id = tl_items.len() + usize::from(self.show_app_service_actions); let list = list_ref.deref_mut(); list.set_item_range(cx, 0, last_item_id); @@ -1074,143 +1691,174 @@ impl Widget for RoomScreen { while let Some(item_id) = list.next_visible_item(cx) { let item = { let tl_idx = item_id; - let Some(timeline_item) = tl_items.get(tl_idx) else { - // This shouldn't happen (unless the timeline gets corrupted or some other weird error), - // but we can always safely fill the item with an empty widget that takes up no space. - list.item(cx, item_id, id!(Empty)); - continue; - }; + if self.show_app_service_actions && tl_idx == tl_items.len() { + list.item(cx, item_id, id!(AppServicePanel)) + } else { + let Some(timeline_item) = tl_items.get(tl_idx) else { + // This shouldn't happen (unless the timeline gets corrupted or some other weird error), + // but we can always safely fill the item with an empty widget that takes up no space. + list.item(cx, item_id, id!(Empty)); + continue; + }; - // Determine whether this item's content and profile have been drawn since the last update. - // Pass this state to each of the `populate_*` functions so they can attempt to re-use - // an item in the timeline's portallist that was previously populated, if one exists. - let item_drawn_status = ItemDrawnStatus { - content_drawn: tl_state.content_drawn_since_last_update.contains(&tl_idx), - profile_drawn: tl_state.profile_drawn_since_last_update.contains(&tl_idx), - }; - let (item, item_new_draw_status) = match timeline_item.kind() { - TimelineItemKind::Event(event_tl_item) => match event_tl_item.content() { - TimelineItemContent::MsgLike(msg_like_content) => { - if tl_state.kind.thread_root_event_id().is_none() - && msg_like_content.thread_root.is_some() - { - // Hide threaded replies from the main room timeline UI. - (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::both_drawn()) - } else { - match &msg_like_content.kind { - MsgLikeKind::Message(_) - | MsgLikeKind::Sticker(_) - | MsgLikeKind::Redacted => { - let prev_event = tl_idx.checked_sub(1).and_then(|i| tl_items.get(i)); - populate_message_view( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - msg_like_content, - prev_event, - &mut tl_state.media_cache, - &mut tl_state.link_preview_cache, - &tl_state.fetched_thread_summaries, - &mut tl_state.pending_thread_summary_fetches, - &tl_state.user_power, - &self.pinned_events, - item_drawn_status, - room_screen_widget_uid, - ) - }, - // TODO: properly implement `Poll` as a regular Message-like timeline item. - MsgLikeKind::Poll(poll_state) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - poll_state, - item_drawn_status, - ), - MsgLikeKind::UnableToDecrypt(utd) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - utd, - item_drawn_status, - ), - MsgLikeKind::Other(other) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - other, - item_drawn_status, - ), + // Determine whether this item's content and profile have been drawn since the last update. + // Pass this state to each of the `populate_*` functions so they can attempt to re-use + // an item in the timeline's portallist that was previously populated, if one exists. + let item_drawn_status = ItemDrawnStatus { + content_drawn: tl_state + .content_drawn_since_last_update + .contains(&tl_idx), + profile_drawn: tl_state + .profile_drawn_since_last_update + .contains(&tl_idx), + }; + let (item, item_new_draw_status) = match timeline_item.kind() { + TimelineItemKind::Event(event_tl_item) => match event_tl_item.content() + { + TimelineItemContent::MsgLike(msg_like_content) => { + if tl_state.kind.thread_root_event_id().is_none() + && msg_like_content.thread_root.is_some() + { + // Hide threaded replies from the main room timeline UI. + ( + list.item(cx, item_id, id!(Empty)), + ItemDrawnStatus::both_drawn(), + ) + } else { + match &msg_like_content.kind { + MsgLikeKind::Message(_) + | MsgLikeKind::Sticker(_) + | MsgLikeKind::Redacted => { + let prev_event = tl_idx + .checked_sub(1) + .and_then(|i| tl_items.get(i)); + populate_message_view( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + msg_like_content, + prev_event, + &mut tl_state.media_cache, + &mut tl_state.link_preview_cache, + &tl_state.fetched_thread_summaries, + &mut tl_state.pending_thread_summary_fetches, + &tl_state.user_power, + &self.pinned_events, + item_drawn_status, + room_screen_widget_uid, + ) + } + // TODO: properly implement `Poll` as a regular Message-like timeline item. + MsgLikeKind::Poll(poll_state) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + poll_state, + item_drawn_status, + ) + } + MsgLikeKind::UnableToDecrypt(utd) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + utd, + item_drawn_status, + ) + } + MsgLikeKind::Other(other) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + other, + item_drawn_status, + ) + } + } } } + TimelineItemContent::MembershipChange(membership_change) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + membership_change, + item_drawn_status, + ) + } + TimelineItemContent::ProfileChange(profile_change) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + profile_change, + item_drawn_status, + ) + } + TimelineItemContent::OtherState(other) => { + populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + other, + item_drawn_status, + ) + } + unhandled => { + let item = list.item(cx, item_id, id!(SmallStateEvent)); + item.label(cx, ids!(content)) + .set_text(cx, &format!("[Unsupported] {:?}", unhandled)); + (item, ItemDrawnStatus::both_drawn()) + } }, - TimelineItemContent::MembershipChange(membership_change) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - membership_change, - item_drawn_status, - ), - TimelineItemContent::ProfileChange(profile_change) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - profile_change, - item_drawn_status, - ), - TimelineItemContent::OtherState(other) => populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - other, - item_drawn_status, - ), - unhandled => { - let item = list.item(cx, item_id, id!(SmallStateEvent)); - item.label(cx, ids!(content)).set_text(cx, &format!("[Unsupported] {:?}", unhandled)); + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(millis)) => { + let item = list.item(cx, item_id, id!(DateDivider)); + let text = unix_time_millis_to_datetime(*millis) + // format the time as a shortened date (Sat, Sept 5, 2021) + .map(|dt| format!("{}", dt.date_naive().format("%a %b %-d, %Y"))) + .unwrap_or_else(|| format!("{:?}", millis)); + item.label(cx, ids!(date)).set_text(cx, &text); (item, ItemDrawnStatus::both_drawn()) } + TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { + let item = list.item(cx, item_id, id!(ReadMarker)); + (item, ItemDrawnStatus::both_drawn()) + } + TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { + let item = list.item(cx, item_id, id!(Empty)); + (item, ItemDrawnStatus::both_drawn()) + } + }; + + // Now that we've drawn the item, add its index to the set of drawn items. + if item_new_draw_status.content_drawn { + tl_state + .content_drawn_since_last_update + .insert(tl_idx..tl_idx + 1); } - TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(millis)) => { - let item = list.item(cx, item_id, id!(DateDivider)); - let text = unix_time_millis_to_datetime(*millis) - // format the time as a shortened date (Sat, Sept 5, 2021) - .map(|dt| format!("{}", dt.date_naive().format("%a %b %-d, %Y"))) - .unwrap_or_else(|| format!("{:?}", millis)); - item.label(cx, ids!(date)).set_text(cx, &text); - (item, ItemDrawnStatus::both_drawn()) - } - TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { - let item = list.item(cx, item_id, id!(ReadMarker)); - (item, ItemDrawnStatus::both_drawn()) - } - TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { - let item = list.item(cx, item_id, id!(Empty)); - (item, ItemDrawnStatus::both_drawn()) + if item_new_draw_status.profile_drawn { + tl_state + .profile_drawn_since_last_update + .insert(tl_idx..tl_idx + 1); } - }; - - // Now that we've drawn the item, add its index to the set of drawn items. - if item_new_draw_status.content_drawn { - tl_state.content_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); - } - if item_new_draw_status.profile_drawn { - tl_state.profile_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); + item } - item }; item.draw_all(cx, scope); } @@ -1218,7 +1866,10 @@ impl Widget for RoomScreen { // If the list is not filling the viewport, we need to back paginate the timeline // until we have enough events items to fill the viewport. if !tl_state.fully_paginated && !list.is_filling_viewport() { - log!("Automatically paginating timeline to fill viewport for room {:?}", self.room_name_id); + log!( + "Automatically paginating timeline to fill viewport for room {:?}", + self.room_name_id + ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -1235,6 +1886,184 @@ impl RoomScreen { self.room_name_id.as_ref().map(|r| r.room_id()) } + fn set_app_service_actions_visible(&mut self, cx: &mut Cx, visible: bool) { + self.show_app_service_actions = visible; + self.redraw(cx); + } + + fn toggle_app_service_actions(&mut self, cx: &mut Cx) { + self.set_app_service_actions_visible(cx, !self.show_app_service_actions); + } + + fn close_create_bot_modal(&self, cx: &mut Cx) { + self.view.modal(cx, ids!(create_bot_modal)).close(cx); + } + + fn close_delete_bot_modal(&self, cx: &mut Cx) { + self.view.modal(cx, ids!(delete_bot_modal)).close(cx); + } + + fn open_create_bot_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.clone() else { + return; + }; + self.set_app_service_actions_visible(cx, false); + self.view + .create_bot_modal(cx, ids!(create_bot_modal_inner)) + .show(cx, room_name_id); + self.view.modal(cx, ids!(create_bot_modal)).open(cx); + } + + fn open_delete_bot_modal(&mut self, cx: &mut Cx) { + let Some(room_name_id) = self.room_name_id.clone() else { + return; + }; + self.set_app_service_actions_visible(cx, false); + self.view + .delete_bot_modal(cx, ids!(delete_bot_modal_inner)) + .show(cx, room_name_id); + self.view.modal(cx, ids!(delete_bot_modal)).open(cx); + } + + fn reset_app_service_ui(&mut self, cx: &mut Cx) { + self.set_app_service_actions_visible(cx, false); + self.close_create_bot_modal(cx); + self.close_delete_bot_modal(cx); + } + + fn send_botfather_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + command: &str, + success_message: &str, + ) { + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; + if timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let Some(room_id) = self.room_id().cloned() else { + return; + }; + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before using BotFather commands in a room.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + if !app_state.bot_settings.is_room_bound(&room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before using BotFather commands.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + submit_async_request(MatrixRequest::SendMessage { + timeline_kind, + message: RoomMessageEventContent::text_plain(command), + replied_to: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + + enqueue_popup_notification(success_message.to_string(), PopupKind::Info, Some(4.0)); + self.set_app_service_actions_visible(cx, false); + } + + fn send_create_bot_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + username: &str, + display_name: &str, + system_prompt: Option<&str>, + ) { + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; + if timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot creation commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let Some(room_id) = self.room_id().cloned() else { + return; + }; + if !app_state.bot_settings.enabled { + enqueue_popup_notification( + "Enable App Service before creating bots in a room.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + if !app_state.bot_settings.is_room_bound(&room_id) { + enqueue_popup_notification( + "Bind BotFather to this room before creating a bot.", + PopupKind::Warning, + Some(4.0), + ); + return; + } + + let command = format_create_bot_command(username, display_name, system_prompt); + submit_async_request(MatrixRequest::SendMessage { + timeline_kind, + message: RoomMessageEventContent::text_plain(command), + replied_to: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + + enqueue_popup_notification( + format!("Sent `/createbot` for `{username}` to BotFather."), + PopupKind::Info, + Some(4.0), + ); + self.close_create_bot_modal(cx); + } + + fn send_delete_bot_command( + &mut self, + cx: &mut Cx, + app_state: &AppState, + user_id_or_localpart: &str, + ) { + let matrix_user_id = + match resolve_delete_bot_user_id(user_id_or_localpart, current_user_id().as_deref()) { + Ok(user_id) => user_id, + Err(error) => { + enqueue_popup_notification(error, PopupKind::Error, Some(4.0)); + return; + } + }; + + let command = format_delete_bot_command(matrix_user_id.as_ref()); + self.send_botfather_command( + cx, + app_state, + &command, + &format!("Sent `/deletebot` for {matrix_user_id} to BotFather."), + ); + self.close_delete_bot_modal(cx); + } + /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. @@ -1243,7 +2072,9 @@ impl RoomScreen { let jump_to_bottom_button = self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)); let curr_first_id = portal_list.first_id(); let ui = self.widget_uid(); - let Some(tl) = self.tl_state.as_mut() else { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; let mut done_loading = false; let mut should_continue_backwards_pagination = false; @@ -1264,10 +2095,19 @@ impl RoomScreen { tl.items = initial_items; done_loading = true; } - TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { + TimelineUpdate::NewItems { + new_items, + changed_indices, + is_append, + clear_cache, + } => { if new_items.is_empty() { if !tl.items.is_empty() { - log!("process_timeline_updates(): timeline (had {} items) was cleared for room {}", tl.items.len(), tl.kind.room_id()); + log!( + "process_timeline_updates(): timeline (had {} items) was cleared for room {}", + tl.items.len(), + tl.kind.room_id() + ); // For now, we paginate a cleared timeline in order to be able to show something at least. // A proper solution would be what's described below, which would be to save a few event IDs // and then either focus on them (if we're not close to the end of the timeline) @@ -1301,9 +2141,12 @@ impl RoomScreen { if new_items.len() == tl.items.len() { // log!("process_timeline_updates(): no jump necessary for updated timeline of same length: {}", items.len()); - } - else if curr_first_id > new_items.len() { - log!("process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", curr_first_id, new_items.len()); + } else if curr_first_id > new_items.len() { + log!( + "process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", + curr_first_id, + new_items.len() + ); portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0); portal_list.set_tail_range(true); jump_to_bottom_button.update_visibility(cx, true); @@ -1312,19 +2155,28 @@ impl RoomScreen { // in the timeline viewport so that we can maintain the scroll position of that item, // which ensures that the timeline doesn't jump around unexpectedly and ruin the user's experience. else if let Some((curr_item_idx, new_item_idx, new_item_scroll, _event_id)) = - prior_items_changed.then(|| - find_new_item_matching_current_item(cx, portal_list, curr_first_id, &tl.items, &new_items) - ) - .flatten() + prior_items_changed + .then(|| { + find_new_item_matching_current_item( + cx, + portal_list, + curr_first_id, + &tl.items, + &new_items, + ) + }) + .flatten() { if curr_item_idx != new_item_idx { - log!("process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}"); + log!( + "process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}" + ); portal_list.set_first_id_and_scroll(new_item_idx, new_item_scroll); tl.prev_first_index = Some(new_item_idx); // Set scrolled_past_read_marker false when we jump to a new event tl.scrolled_past_read_marker = false; // Hide the tooltip when the timeline jumps, as a hover-out event won't occur. - cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); + cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); } } // @@ -1340,8 +2192,9 @@ impl RoomScreen { // because the matrix SDK doesn't currently support querying unread message counts for threads. if matches!(tl.kind, TimelineKind::MainRoom { .. }) { // Immediately show the unread badge with no count while we fetch the actual count in the background. - jump_to_bottom_button.show_unread_message_badge(cx, UnreadMessageCount::Unknown); - submit_async_request(MatrixRequest::GetNumberUnreadMessages{ + jump_to_bottom_button + .show_unread_message_badge(cx, UnreadMessageCount::Unknown); + submit_async_request(MatrixRequest::GetNumberUnreadMessages { timeline_kind: tl.kind.clone(), }); } @@ -1355,10 +2208,15 @@ impl RoomScreen { let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); let mut loading_pane_state = loading_pane.take_state(); if let LoadingPaneState::BackwardsPaginateUntilEvent { - events_paginated, target_event_id, .. - } = &mut loading_pane_state { + events_paginated, + target_event_id, + .. + } = &mut loading_pane_state + { *events_paginated += new_items.len().saturating_sub(tl.items.len()); - log!("While finding target event {target_event_id}, we have now loaded {events_paginated} messages..."); + log!( + "While finding target event {target_event_id}, we have now loaded {events_paginated} messages..." + ); // Here, we assume that we have not yet found the target event, // so we need to continue paginating backwards. // If the target event has already been found, it will be handled @@ -1375,8 +2233,10 @@ impl RoomScreen { tl.profile_drawn_since_last_update.clear(); tl.fully_paginated = false; } else { - tl.content_drawn_since_last_update.remove(changed_indices.clone()); - tl.profile_drawn_since_last_update.remove(changed_indices.clone()); + tl.content_drawn_since_last_update + .remove(changed_indices.clone()); + tl.profile_drawn_since_last_update + .remove(changed_indices.clone()); // log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update); } tl.items = new_items; @@ -1389,7 +2249,10 @@ impl RoomScreen { jump_to_bottom_button.show_unread_message_badge(cx, unread_messages_count); } } - TimelineUpdate::TargetEventFound { target_event_id, index } => { + TimelineUpdate::TargetEventFound { + target_event_id, + index, + } => { // log!("Target event found in room {}: {target_event_id}, index: {index}", tl.kind.room_id()); tl.request_sender.send_if_modified(|requests| { requests.retain(|r| &r.room_id != tl.kind.room_id()); @@ -1399,10 +2262,10 @@ impl RoomScreen { // sanity check: ensure the target event is in the timeline at the given `index`. let item = tl.items.get(index); - let is_valid = item.is_some_and(|item| + let is_valid = item.is_some_and(|item| { item.as_event() .is_some_and(|ev| ev.event_id() == Some(&target_event_id)) - ); + }); let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); // log!("TargetEventFound: is_valid? {is_valid}. room {}, event {target_event_id}, index {index} of {}\n --> item: {item:?}", tl.kind.room_id(), tl.items.len()); @@ -1421,19 +2284,24 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { - item_id: index - }; - } - else { + tl.message_highlight_animation_state = + MessageHighlightAnimationState::Pending { item_id: index }; + } else { // Here, the target event was not found in the current timeline, // or we found it previously but it is no longer in the timeline (or has moved), // which means we encountered an error and are unable to jump to the target event. - error!("Target event index {index} of {} is out of bounds for room {}", tl.items.len(), tl.kind.room_id()); + error!( + "Target event index {index} of {} is out of bounds for room {}", + tl.items.len(), + tl.kind.room_id() + ); // Show this error in the loading pane, which should already be open. - loading_pane.set_state(cx, LoadingPaneState::Error( - String::from("Unable to find related message; it may have been deleted.") - )); + loading_pane.set_state( + cx, + LoadingPaneState::Error(String::from( + "Unable to find related message; it may have been deleted.", + )), + ); } should_continue_backwards_pagination = false; @@ -1450,16 +2318,25 @@ impl RoomScreen { } } TimelineUpdate::PaginationError { error, direction } => { - error!("Pagination error ({direction}) in {:?}: {error:?}", self.room_name_id); + error!( + "Pagination error ({direction}) in {:?}: {error:?}", + self.room_name_id + ); let room_name = self.room_name_id.as_ref().map(|r| r.to_string()); enqueue_popup_notification( - utils::stringify_pagination_error(&error, room_name.as_deref().unwrap_or(UNNAMED_ROOM)), + utils::stringify_pagination_error( + &error, + room_name.as_deref().unwrap_or(UNNAMED_ROOM), + ), PopupKind::Error, Some(10.0), ); done_loading = true; } - TimelineUpdate::PaginationIdle { fully_paginated, direction } => { + TimelineUpdate::PaginationIdle { + fully_paginated, + direction, + } => { if direction == PaginationDirection::Backwards { // Don't set `done_loading` to `true` here, because we want to keep the top space visible // (with the "loading" message) until the corresponding `NewItems` update is received. @@ -1471,9 +2348,12 @@ impl RoomScreen { error!("Unexpected PaginationIdle update in the Forwards direction"); } } - TimelineUpdate::EventDetailsFetched {event_id, result } => { + TimelineUpdate::EventDetailsFetched { event_id, result } => { if let Err(_e) = result { - error!("Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", tl.kind.room_id()); + error!( + "Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", + tl.kind.room_id() + ); } // Here, to be most efficient, we could redraw only the updated event, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. @@ -1484,7 +2364,8 @@ impl RoomScreen { num_replies, latest_reply_preview_text, } => { - tl.pending_thread_summary_fetches.remove(&thread_root_event_id); + tl.pending_thread_summary_fetches + .remove(&thread_root_event_id); tl.fetched_thread_summaries.insert( thread_root_event_id.clone(), FetchedThreadSummary { @@ -1492,14 +2373,15 @@ impl RoomScreen { latest_reply_preview_text, }, ); - let event_id_matches_at_index = tl.items + let event_id_matches_at_index = tl + .items .get(timeline_item_index) .and_then(|item| item.as_event()) .and_then(|ev| ev.event_id()) .is_some_and(|id| id == thread_root_event_id); if event_id_matches_at_index { tl.content_drawn_since_last_update - .remove(timeline_item_index .. timeline_item_index + 1); + .remove(timeline_item_index..timeline_item_index + 1); } else { tl.content_drawn_since_last_update.clear(); } @@ -1512,9 +2394,12 @@ impl RoomScreen { TimelineUpdate::RoomMembersListFetched { members } => { // Store room members directly in TimelineUiState tl.room_members = Some(Arc::new(members)); - }, + } TimelineUpdate::MediaFetched(request) => { - log!("process_timeline_updates(): media fetched for room {}", tl.kind.room_id()); + log!( + "process_timeline_updates(): media fetched for room {}", + tl.kind.room_id() + ); // Set Image to image viewer modal if the media is not a thumbnail. if let (MediaFormat::File, media_source) = (request.format, request.source) { populate_matrix_image_modal(cx, media_source, &mut tl.media_cache); @@ -1522,26 +2407,39 @@ impl RoomScreen { // Here, to be most efficient, we could redraw only the media items in the timeline, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. } - TimelineUpdate::MessageEdited { timeline_event_item_id: timeline_event_id, result } => { - self.view.room_input_bar(cx, ids!(room_input_bar)) + TimelineUpdate::MessageEdited { + timeline_event_item_id: timeline_event_id, + result, + } => { + self.view + .room_input_bar(cx, ids!(room_input_bar)) .handle_edit_result(cx, timeline_event_id, result); } TimelineUpdate::PinResult { result, pin, .. } => { let (message, auto_dismissal_duration, kind) = match &result { Ok(true) => ( - format!("Successfully {} event.", if pin { "pinned" } else { "unpinned" }), + format!( + "Successfully {} event.", + if pin { "pinned" } else { "unpinned" } + ), Some(4.0), - PopupKind::Success + PopupKind::Success, ), Ok(false) => ( - format!("Message was already {}.", if pin { "pinned" } else { "unpinned" }), + format!( + "Message was already {}.", + if pin { "pinned" } else { "unpinned" } + ), Some(4.0), - PopupKind::Info + PopupKind::Info, ), Err(e) => ( - format!("Failed to {} event. Error: {e}", if pin { "pin" } else { "unpin" }), + format!( + "Failed to {} event. Error: {e}", + if pin { "pin" } else { "unpin" } + ), None, - PopupKind::Error + PopupKind::Error, ), }; enqueue_popup_notification(message, kind, auto_dismissal_duration); @@ -1565,7 +2463,8 @@ impl RoomScreen { } TimelineUpdate::UserPowerLevels(user_power_levels) => { tl.user_power = user_power_levels; - self.view.room_input_bar(cx, ids!(room_input_bar)) + self.view + .room_input_bar(cx, ids!(room_input_bar)) .update_user_power_levels(cx, user_power_levels); // Update the @room mention capability based on the user's power level cx.action(MentionableTextInputAction::PowerLevelsUpdated { @@ -1581,8 +2480,13 @@ impl RoomScreen { tl.latest_own_user_receipt = Some(receipt); } TimelineUpdate::Tombstoned(successor_room_details) => { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .update_tombstone_footer(cx, tl.kind.room_id(), Some(&successor_room_details)); + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .update_tombstone_footer( + cx, + tl.kind.room_id(), + Some(&successor_room_details), + ); tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} @@ -1613,7 +2517,6 @@ impl RoomScreen { } } - /// Handles a link being clicked in any child widgets of this RoomScreen. /// /// Returns `true` if the given `action` was handled as a link click. @@ -1657,7 +2560,11 @@ impl RoomScreen { true } MatrixId::Room(room_id) => { - if self.room_name_id.as_ref().is_some_and(|r| r.room_id() == room_id) { + if self + .room_name_id + .as_ref() + .is_some_and(|r| r.room_id() == room_id) + { enqueue_popup_notification( "You are already viewing that room.", PopupKind::Info, @@ -1665,7 +2572,9 @@ impl RoomScreen { ); return true; } - if let Some(room_name_id) = cx.get_global::().get_room_name(room_id) { + if let Some(room_name_id) = + cx.get_global::().get_room_name(room_id) + { cx.action(AppStateAction::NavigateToRoom { room_to_close: None, destination_room: BasicRoomDetails::Name(room_name_id), @@ -1699,8 +2608,7 @@ impl RoomScreen { let mut link_was_handled = false; if let Ok(matrix_to_uri) = MatrixToUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_to_uri.id(), matrix_to_uri.via()); - } - else if let Ok(matrix_uri) = MatrixUri::parse(&url) { + } else if let Ok(matrix_uri) = MatrixUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_uri.id(), matrix_uri.via()); } @@ -1716,8 +2624,13 @@ impl RoomScreen { } } true - } - else if let RobrixHtmlLinkAction::ClickedMatrixLink { url, matrix_id, via, .. } = action.as_widget_action().cast() { + } else if let RobrixHtmlLinkAction::ClickedMatrixLink { + url, + matrix_id, + via, + .. + } = action.as_widget_action().cast() + { let link_was_handled = handle_matrix_link(&matrix_id, &via); if !link_was_handled { log!("Opening URL \"{}\"", url); @@ -1731,8 +2644,7 @@ impl RoomScreen { } } true - } - else { + } else { false } } @@ -1748,8 +2660,13 @@ impl RoomScreen { let Some(media_source) = mxc_uri else { return; }; - let Some(tl_state) = self.tl_state.as_mut() else { return }; - let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) else { return }; + let Some(tl_state) = self.tl_state.as_mut() else { + return; + }; + let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) + else { + return; + }; let timestamp_millis = event_tl_item.timestamp(); let (image_name, image_file_size) = get_image_name_and_filesize(event_tl_item); @@ -1759,10 +2676,7 @@ impl RoomScreen { image_name, image_file_size, timestamp: unix_time_millis_to_datetime(timestamp_millis), - avatar_parameter: Some(( - tl_state.kind.clone(), - event_tl_item.clone(), - )), + avatar_parameter: Some((tl_state.kind.clone(), event_tl_item.clone())), }), ))); @@ -1783,13 +2697,15 @@ impl RoomScreen { details: &MessageDetails, ) -> Option<&'a EventTimelineItem> { let target_event_id = details.event_id()?; - if let Some(event) = items.get(details.item_id) + if let Some(event) = items + .get(details.item_id) .and_then(|item| item.as_event()) .filter(|ev| ev.event_id().is_some_and(|id| id == target_event_id)) { return Some(event); } - items.iter() + items + .iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .filter_map(|item| item.as_event()) @@ -1806,9 +2722,15 @@ impl RoomScreen { ) { let room_screen_widget_uid = self.widget_uid(); for action in actions { - match action.as_widget_action().widget_uid_eq(room_screen_widget_uid).cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(room_screen_widget_uid) + .cast_ref() + { MessageAction::React { details, reaction } => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; submit_async_request(MatrixRequest::ToggleReaction { timeline_kind: tl.kind.clone(), timeline_event_id: details.timeline_event_id.clone(), @@ -1816,19 +2738,24 @@ impl RoomScreen { }); } MessageAction::Reply(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; - if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details).cloned() { + let Some(tl) = self.tl_state.as_ref() else { + return; + }; + if let Some(event_tl_item) = + Self::find_event_in_timeline(&tl.items, details).cloned() + { let replied_to_info = EmbeddedEvent::from_timeline_item(&event_tl_item); - self.view.room_input_bar(cx, ids!(room_input_bar)) + self.view + .room_input_bar(cx, ids!(room_input_bar)) .show_replying_to(cx, (event_tl_item, replied_to_info), &tl.kind); - } - else { + } else { enqueue_popup_notification( "Could not find message in timeline to reply to. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", + error!( + "MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -1836,22 +2763,21 @@ impl RoomScreen { } } MessageAction::Edit(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane( - cx, - event_tl_item.clone(), - tl.kind.clone(), - ); - } - else { + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane(cx, event_tl_item.clone(), tl.kind.clone()); + } else { enqueue_popup_notification( "Could not find message in timeline to edit. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", + error!( + "MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -1859,21 +2785,20 @@ impl RoomScreen { } } MessageAction::EditLatest => { - let Some(tl) = self.tl_state.as_ref() else { return }; - if let Some(latest_sent_msg) = tl.items + let Some(tl) = self.tl_state.as_ref() else { + return; + }; + if let Some(latest_sent_msg) = tl + .items .iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .find_map(|item| item.as_event().filter(|ev| ev.is_editable()).cloned()) { - self.view.room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane( - cx, - latest_sent_msg, - tl.kind.clone(), - ); - } - else { + self.view + .room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane(cx, latest_sent_msg, tl.kind.clone()); + } else { enqueue_popup_notification( "No recent message available to edit. Please manually select a message to edit.", PopupKind::Warning, @@ -1882,7 +2807,9 @@ impl RoomScreen { } } MessageAction::Pin(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -1898,7 +2825,9 @@ impl RoomScreen { } } MessageAction::Unpin(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -1914,17 +2843,19 @@ impl RoomScreen { } } MessageAction::CopyText(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { cx.copy_to_clipboard(&plaintext_body_of_timeline_item(event_tl_item)); - } - else { + } else { enqueue_popup_notification( "Could not find message in timeline to copy text from. Please try again.", PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", + error!( + "MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1932,22 +2863,49 @@ impl RoomScreen { } } MessageAction::CopyHtml(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; // The logic for getting the formatted body of a message is the same // as the logic used in `populate_message_view()`. let mut success = false; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { if let Some(message) = event_tl_item.content().as_message() { match message.msgtype() { - MessageType::Text(TextMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Notice(NoticeMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Emote(EmoteMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Image(ImageMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::File(FileMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Audio(AudioMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::Video(VideoMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) - | MessageType::VerificationRequest(KeyVerificationRequestEventContent { formatted: Some(FormattedBody { body, .. }), .. }) => - { + MessageType::Text(TextMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Notice(NoticeMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Emote(EmoteMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Image(ImageMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::File(FileMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Audio(AudioMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::Video(VideoMessageEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }) + | MessageType::VerificationRequest( + KeyVerificationRequestEventContent { + formatted: Some(FormattedBody { body, .. }), + .. + }, + ) => { cx.copy_to_clipboard(body); success = true; } @@ -1961,7 +2919,8 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", + error!( + "MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1969,7 +2928,9 @@ impl RoomScreen { } } MessageAction::CopyLink(details) => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; if let Some(event_id) = details.event_id() { let matrix_to_uri = tl.kind.room_id().matrix_to_event_uri(event_id.clone()); cx.copy_to_clipboard(&matrix_to_uri.to_string()); @@ -1979,7 +2940,8 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!("MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", + error!( + "MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -1987,8 +2949,11 @@ impl RoomScreen { } } MessageAction::ViewSource(details) => { - let Some(tl) = self.tl_state.as_ref() else { continue }; - let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) else { + let Some(tl) = self.tl_state.as_ref() else { + continue; + }; + let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) + else { enqueue_popup_notification( "Could not find message in timeline to view source.", PopupKind::Error, @@ -2012,7 +2977,9 @@ impl RoomScreen { } MessageAction::JumpToRelated(details) => { let Some(related_event_id) = details.related_event_id.as_ref() else { - error!("BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}"); + error!( + "BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}" + ); enqueue_popup_notification( "Could not find related message or event in timeline.", PopupKind::Error, @@ -2025,25 +2992,21 @@ impl RoomScreen { related_event_id, Some(details.item_id), portal_list, - loading_pane + loading_pane, ); } MessageAction::JumpToEvent(event_id) => { - self.jump_to_event( - cx, - event_id, - None, - portal_list, - loading_pane - ); + self.jump_to_event(cx, event_id, None, portal_list, loading_pane); } MessageAction::OpenThread(thread_root_event_id) => { let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { - error!("### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!"); - continue + error!( + "### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!" + ); + continue; }; cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, RoomsListAction::Selected(SelectedRoom::Thread { room_name_id, thread_root_event_id: thread_root_event_id.clone(), @@ -2051,13 +3014,17 @@ impl RoomScreen { ); } MessageAction::Redact { details, reason } => { - let Some(tl) = self.tl_state.as_ref() else { return }; + let Some(tl) = self.tl_state.as_ref() else { + return; + }; let timeline_event_id = details.timeline_event_id.clone(); let timeline_kind = tl.kind.clone(); let reason = reason.clone(); let content = ConfirmationModalContent { title_text: "Delete Message".into(), - body_text: "Are you sure you want to delete this message? This cannot be undone.".into(), + body_text: + "Are you sure you want to delete this message? This cannot be undone." + .into(), accept_button_text: Some("Delete".into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::RedactMessage { @@ -2075,14 +3042,15 @@ impl RoomScreen { // } // This is handled within the Message widget itself. - MessageAction::HighlightMessage(..) => { } + MessageAction::HighlightMessage(..) => {} // This is handled by the top-level App itself. - MessageAction::OpenMessageContextMenu { .. } => { } + MessageAction::OpenMessageContextMenu { .. } => {} // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarOpen { .. } => { } + MessageAction::ActionBarOpen { .. } => {} // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarClose => { } - MessageAction::None => { } + MessageAction::ActionBarClose => {} + MessageAction::ToggleAppServiceActions => {} + MessageAction::None => {} } } } @@ -2100,14 +3068,17 @@ impl RoomScreen { portal_list: &PortalListRef, loading_pane: &LoadingPaneRef, ) { - let Some(tl) = self.tl_state.as_mut() else { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; let max_tl_idx = max_tl_idx.unwrap_or_else(|| tl.items.len()); // Attempt to find the index of replied-to message in the timeline. // Start from the current item's index (`tl_idx`) and search backwards, // since we know the related message must come before the current item. let mut num_items_searched = 0; - let related_msg_tl_index = tl.items + let related_msg_tl_index = tl + .items .focus() .narrow(..max_tl_idx) .into_iter() @@ -2130,11 +3101,13 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { - item_id: index - }; + tl.message_highlight_animation_state = + MessageHighlightAnimationState::Pending { item_id: index }; } else { - log!("The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", tl.kind.room_id()); + log!( + "The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", + tl.kind.room_id() + ); // Here, we set the state of the loading pane and display it to the user. // The main logic will be handled in `process_timeline_updates()`, which is the only // place where we can receive updates to the timeline from the background tasks. @@ -2187,7 +3160,9 @@ impl RoomScreen { /// Invoke this when this timeline is being shown, /// e.g., when the user navigates to this timeline. fn show_timeline(&mut self, cx: &mut Cx) { - let kind = self.timeline_kind.clone() + let kind = self + .timeline_kind + .clone() .expect("BUG: Timeline::show_timeline(): no timeline_kind was set."); let room_id = kind.room_id().clone(); @@ -2204,8 +3179,10 @@ impl RoomScreen { return; } if !self.is_loaded && self.all_rooms_loaded { - panic!("BUG: timeline {kind} is not loaded, but its RoomScreen \ - was not waiting for its timeline to be loaded either."); + panic!( + "BUG: timeline {kind} is not loaded, but its RoomScreen \ + was not waiting for its timeline to be loaded either." + ); } return; }; @@ -2278,14 +3255,19 @@ impl RoomScreen { self.is_loaded = is_loaded_now; } - self.view.restore_status_view(cx, ids!(restore_status_view)).set_visible(cx, !self.is_loaded); + self.view + .restore_status_view(cx, ids!(restore_status_view)) + .set_visible(cx, !self.is_loaded); // Kick off a back pagination request if it's the first time loading this room, // because we want to show the user some messages as soon as possible // when they first open the room, and there might not be any messages yet. if is_first_time_being_loaded { if !tl_state.fully_paginated { - log!("Sending a first-time backwards pagination request for {}", tl_state.kind); + log!( + "Sending a first-time backwards pagination request for {}", + tl_state.kind + ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -2354,7 +3336,9 @@ impl RoomScreen { /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown. fn hide_timeline(&mut self) { - let Some(timeline_kind) = self.timeline_kind.clone() else { return }; + let Some(timeline_kind) = self.timeline_kind.clone() else { + return; + }; self.save_state(); @@ -2386,13 +3370,23 @@ impl RoomScreen { /// Note: after calling this function, the widget's `tl_state` will be `None`. fn save_state(&mut self) { let Some(mut tl) = self.tl_state.take() else { - error!("Timeline::save_state(): skipping due to missing state, room {:?}, {:?}", self.timeline_kind, self.room_name_id.as_ref().map(|r| r.display_name())); + error!( + "Timeline::save_state(): skipping due to missing state, room {:?}, {:?}", + self.timeline_kind, + self.room_name_id.as_ref().map(|r| r.display_name()) + ); return; }; let portal_list = self.child_by_path(ids!(timeline.list)).as_portal_list(); let room_input_bar = self.child_by_path(ids!(room_input_bar)).as_room_input_bar(); - log!("Saving state for room {:?}\n\t{:?}\n\tfirst_id: {:?}, scroll: {}", self.room_name_id.as_ref().map(|r| r.display_name()), self.timeline_kind, portal_list.first_id(), portal_list.scroll_position()); + log!( + "Saving state for room {:?}\n\t{:?}\n\tfirst_id: {:?}, scroll: {}", + self.room_name_id.as_ref().map(|r| r.display_name()), + self.timeline_kind, + portal_list.first_id(), + portal_list.scroll_position() + ); let state = SavedState { first_index_and_scroll: Some((portal_list.first_id(), portal_list.scroll_position())), room_input_bar_state: room_input_bar.save_state(), @@ -2417,7 +3411,12 @@ impl RoomScreen { // 1. Restore the position of the timeline. let portal_list = self.portal_list(cx, ids!(timeline.list)); if let Some((first_index, scroll_from_first_id)) = first_index_and_scroll { - log!("Restoring state for room {:?}: first_id: {:?}, scroll: {}", self.room_name_id, first_index, scroll_from_first_id); + log!( + "Restoring state for room {:?}: first_id: {:?}, scroll: {}", + self.room_name_id, + first_index, + scroll_from_first_id + ); portal_list.set_first_id_and_scroll(*first_index, *scroll_from_first_id); portal_list.set_tail_range(false); } else { @@ -2426,7 +3425,10 @@ impl RoomScreen { // The explicit reset is necessary when the same RoomScreen widget is reused for a // different room (e.g., via stack navigation view alternation), otherwise the portal list // would retain the previous room's scroll position which may be out of bounds. - log!("Restoring state for room {:?}: first_id: None, scroll: None", self.room_name_id); + log!( + "Restoring state for room {:?}: first_id: None, scroll: None", + self.room_name_id + ); portal_list.set_first_id_and_scroll(0, 0.0); portal_list.set_tail_range(true); } @@ -2463,12 +3465,17 @@ impl RoomScreen { // If this timeline is already displayed, we don't need to do anything major, // but we do need update the `room_name_id` in case it has changed, or it has been cleared. - if self.timeline_kind.as_ref().is_some_and(|kind| kind == &timeline_kind) { + if self + .timeline_kind + .as_ref() + .is_some_and(|kind| kind == &timeline_kind) + { self.room_name_id = Some(room_name_id.clone()); return; } self.hide_timeline(); + self.reset_app_service_ui(cx); // Reset the the state of the inner loading pane. self.loading_pane(cx, ids!(loading_pane)).take_state(); @@ -2498,7 +3505,9 @@ impl RoomScreen { return; } let first_index = portal_list.first_id(); - let Some(tl_state) = self.tl_state.as_mut() else { return }; + let Some(tl_state) = self.tl_state.as_mut() else { + return; + }; if let Some(ref mut index) = tl_state.prev_first_index { // to detect change of scroll when scroll ends @@ -2509,7 +3518,7 @@ impl RoomScreen { .items .get(std::cmp::min( first_index + portal_list.visible_items(), - tl_state.items.len().saturating_sub(1) + tl_state.items.len().saturating_sub(1), )) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) @@ -2529,17 +3538,20 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } else { - if let Some(own_user_receipt_timestamp) = &tl_state.latest_own_user_receipt.clone() - .and_then(|receipt| receipt.ts) { + if let Some(own_user_receipt_timestamp) = &tl_state + .latest_own_user_receipt + .clone() + .and_then(|receipt| receipt.ts) + { let Some((_first_event_id, first_timestamp)) = tl_state .items .get(first_index) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) - else { - *index = first_index; - return; - }; + else { + *index = first_index; + return; + }; if own_user_receipt_timestamp >= &first_timestamp && own_user_receipt_timestamp <= &last_timestamp { @@ -2550,7 +3562,6 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } - } } } @@ -2569,14 +3580,22 @@ impl RoomScreen { actions: &ActionsBuf, portal_list: &PortalListRef, ) { - let Some(tl) = self.tl_state.as_mut() else { return }; - if tl.fully_paginated { return }; - if !portal_list.scrolled(actions) { return }; + let Some(tl) = self.tl_state.as_mut() else { + return; + }; + if tl.fully_paginated { + return; + }; + if !portal_list.scrolled(actions) { + return; + }; let first_index = portal_list.first_id(); if first_index == 0 && tl.last_scrolled_index > 0 { - log!("Scrolled up from item {} --> 0, sending back pagination request for room {}", - tl.last_scrolled_index, tl.kind, + log!( + "Scrolled up from item {} --> 0, sending back pagination request for room {}", + tl.last_scrolled_index, + tl.kind, ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl.kind.clone(), @@ -2596,7 +3615,9 @@ impl RoomScreenRef { room_name_id: &RoomNameId, thread_root_event_id: Option, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_displayed_room(cx, room_name_id, thread_root_event_id); } } @@ -2609,9 +3630,10 @@ pub struct RoomScreenProps { pub timeline_kind: TimelineKind, pub room_members: Option>>, pub room_avatar_url: Option, + pub app_service_enabled: bool, + pub app_service_room_bound: bool, } - /// Actions for the room screen's tooltip. #[derive(Clone, Debug, Default)] pub enum RoomScreenTooltipActions { @@ -2710,9 +3732,7 @@ pub enum TimelineUpdate { /// includes a complete list of room members that can be shared across components. /// This is different from RoomMembersSynced which only indicates members were fetched /// but doesn't provide the actual data. - RoomMembersListFetched { - members: Vec, - }, + RoomMembersListFetched { members: Vec }, /// A notice with an option of Media Request Parameters that one or more requested media items (images, videos, etc.) /// that should be displayed in this timeline have now been fetched and are available. MediaFetched(MediaRequestParameters), @@ -2744,7 +3764,7 @@ thread_local! { /// The global set of all timeline states, one entry per room. /// /// This is only useful when accessed from the main UI thread. - static TIMELINE_STATES: RefCell> = + static TIMELINE_STATES: RefCell> = RefCell::new(HashMap::new()); } @@ -2860,7 +3880,9 @@ struct TimelineUiState { #[derive(Default, Debug)] enum MessageHighlightAnimationState { - Pending { item_id: usize }, + Pending { + item_id: usize, + }, #[default] Off, } @@ -2897,9 +3919,8 @@ fn find_new_item_matching_current_item( ) -> Option<(usize, usize, f64, OwnedEventId)> { let mut curr_item_focus = curr_items.focus(); let mut idx_curr = starting_at_curr_idx; - let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = Vec::with_capacity( - portal_list.visible_items() - ); + let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = + Vec::with_capacity(portal_list.visible_items()); // Find all items with real event IDs that are currently visible in the portal list. // TODO: if this is slow, we could limit it to 3-5 events at the most. @@ -2928,7 +3949,9 @@ fn find_new_item_matching_current_item( // some may be zeroed-out, so we need to account for that possibility by only // using events that have a real non-zero area if let Some(pos_offset) = portal_list.position_of_item(cx, *idx_curr) { - log!("Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}"); + log!( + "Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}" + ); return Some((*idx_curr, idx_new, pos_offset, event_id.to_owned())); } } @@ -3002,7 +4025,8 @@ fn populate_message_view( TimelineItemContent::MsgLike(_msg_like_content) => { let prev_msg_sender = prev_event_tl_item.sender(); prev_msg_sender == event_tl_item.sender() - && ts_millis.0 + && ts_millis + .0 .checked_sub(prev_event_tl_item.timestamp().0) .is_some_and(|d| d < uint!(600000)) // 10 mins in millis } @@ -3019,8 +4043,12 @@ fn populate_message_view( let (item, used_cached_item) = match &msg_like_content.kind { MsgLikeKind::Message(msg) => { match msg.msgtype() { - MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Text(TextMessageEventContent { + body, formatted, .. + }) => { + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3048,9 +4076,13 @@ fn populate_message_view( } // A notice message is just a message sent by an automated bot, // so we treat it just like a message but use a different font color. - MessageType::Notice(NoticeMessageEventContent{body, formatted, ..}) => { + MessageType::Notice(NoticeMessageEventContent { + body, formatted, .. + }) => { is_notice = true; - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3060,7 +4092,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = + item.html_or_plaintext(cx, ids!(content.message)); // Apply gray color to all text styles for notice messages. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -3090,7 +4123,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = + item.html_or_plaintext(cx, ids!(content.message)); // Apply red color to all text styles for server notices. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -3105,10 +4139,12 @@ fn populate_message_view( "Server notice: {}\n\nNotice type:: {}{}{}", sn.body, sn.server_notice_type.as_str(), - sn.limit_type.as_ref() + sn.limit_type + .as_ref() .map(|l| format!("\nLimit type: {}", l.as_str())) .unwrap_or_default(), - sn.admin_contact.as_ref() + sn.admin_contact + .as_ref() .map(|c| format!("\nAdmin contact: {}", c)) .unwrap_or_default(), ); @@ -3131,8 +4167,12 @@ fn populate_message_view( } // An emote is just like a message but is prepended with the user's name // to indicate that it's an "action" that the user is performing. - MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { - has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Emote(EmoteMessageEventContent { + body, formatted, .. + }) => { + has_html_body = formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3143,14 +4183,16 @@ fn populate_message_view( (item, true) } else { // Draw the profile up front here because we need the username for the emote body. - let (username, profile_drawn) = item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ); + let (username, profile_drawn) = item + .avatar(cx, ids!(profile.avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ); // Prepend a "* " to the emote body, as suggested by the Matrix spec. let (body, formatted) = if let Some(fb) = formatted.as_ref() { @@ -3159,7 +4201,7 @@ fn populate_message_view( Some(FormattedBody { format: fb.format.clone(), body: format!("* {} {}", &username, &fb.body), - }) + }), ) } else { (Cow::from(format!("* {} {}", &username, body)), None) @@ -3183,7 +4225,9 @@ fn populate_message_view( } } MessageType::Image(image) => { - has_html_body = image.formatted.as_ref() + has_html_body = image + .formatted + .as_ref() .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedImageMessage) @@ -3221,17 +4265,17 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - let is_location_fully_drawn = populate_location_message_content( - cx, - &html_or_plaintext_ref, - location, - ); + let is_location_fully_drawn = + populate_location_message_content(cx, &html_or_plaintext_ref, location); new_drawn_status.content_drawn = is_location_fully_drawn; (item, false) } } MessageType::File(file_content) => { - has_html_body = file_content.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = file_content + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3243,16 +4287,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_file_message_content( - cx, - &html_or_plaintext_ref, - file_content, - ); + new_drawn_status.content_drawn = + populate_file_message_content(cx, &html_or_plaintext_ref, file_content); (item, false) } } MessageType::Audio(audio) => { - has_html_body = audio.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = audio + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3264,16 +4308,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_audio_message_content( - cx, - &html_or_plaintext_ref, - audio, - ); + new_drawn_status.content_drawn = + populate_audio_message_content(cx, &html_or_plaintext_ref, audio); (item, false) } } MessageType::Video(video) => { - has_html_body = video.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = video + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -3285,16 +4329,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = populate_video_message_content( - cx, - &html_or_plaintext_ref, - video, - ); + new_drawn_status.content_drawn = + populate_video_message_content(cx, &html_or_plaintext_ref, video); (item, false) } } MessageType::VerificationRequest(verification) => { - has_html_body = verification.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = verification + .formatted + .as_ref() + .is_some_and(|f| f.format == MessageFormat::Html); let template = id!(Message); let (item, existed) = list.item_with_existed(cx, item_id, template); if existed && item_drawn_status.content_drawn { @@ -3306,7 +4350,8 @@ fn populate_message_view( body: format!( "Sent a verification request to {}.
(Supported methods: {})
", verification.to, - verification.methods + verification + .methods .iter() .map(|m| m.as_str()) .collect::>() @@ -3336,10 +4381,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)).set_text( - cx, - &format!("[Unsupported {:?}]", msg_like_content.kind), - ); + item.label(cx, ids!(content.message)) + .set_text(cx, &format!("[Unsupported {:?}]", msg_like_content.kind)); new_drawn_status.content_drawn = true; (item, false) } @@ -3349,7 +4392,9 @@ fn populate_message_view( // Handle sticker messages that are static images. MsgLikeKind::Sticker(sticker) => { has_html_body = false; - let StickerEventContent { body, info, source, .. } = sticker.content(); + let StickerEventContent { + body, info, source, .. + } = sticker.content(); let template = if use_compact_view { id!(CondensedImageMessage) @@ -3378,7 +4423,7 @@ fn populate_message_view( (item, true) } } - } + } // Handle messages that have been redacted (deleted). MsgLikeKind::Redacted => { has_html_body = false; @@ -3417,10 +4462,8 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)).set_text( - cx, - &format!("[Unsupported {:?}] ", other), - ); + item.label(cx, ids!(content.message)) + .set_text(cx, &format!("[Unsupported {:?}] ", other)); new_drawn_status.content_drawn = true; (item, false) } @@ -3432,13 +4475,14 @@ fn populate_message_view( // If we didn't use a cached item, we need to draw all other message content: // the reactions, the read receipts avatar row, the reply preview. if !used_cached_item { - item.reaction_list(cx, ids!(content.reaction_list)).set_list( - cx, - event_tl_item.content().reactions(), - timeline_kind.clone(), - timeline_event_id.clone(), - item_id, - ); + item.reaction_list(cx, ids!(content.reaction_list)) + .set_list( + cx, + event_tl_item.content().reactions(), + timeline_kind.clone(), + timeline_event_id.clone(), + item_id, + ); populate_read_receipts(&item, cx, timeline_kind, event_tl_item); let is_reply_fully_drawn = draw_replied_to_message( cx, @@ -3465,17 +4509,21 @@ fn populate_message_view( new_drawn_status.content_drawn &= is_thread_summary_fully_drawn; } - // We must always re-set the message details, even when re-using a cached portallist item, // because the item type might be the same but for a different message entirely. let message_details = MessageDetails { thread_root_event_id: msg_like_content.thread_root.clone().or_else(|| { - msg_like_content.thread_summary.as_ref() + msg_like_content + .thread_summary + .as_ref() .and_then(|_| event_tl_item.event_id().map(|id| id.to_owned())) }), timeline_event_id, item_id, - related_event_id: msg_like_content.in_reply_to.as_ref().map(|r| r.event_id.clone()), + related_event_id: msg_like_content + .in_reply_to + .as_ref() + .map(|r| r.event_id.clone()), room_screen_widget_uid, abilities: MessageAbilities::from_user_power_and_event( user_power_levels, @@ -3488,7 +4536,6 @@ fn populate_message_view( }; item.as_message().set_data(message_details); - // If `used_cached_item` is false, we should always redraw the profile, even if profile_drawn is true. let skip_draw_profile = use_compact_view || (used_cached_item && item_drawn_status.profile_drawn); @@ -3499,17 +4546,20 @@ fn populate_message_view( // log!("\t --> populate_message_view(): DRAWING profile draw for item_id: {item_id}"); let mut username_label = item.label(cx, ids!(content.username)); - if !is_server_notice { // the normal case - let (username, profile_drawn) = set_username_and_get_avatar_retval.unwrap_or_else(|| - item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ) - ); + if !is_server_notice { + // the normal case + let (username, profile_drawn) = + set_username_and_get_avatar_retval.unwrap_or_else(|| { + item.avatar(cx, ids!(profile.avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ) + }); if is_notice { script_apply_eval!(cx, username_label, { draw_text +: { @@ -3519,8 +4569,7 @@ fn populate_message_view( } username_label.set_text(cx, &username); new_drawn_status.profile_drawn = profile_drawn; - } - else { + } else { // Server notices are drawn with a red color avatar background and username. let avatar = item.avatar(cx, ids!(profile.avatar)); avatar.show_text(cx, Some(COLOR_FG_DANGER_RED), None, "⚠"); @@ -3541,33 +4590,46 @@ fn populate_message_view( // Set the timestamp. if let Some(dt) = unix_time_millis_to_datetime(ts_millis) { - item.timestamp(cx, ids!(profile.timestamp)).set_date_time(cx, dt); + item.timestamp(cx, ids!(profile.timestamp)) + .set_date_time(cx, dt); } // Set the "edited" indicator if this message was edited. if msg_like_content.as_message().is_some_and(|m| m.is_edited()) { - item.edited_indicator(cx, ids!(profile.edited_indicator)).set_latest_edit( - cx, - event_tl_item, - ); + item.edited_indicator(cx, ids!(profile.edited_indicator)) + .set_latest_edit(cx, event_tl_item); } - #[cfg(feature = "tsp")] { + #[cfg(feature = "tsp")] + { use matrix_sdk::ruma::serde::Base64; - use crate::tsp::{self, tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}}; + use crate::tsp::{ + self, + tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}, + }; - if let Some(mut tsp_sig) = event_tl_item.latest_json() + if let Some(mut tsp_sig) = event_tl_item + .latest_json() .and_then(|raw| raw.get_field::("content").ok()) .flatten() .and_then(|content_obj| content_obj.get("org.robius.tsp_signature").cloned()) .and_then(|tsp_sig_value| serde_json::from_value::(tsp_sig_value).ok()) .map(|b64| b64.into_inner()) { - log!("Found event {:?} with TSP signature.", event_tl_item.event_id()); - let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref().lock().unwrap() + log!( + "Found event {:?} with TSP signature.", + event_tl_item.event_id() + ); + let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref() + .lock() + .unwrap() .get_verified_vid_for(event_tl_item.sender()) { - log!("Found verified VID for sender {}: \"{}\"", event_tl_item.sender(), sender_vid.identifier()); + log!( + "Found verified VID for sender {}: \"{}\"", + event_tl_item.sender(), + sender_vid.identifier() + ); tsp_sdk::crypto::verify(&*sender_vid, &mut tsp_sig).map_or( TspSignState::WrongSignature, |(msg, msg_type)| { @@ -3579,7 +4641,11 @@ fn populate_message_view( TspSignState::Unknown }; - log!("TSP signature state for event {:?} is {:?}", event_tl_item.event_id(), tsp_sign_state); + log!( + "TSP signature state for event {:?} is {:?}", + event_tl_item.event_id(), + tsp_sign_state + ); item.tsp_sign_indicator(cx, ids!(profile.tsp_sign_indicator)) .show_with_state(cx, tsp_sign_state); } @@ -3602,7 +4668,8 @@ fn populate_text_message_content( ) -> bool { // The message was HTML-formatted rich text. let mut links = Vec::new(); - if let Some(fb) = formatted_body.as_ref() + if let Some(fb) = formatted_body + .as_ref() .and_then(|fb| (fb.format == MessageFormat::Html).then_some(fb)) { let linkified_html = utils::linkify_get_urls( @@ -3622,7 +4689,7 @@ fn populate_text_message_content( }; // Populate link previews if all required parameters are provided - if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = + if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = (link_preview_ref, media_cache, link_preview_cache) { link_preview_ref.populate_below_message( @@ -3650,7 +4717,8 @@ fn populate_image_message_content( ) -> bool { // We don't use thumbnails, as their resolution is too low to be visually useful. // We also don't trust the provided mimetype, as it can be incorrect. - let (mimetype, _width, _height) = image_info_source.as_ref() + let (mimetype, _width, _height) = image_info_source + .as_ref() .map(|info| (info.mimetype.as_deref(), info.width, info.height)) .unwrap_or_default(); @@ -3658,10 +4726,7 @@ fn populate_image_message_content( // then show a message about it being unsupported (e.g., for animated gifs). if let Some(mime) = mimetype.as_ref() { if ImageFormat::from_mimetype(mime).is_none() { - text_or_image_ref.show_text( - cx, - format!("{body}\n\nUnsupported type {mime:?}"), - ); + text_or_image_ref.show_text(cx, format!("{body}\n\nUnsupported type {mime:?}")); return true; // consider this as fully drawn } } @@ -3670,102 +4735,132 @@ fn populate_image_message_content( // A closure that fetches and shows the image from the given `mxc_uri`, // marking it as fully drawn if the image was available. - let mut fetch_and_show_image_uri = |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { - match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { - (MediaCacheEntry::Loaded(data), _media_format) => { - let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)),|cx, img| { - utils::load_png_or_jpg(&img, cx, &data) - .map(|()| img.size_in_pixels(cx).unwrap_or_default()) - }); - if let Err(e) = show_image_result { - let err_str = format!("{body}\n\nFailed to display image: {e:?}"); - error!("{err_str}"); - text_or_image_ref.show_text(cx, &err_str); - } - - // We're done drawing the image, so mark it as fully drawn. - fully_drawn = true; - } - (MediaCacheEntry::Requested, _media_format) => { - // If the image is being fetched, we try to show its blurhash. - if let (Some(ref blurhash), Some(width), Some(height)) = (image_info.blurhash.clone(), image_info.width, image_info.height) { - let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)), |cx, img| { - let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) else { - return Err(image_cache::ImageError::EmptyData) - }; - let (width, height): (u32, u32) = (width, height); - if width == 0 || height == 0 { - warning!("Image had an invalid aspect ratio (width or height of 0)."); - return Err(image_cache::ImageError::EmptyData); - } - let aspect_ratio: f32 = width as f32 / height as f32; - // Cap the blurhash to a max size of 500 pixels in each dimension - // because the `blurhash::decode()` function can be rather expensive. - let (mut capped_width, mut capped_height) = (width, height); - if capped_height > BLURHASH_IMAGE_MAX_SIZE { - capped_height = BLURHASH_IMAGE_MAX_SIZE; - capped_width = (capped_height as f32 * aspect_ratio).floor() as u32; - } - if capped_width > BLURHASH_IMAGE_MAX_SIZE { - capped_width = BLURHASH_IMAGE_MAX_SIZE; - capped_height = (capped_width as f32 / aspect_ratio).floor() as u32; - } - - match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { - Ok(data) => { - ImageBuffer::new(&data, capped_width as usize, capped_height as usize).map(|img_buff| { - let texture = Some(img_buff.into_new_texture(cx)); - img.set_texture(cx, texture); - img.size_in_pixels(cx).unwrap_or_default() - }) - } - Err(e) => { - error!("Failed to decode blurhash {e:?}"); - Err(image_cache::ImageError::EmptyData) - } - } - }); + let mut fetch_and_show_image_uri = + |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { + match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { + (MediaCacheEntry::Loaded(data), _media_format) => { + let show_image_result = text_or_image_ref.show_image( + cx, + Some(MediaSource::Plain(mxc_uri)), + |cx, img| { + utils::load_png_or_jpg(&img, cx, &data) + .map(|()| img.size_in_pixels(cx).unwrap_or_default()) + }, + ); if let Err(e) = show_image_result { let err_str = format!("{body}\n\nFailed to display image: {e:?}"); error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } + + // We're done drawing the image, so mark it as fully drawn. + fully_drawn = true; } - fully_drawn = false; - } - (MediaCacheEntry::Failed(_status_code), _media_format) => { - if text_or_image_ref.view(cx, ids!(default_image_view)).visible() { + (MediaCacheEntry::Requested, _media_format) => { + // If the image is being fetched, we try to show its blurhash. + if let (Some(ref blurhash), Some(width), Some(height)) = ( + image_info.blurhash.clone(), + image_info.width, + image_info.height, + ) { + let show_image_result = text_or_image_ref.show_image( + cx, + Some(MediaSource::Plain(mxc_uri)), + |cx, img| { + let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) + else { + return Err(image_cache::ImageError::EmptyData); + }; + let (width, height): (u32, u32) = (width, height); + if width == 0 || height == 0 { + warning!( + "Image had an invalid aspect ratio (width or height of 0)." + ); + return Err(image_cache::ImageError::EmptyData); + } + let aspect_ratio: f32 = width as f32 / height as f32; + // Cap the blurhash to a max size of 500 pixels in each dimension + // because the `blurhash::decode()` function can be rather expensive. + let (mut capped_width, mut capped_height) = (width, height); + if capped_height > BLURHASH_IMAGE_MAX_SIZE { + capped_height = BLURHASH_IMAGE_MAX_SIZE; + capped_width = + (capped_height as f32 * aspect_ratio).floor() as u32; + } + if capped_width > BLURHASH_IMAGE_MAX_SIZE { + capped_width = BLURHASH_IMAGE_MAX_SIZE; + capped_height = + (capped_width as f32 / aspect_ratio).floor() as u32; + } + + match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { + Ok(data) => ImageBuffer::new( + &data, + capped_width as usize, + capped_height as usize, + ) + .map(|img_buff| { + let texture = Some(img_buff.into_new_texture(cx)); + img.set_texture(cx, texture); + img.size_in_pixels(cx).unwrap_or_default() + }), + Err(e) => { + error!("Failed to decode blurhash {e:?}"); + Err(image_cache::ImageError::EmptyData) + } + } + }, + ); + if let Err(e) = show_image_result { + let err_str = format!("{body}\n\nFailed to display image: {e:?}"); + error!("{err_str}"); + text_or_image_ref.show_text(cx, &err_str); + } + } + fully_drawn = false; + } + (MediaCacheEntry::Failed(_status_code), _media_format) => { + if text_or_image_ref + .view(cx, ids!(default_image_view)) + .visible() + { + fully_drawn = true; + return; + } + text_or_image_ref.show_text( + cx, + format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri), + ); + // For now, we consider this as being "complete". In the future, we could support + // retrying to fetch thumbnail of the image on a user click/tap. fully_drawn = true; - return; } - text_or_image_ref - .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri)); - // For now, we consider this as being "complete". In the future, we could support - // retrying to fetch thumbnail of the image on a user click/tap. - fully_drawn = true; } - } - }; + }; - let mut fetch_and_show_media_source = |cx: &mut Cx, media_source: MediaSource, image_info: Box| { - match media_source { - MediaSource::Encrypted(encrypted) => { - // We consider this as "fully drawn" since we don't yet support encryption. - text_or_image_ref.show_text( - cx, - format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) - ); - }, - MediaSource::Plain(mxc_uri) => { - fetch_and_show_image_uri(cx, mxc_uri, image_info) + let mut fetch_and_show_media_source = + |cx: &mut Cx, media_source: MediaSource, image_info: Box| { + match media_source { + MediaSource::Encrypted(encrypted) => { + // We consider this as "fully drawn" since we don't yet support encryption. + text_or_image_ref.show_text( + cx, + format!( + "{body}\n\n[TODO] fetch encrypted image at {:?}", + encrypted.url + ), + ); + } + MediaSource::Plain(mxc_uri) => fetch_and_show_image_uri(cx, mxc_uri, image_info), } - } - }; + }; match image_info_source { Some(image_info) => { // Use the provided thumbnail URI if it exists; otherwise use the original URI. - let media_source = image_info.thumbnail_source.clone() + let media_source = image_info + .thumbnail_source + .clone() .unwrap_or(original_source); fetch_and_show_media_source(cx, media_source, image_info); } @@ -3778,7 +4873,6 @@ fn populate_image_message_content( fully_drawn } - /// Draws a file message's content into the given `message_content_widget`. /// /// Returns whether the file message content was fully drawn. @@ -3795,7 +4889,8 @@ fn populate_file_message_content( .and_then(|info| info.size) .map(|bytes| format!(" ({})", ByteSize::b(bytes.into()))) .unwrap_or_default(); - let caption = file_content.formatted_caption() + let caption = file_content + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| file_content.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3822,20 +4917,23 @@ fn populate_audio_message_content( let (duration, mime, size) = audio .info .as_ref() - .map(|info| ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - )) + .map(|info| { + ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + ) + }) .unwrap_or_default(); - let caption = audio.formatted_caption() + let caption = audio + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| audio.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3849,7 +4947,6 @@ fn populate_audio_message_content( true } - /// Draws a video message's content into the given `message_content_widget`. /// /// Returns whether the video message content was fully drawn. @@ -3863,23 +4960,26 @@ fn populate_video_message_content( let (duration, mime, size, dimensions) = video .info .as_ref() - .map(|info| ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - info.width.and_then(|width| - info.height.map(|height| format!(" {width}x{height},")) - ).unwrap_or_default(), - )) + .map(|info| { + ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + info.width + .and_then(|width| info.height.map(|height| format!(" {width}x{height},"))) + .unwrap_or_default(), + ) + }) .unwrap_or_default(); - let caption = video.formatted_caption() + let caption = video + .formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| video.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -3893,8 +4993,6 @@ fn populate_video_message_content( true } - - /// Draws the given location message's content into the `message_content_widget`. /// /// Returns whether the location message content was fully drawn. @@ -3903,8 +5001,9 @@ fn populate_location_message_content( message_content_widget: &HtmlOrPlaintextRef, location: &LocationMessageEventContent, ) -> bool { - let coords = location.geo_uri - .get(utils::GEO_URI_SCHEME.len() ..) + let coords = location + .geo_uri + .get(utils::GEO_URI_SCHEME.len()..) .and_then(|s| { let mut iter = s.split(','); if let (Some(lat), Some(long)) = (iter.next(), iter.next()) { @@ -3914,8 +5013,14 @@ fn populate_location_message_content( } }); if let Some((lat, long)) = coords { - let short_lat = lat.find('.').and_then(|dot| lat.get(..dot + 7)).unwrap_or(lat); - let short_long = long.find('.').and_then(|dot| long.get(..dot + 7)).unwrap_or(long); + let short_lat = lat + .find('.') + .and_then(|dot| lat.get(..dot + 7)) + .unwrap_or(lat); + let short_long = long + .find('.') + .and_then(|dot| long.get(..dot + 7)) + .unwrap_or(long); let safe_lat = htmlize::escape_attribute(lat); let safe_long = htmlize::escape_attribute(long); let safe_geo_uri = htmlize::escape_attribute(&location.geo_uri); @@ -3934,7 +5039,10 @@ fn populate_location_message_content( } else { message_content_widget.show_html( cx, - format!("[Location invalid] {}", htmlize::escape_text(&location.body)) + format!( + "[Location invalid] {}", + htmlize::escape_text(&location.body) + ), ); } @@ -3944,7 +5052,6 @@ fn populate_location_message_content( true } - /// Draws the given redacted message's content into the `message_content_widget`. /// /// Returns whether the redacted message content was fully drawn. @@ -3957,16 +5064,13 @@ fn populate_redacted_message_content( let fully_drawn: bool; let mut redactor_id_and_reason = None; if let Some(redacted_msg) = event_tl_item.latest_json() { - if let Ok(AnySyncTimelineEvent::MessageLike( - AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Redacted(redaction) - ) - )) = redacted_msg.deserialize() { + if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(redaction), + ))) = redacted_msg.deserialize() + { if let Ok(redacted_because) = redaction.unsigned.redacted_because.deserialize() { - redactor_id_and_reason = Some(( - redacted_because.sender, - redacted_because.content.reason, - )); + redactor_id_and_reason = + Some((redacted_because.sender, redacted_because.content.reason)); } } } @@ -3975,7 +5079,10 @@ fn populate_redacted_message_content( if redactor == event_tl_item.sender() { fully_drawn = true; match reason { - Some(r) => format!("⛔ Deleted their own message. Reason: \"{}\".", htmlize::escape_text(r)), + Some(r) => format!( + "⛔ Deleted their own message. Reason: \"{}\".", + htmlize::escape_text(r) + ), None => String::from("⛔ Deleted their own message."), } } else { @@ -3987,9 +5094,11 @@ fn populate_redacted_message_content( true, ); fully_drawn = redactor_name.was_found(); - let redactor_name_esc = htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); + let redactor_name_esc = + htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); match reason { - Some(r) => format!("⛔ {} deleted this message. Reason: \"{}\".", + Some(r) => format!( + "⛔ {} deleted this message. Reason: \"{}\".", redactor_name_esc, htmlize::escape_text(r), ), @@ -4004,7 +5113,6 @@ fn populate_redacted_message_content( fully_drawn } - /// Draws a ReplyPreview above a message if it was in-reply to another message. /// /// ## Arguments @@ -4031,24 +5139,24 @@ fn draw_replied_to_message( show_reply = true; match &in_reply_to_details.event { TimelineDetails::Ready(replied_to_event) => { - let (in_reply_to_username, is_avatar_fully_drawn) = - replied_to_message_view - .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) - .set_avatar_and_get_username( - cx, - timeline_kind, - &replied_to_event.sender, - Some(&replied_to_event.sender_profile), - Some(in_reply_to_details.event_id.as_ref()), - true, - ); + let (in_reply_to_username, is_avatar_fully_drawn) = replied_to_message_view + .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + &replied_to_event.sender, + Some(&replied_to_event.sender_profile), + Some(in_reply_to_details.event_id.as_ref()), + true, + ); fully_drawn = is_avatar_fully_drawn; replied_to_message_view .label(cx, ids!(replied_to_message_content.reply_preview_username)) .set_text(cx, in_reply_to_username.as_str()); - let msg_body = replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); + let msg_body = + replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); populate_preview_of_timeline_item( cx, &msg_body, @@ -4160,7 +5268,8 @@ fn populate_thread_root_summary( &embedded_event.content, &embedded_event.sender, sender_username, - ).format_with(sender_username, true); + ) + .format_with(sender_username, true); match utils::replace_linebreaks_separators(&preview, true) { Cow::Borrowed(_) => Cow::Owned(preview), Cow::Owned(replaced) => Cow::Owned(replaced), @@ -4171,9 +5280,11 @@ fn populate_thread_root_summary( if td.is_unavailable() && let Some(thread_root_event_id) = thread_root_event_id.clone() { - let needs_refresh = fetched_summary - .is_none_or(|fs| fs.latest_reply_preview_text.is_none()); - if needs_refresh && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) { + let needs_refresh = + fetched_summary.is_none_or(|fs| fs.latest_reply_preview_text.is_none()); + if needs_refresh + && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) + { submit_async_request(MatrixRequest::FetchThreadSummaryDetails { timeline_kind: timeline_kind.clone(), thread_root_event_id, @@ -4181,7 +5292,8 @@ fn populate_thread_root_summary( }); } } - fetched_summary.and_then(|fs| fs.latest_reply_preview_text.as_deref()) + fetched_summary + .and_then(|fs| fs.latest_reply_preview_text.as_deref()) .unwrap_or("Loading latest reply...") .into() } @@ -4193,7 +5305,7 @@ fn populate_thread_root_summary( let replies_count_text = match replies_count { 1 => Cow::Borrowed("1 reply"), - n => Cow::Owned(format!("{n} replies")) + n => Cow::Owned(format!("{n} replies")), }; item.label(cx, ids!(thread_summary_count)) .set_text(cx, &replies_count_text); @@ -4213,23 +5325,32 @@ pub fn populate_preview_of_timeline_item( ) { if let Some(m) = timeline_item_content.as_message() { match m.msgtype() { - MessageType::Text(TextMessageEventContent { body, formatted, .. }) - | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { - let _ = populate_text_message_content(cx, widget_out, body, formatted.as_ref(), None, None, None); + MessageType::Text(TextMessageEventContent { + body, formatted, .. + }) + | MessageType::Notice(NoticeMessageEventContent { + body, formatted, .. + }) => { + let _ = populate_text_message_content( + cx, + widget_out, + body, + formatted.as_ref(), + None, + None, + None, + ); return; } - _ => { } // fall through to the general case for all timeline items below. + _ => {} // fall through to the general case for all timeline items below. } } - let html = text_preview_of_timeline_item( - timeline_item_content, - sender_user_id, - sender_username, - ).format_with(sender_username, true); + let html = + text_preview_of_timeline_item(timeline_item_content, sender_user_id, sender_username) + .format_with(sender_username, true); widget_out.show_html(cx, html); } - /// A trait for abstracting over the different types of timeline events /// that can be displayed in a `SmallStateEvent` widget. trait SmallStateEventContent { @@ -4320,7 +5441,9 @@ impl SmallStateEventContent for PollState { ) -> (WidgetRef, ItemDrawnStatus) { item.label(cx, ids!(content)).set_text( cx, - self.fallback_text().unwrap_or_else(|| self.results().question).as_str(), + self.fallback_text() + .unwrap_or_else(|| self.results().question) + .as_str(), ); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -4389,20 +5512,15 @@ impl SmallStateEventContent for RoomMembershipChange { ) -> (WidgetRef, ItemDrawnStatus) { let Some(preview) = text_preview_of_room_membership_change(self, false) else { // Don't actually display anything for nonexistent/unimportant membership changes. - return ( - list.item(cx, item_id, id!(Empty)), - ItemDrawnStatus::new(), - ); + return (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::new()); }; item.label(cx, ids!(content)) .set_text(cx, &preview.format_with(username, false)); // The invite_user_button is only used for "Knocked" membership change events. - item.button(cx, ids!(invite_user_button)).set_visible( - cx, - matches!(self.change(), Some(MembershipChange::Knocked)), - ); + item.button(cx, ids!(invite_user_button)) + .set_visible(cx, matches!(self.change(), Some(MembershipChange::Knocked))); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -4454,7 +5572,8 @@ fn populate_small_state_event( ); // Draw the timestamp as part of the profile. if let Some(dt) = unix_time_millis_to_datetime(event_tl_item.timestamp()) { - item.timestamp(cx, ids!(left_container.timestamp)).set_date_time(cx, dt); + item.timestamp(cx, ids!(left_container.timestamp)) + .set_date_time(cx, dt); } new_drawn_status.profile_drawn = profile_drawn; username @@ -4473,7 +5592,6 @@ fn populate_small_state_event( ) } - /// Returns the display name of the sender of the given `event_tl_item`, if available. fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option { if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { @@ -4483,7 +5601,6 @@ fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option } } - /// Actions related to invites within a room. /// /// These are NOT widget actions, just regular actions. @@ -4518,7 +5635,6 @@ pub enum InviteResultAction { }, } - /// Actions related to a specific message within a room timeline. #[derive(Clone, Default, Debug)] pub enum MessageAction { @@ -4563,7 +5679,6 @@ pub enum MessageAction { // /// The user clicked the "report" button on a message. // Report(MessageDetails), - /// The message at the given item index in the timeline should be highlighted. HighlightMessage(usize), /// The user requested that we show a context menu with actions @@ -4583,6 +5698,8 @@ pub enum MessageAction { }, /// The user requested closing the message action bar ActionBarClose, + /// The user requested toggling the in-room app service quick actions card. + ToggleAppServiceActions, #[default] None, } @@ -4594,14 +5711,126 @@ impl ActionDefaultRef for MessageAction { } } +#[derive(Clone, Default, Debug)] +pub enum AppServicePanelAction { + Dismiss, + OpenCreateBotModal, + OpenDeleteBotModal, + SendListBots, + SendBotHelp, + Unbind, + #[default] + None, +} + +impl ActionDefaultRef for AppServicePanelAction { + fn default_ref() -> &'static Self { + static DEFAULT: AppServicePanelAction = AppServicePanelAction::None; + &DEFAULT + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct AppServicePanel { + #[deref] + view: View, +} + +impl Widget for AppServicePanel { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + + let room_screen_props = scope + .props + .get::() + .expect("BUG: RoomScreenProps should be available in Scope::props for AppServicePanel"); + + if let Event::Actions(actions) = event { + if self + .view + .button(cx, ids!(bubble.header.dismiss_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::Dismiss, + ); + } + + if self + .view + .button(cx, ids!(keyboard.first_row.create_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::OpenCreateBotModal, + ); + } + + if self + .view + .button(cx, ids!(keyboard.first_row.list_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::SendListBots, + ); + } + + if self + .view + .button(cx, ids!(keyboard.second_row.delete_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::OpenDeleteBotModal, + ); + } + + if self + .view + .button(cx, ids!(keyboard.second_row.help_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::SendBotHelp, + ); + } + + if self + .view + .button(cx, ids!(keyboard.third_row.unbind_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::Unbind, + ); + } + } + } + + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} + /// A widget representing a single message of any kind within a room timeline. #[derive(Script, ScriptHook, Widget, Animator)] pub struct Message { - #[source] source: ScriptObjectRef, - #[deref] view: View, - #[apply_default] animator: Animator, - - #[rust] details: Option, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, + #[apply_default] + animator: Animator, + + #[rust] + details: Option, } impl Widget for Message { @@ -4616,7 +5845,9 @@ impl Widget for Message { self.animator_play(cx, ids!(highlight.off)); } - let Some(details) = self.details.clone() else { return }; + let Some(details) = self.details.clone() else { + return; + }; // We first handle a click on the replied-to message preview, if present, // because we don't want any widgets within the replied-to message to be @@ -4625,31 +5856,31 @@ impl Widget for Message { Hit::FingerDown(fe) => { if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } // If the hit occurred on the replied-to message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::JumpToRelated(details.clone()), ); } - _ => { } + _ => {} } // Handle clicks on the thread summary shown beneath a thread-root message. @@ -4666,11 +5897,11 @@ impl Widget for Message { apply_hover(cx, COLOR_THREAD_SUMMARY_BG_HOVER); if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } @@ -4682,23 +5913,23 @@ impl Widget for Message { } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } Hit::FingerUp(fe) => { apply_hover(cx, COLOR_THREAD_SUMMARY_BG); if fe.is_over && fe.is_primary_hit() && fe.was_tap() { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenThread(thread_root_event_id.clone()), ); } } - _ => { } + _ => {} } } @@ -4717,21 +5948,21 @@ impl Widget for Message { // A right click means we should display the context menu. if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - } + }, ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - } + }, ); } Hit::FingerHoverIn(..) => { @@ -4742,12 +5973,16 @@ impl Widget for Message { self.animator_play(cx, ids!(hover.off)); // TODO: here, hide the "action bar" buttons upon hover-out } - _ => { } + _ => {} } if let Event::Actions(actions) = event { for action in actions { - match action.as_widget_action().widget_uid_eq(details.room_screen_widget_uid).cast_ref() { + match action + .as_widget_action() + .widget_uid_eq(details.room_screen_widget_uid) + .cast_ref() + { MessageAction::HighlightMessage(id) if id == &details.item_id => { self.animator_play(cx, ids!(highlight.on)); self.redraw(cx); @@ -4759,7 +5994,11 @@ impl Widget for Message { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - if self.details.as_ref().is_some_and(|d| d.should_be_highlighted) { + if self + .details + .as_ref() + .is_some_and(|d| d.should_be_highlighted) + { script_apply_eval!(cx, self, { draw_bg +: { color: #ffffd1, @@ -4780,7 +6019,9 @@ impl Message { impl MessageRef { fn set_data(&self, details: MessageDetails) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.set_data(details); } } @@ -4789,7 +6030,7 @@ impl MessageRef { /// /// This function requires passing in a reference to `Cx`, /// which isn't used, but acts as a guarantee that this function -/// must only be called by the main UI thread. +/// must only be called by the main UI thread. pub fn clear_timeline_states(_cx: &mut Cx) { // Clear timeline states cache TIMELINE_STATES.with_borrow_mut(|states| { diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 7cf7d5106..0b1ae8c77 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -16,30 +16,50 @@ //! so you can use it from other widgets or functions on the main UI thread //! that need to query basic info about a particular room or space. -use std::{cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, rc::Rc, sync::Arc}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + rc::Rc, + sync::Arc, +}; use crossbeam_queue::SegQueue; use makepad_widgets::*; use matrix_sdk_ui::spaces::room_list::SpaceRoomListPaginationState; use ruma::events::tag::TagName; use tokio::sync::mpsc::UnboundedSender; -use matrix_sdk::{RoomState, ruma::{events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId}}; +use matrix_sdk::{ + RoomState, + ruma::{ + events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, + }, +}; use crate::{ app::{AppState, SelectedRoom}, home::{ - navigation_tab_bar::{NavigationBarAction, SelectedTab}, room_context_menu::RoomContextMenuDetails, rooms_list_entry::RoomsListEntryAction, space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt} + navigation_tab_bar::{NavigationBarAction, SelectedTab}, + room_context_menu::RoomContextMenuDetails, + rooms_list_entry::RoomsListEntryAction, + space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt}, }, room::{ FetchedRoomAvatar, - room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn}, + room_display_filter::{ + RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn, + }, }, shared::{ - collapsible_header::{CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory}, + collapsible_header::{ + CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory, + }, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction, }, - sliding_sync::{MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request}, - space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, utils::{RoomNameId, VecDiff}, + sliding_sync::{ + MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request, + }, + space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, + utils::{RoomNameId, VecDiff}, }; /// Whether to pre-paginate visible rooms at least once in order to @@ -71,11 +91,10 @@ pub fn get_invited_rooms(_cx: &mut Cx) -> Rc }, + LoadedRooms { max_rooms: Option }, /// Add a new room to the list of rooms the user has been invited to. /// This will be maintained and displayed separately from joined rooms. AddInvitedRoom(InvitedRoomInfo), @@ -171,9 +189,7 @@ pub enum RoomsListUpdate { unread_mentions: u64, }, /// Update the displayable name for the given room. - UpdateRoomName { - new_room_name: RoomNameId, - }, + UpdateRoomName { new_room_name: RoomNameId }, /// Update the avatar (image) for the given room. UpdateRoomAvatar { room_id: OwnedRoomId, @@ -196,21 +212,15 @@ pub enum RoomsListUpdate { new_tags: Tags, }, /// Update the status label at the bottom of the list of all rooms. - Status { - status: String, - }, + Status { status: String }, /// Mark the given room as tombstoned. - TombstonedRoom { - room_id: OwnedRoomId - }, + TombstonedRoom { room_id: OwnedRoomId }, /// Hide the given room from being displayed. /// /// This is useful for temporarily preventing a room from being shown, /// e.g., after a room has been left but before the homeserver has registered /// that we left it and removed it via the RoomListService. - HideRoom { - room_id: OwnedRoomId, - }, + HideRoom { room_id: OwnedRoomId }, /// Scroll to the given room. ScrollToRoom(OwnedRoomId), /// The background space service is now listening for requests, @@ -237,9 +247,7 @@ pub enum RoomsListAction { /// A new room was joined from an accepted invite, /// meaning that the existing `InviteScreen` should be converted /// to a `RoomScreen` to display the now-joined room. - InviteAccepted { - room_name_id: RoomNameId, - }, + InviteAccepted { room_name_id: RoomNameId }, /// Instructs the top-level app to show the context menu for the given room. /// /// Emitted by the RoomsList when the user right-clicks or long-presses @@ -259,7 +267,6 @@ impl ActionDefaultRef for RoomsListAction { } } - /// UI-related info about a joined room. /// /// This includes info needed display a preview of that room in the RoomsList @@ -298,7 +305,6 @@ pub struct JoinedRoomInfo { pub is_direct: bool, /// Whether this room is tombstoned (shut down and replaced with a successor room). pub is_tombstoned: bool, - // TODO: we could store the parent chain(s) of this room, i.e., which spaces // they are children of. One room can be in multiple spaces. } @@ -390,28 +396,34 @@ struct SpaceMapValue { #[derive(Script, Widget)] pub struct RoomsList { - #[deref] view: View, + #[deref] + view: View, /// The list of all rooms that the user has been invited to. /// /// This is a shared reference to the thread-local [`ALL_INVITED_ROOMS`] variable. - #[rust] invited_rooms: Rc>>, + #[rust] + invited_rooms: Rc>>, /// The set of all joined rooms and their cached info. /// This includes both direct rooms and regular rooms, but not invited rooms. - #[rust] all_joined_rooms: HashMap, + #[rust] + all_joined_rooms: HashMap, /// The list of all room IDs in display order, matching the order from the room list service. - #[rust] all_known_rooms_order: VecDeque, + #[rust] + all_known_rooms_order: VecDeque, /// The space that is currently selected as a display filter for the rooms list, if any. /// * If `None` (default), no space is selected, and all rooms can be shown. /// * If `Some`, the rooms list is in "space" mode. A special "Space Lobby" entry /// is shown at the top, and only child rooms within this space will be displayed. - #[rust] selected_space: Option, + #[rust] + selected_space: Option, /// The sender used to send Space-related requests to the background service. - #[rust] space_request_sender: Option>, + #[rust] + space_request_sender: Option>, /// A flattened map of all spaces known to the client. /// @@ -419,50 +431,66 @@ pub struct RoomsList { /// and nested subspaces *directly* within that space. /// /// This can include both joined and non-joined spaces. - #[rust] space_map: HashMap, + #[rust] + space_map: HashMap, /// Rooms that are explicitly hidden and should never be shown in the rooms list. - #[rust] hidden_rooms: HashSet, + #[rust] + hidden_rooms: HashSet, /// The currently-active filter function for the list of rooms. /// /// ## Important Notes /// 1. Do not use this directly. Instead, use the `should_display_room!()` macro. /// 2. This does *not* get auto-applied when it changes, for performance reasons. - #[rust] display_filter: RoomDisplayFilter, + #[rust] + display_filter: RoomDisplayFilter, /// The currently-active sort function for the list of rooms. - #[rust] sort_fn: Option>, + #[rust] + sort_fn: Option>, /// The list of invited rooms currently displayed in the UI. - #[rust] displayed_invited_rooms: Vec, - #[rust(false)] is_invited_rooms_header_expanded: bool, - #[rust] invited_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_invited_rooms: Vec, + #[rust(false)] + is_invited_rooms_header_expanded: bool, + #[rust] + invited_rooms_indexes: RoomCategoryIndexes, /// The list of direct rooms currently displayed in the UI. - #[rust] displayed_direct_rooms: Vec, - #[rust(false)] is_direct_rooms_header_expanded: bool, - #[rust] direct_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_direct_rooms: Vec, + #[rust(false)] + is_direct_rooms_header_expanded: bool, + #[rust] + direct_rooms_indexes: RoomCategoryIndexes, /// The list of regular (non-direct) joined rooms currently displayed in the UI. /// /// **Direct rooms are excluded** from this; they are in `displayed_direct_rooms`. - #[rust] displayed_regular_rooms: Vec, - #[rust(true)] is_regular_rooms_header_expanded: bool, - #[rust] regular_rooms_indexes: RoomCategoryIndexes, + #[rust] + displayed_regular_rooms: Vec, + #[rust(true)] + is_regular_rooms_header_expanded: bool, + #[rust] + regular_rooms_indexes: RoomCategoryIndexes, /// The latest status message that should be displayed in the bottom status label. - #[rust] status: String, + #[rust] + status: String, /// The currently-selected room. - #[rust] current_active_room: Option, + #[rust] + current_active_room: Option, /// The maximum number of rooms that will ever be loaded. /// /// This should not be used to determine whether all requested rooms have been loaded, /// because we will likely never receive this many rooms due to the room list service /// excluding rooms that we have filtered out (e.g., left or tombstoned rooms, spaces, etc). - #[rust] max_known_rooms: Option, + #[rust] + max_known_rooms: Option, // /// Whether the room list service has loaded all requested rooms from the homeserver. // #[rust] all_rooms_loaded: bool, } @@ -485,15 +513,16 @@ macro_rules! should_display_room { ($self:expr, $room_id:expr, $room:expr) => { !$self.hidden_rooms.contains($room_id) && ($self.display_filter)($room) - && $self.selected_space.as_ref() + && $self + .selected_space + .as_ref() .is_none_or(|space| $self.is_room_indirectly_in_space(space.room_id(), $room_id)) }; } - impl RoomsList { /// Returns whether the homeserver has finished syncing all of the rooms - /// that should be synced to our client based on the currently-specified room list filter. + /// that should be synced to our client based on the currently-specified room list filter. pub fn all_rooms_loaded(&self) -> bool { // TODO: fix this: figure out a way to determine if // all requested rooms have been received from the homeserver. @@ -522,7 +551,10 @@ impl RoomsList { RoomsListUpdate::AddInvitedRoom(invited_room) => { let room_id = invited_room.room_name_id.room_id().clone(); let should_display = should_display_room!(self, &room_id, &invited_room); - let _replaced = self.invited_rooms.borrow_mut().insert(room_id.clone(), invited_room); + let _replaced = self + .invited_rooms + .borrow_mut() + .insert(room_id.clone(), invited_room); if should_display { self.displayed_invited_rooms.push(room_id); } @@ -548,24 +580,29 @@ impl RoomsList { // 3. Emit an action to inform other widgets that the InviteScreen // displaying the invite to this room should be converted to a // RoomScreen displaying the now-joined room. - if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) { + if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) + { log!("Removed room {room_id} from the list of invited rooms"); - self.displayed_invited_rooms.iter() + self.displayed_invited_rooms + .iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); if let Some(room) = self.all_joined_rooms.get(&room_id) { cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::InviteAccepted { room_name_id: room.room_name_id.clone(), - } + }, ); } } self.update_status(); SignalToUI::set_ui_signal(); // signal the RoomScreen to update itself } - RoomsListUpdate::UpdateRoomAvatar { room_id, room_avatar } => { + RoomsListUpdate::UpdateRoomAvatar { + room_id, + room_avatar, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.room_avatar = room_avatar; } else if let Some(room) = self.invited_rooms.borrow_mut().get_mut(&room_id) { @@ -574,14 +611,23 @@ impl RoomsList { error!("Error: couldn't find room {room_id} to update avatar"); } } - RoomsListUpdate::UpdateLatestEvent { room_id, timestamp, latest_message_text } => { + RoomsListUpdate::UpdateLatestEvent { + room_id, + timestamp, + latest_message_text, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.latest = Some((timestamp, latest_message_text)); } else { error!("Error: couldn't find room {room_id} to update latest event"); } } - RoomsListUpdate::UpdateNumUnreadMessages { room_id, is_marked_unread, unread_messages, unread_mentions } => { + RoomsListUpdate::UpdateNumUnreadMessages { + room_id, + is_marked_unread, + unread_messages, + unread_mentions, + } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.num_unread_messages = match unread_messages { UnreadMessageCount::Unknown => 0, @@ -590,11 +636,13 @@ impl RoomsList { room.num_unread_mentions = unread_mentions; room.is_marked_unread = is_marked_unread; } else { - warning!("Warning: couldn't find room {} to update unread messages count", room_id); + warning!( + "Warning: couldn't find room {} to update unread messages count", + room_id + ); } } RoomsListUpdate::UpdateRoomName { new_room_name } => { - // TODO: broadcast a new AppState action to ensure that this room's or space's new name // gets updated in all of the `SelectedRoom` instances throughout Robrix, // e.g., the name of the room in the Dock Tab or the StackNav header. @@ -607,12 +655,16 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms.iter().position(|r| r == &room_id), + self.displayed_direct_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms.iter().position(|r| r == &room_id), + self.displayed_regular_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -630,7 +682,9 @@ impl RoomsList { if let Some(invited_room) = invited_rooms.get_mut(&room_id) { invited_room.room_name_id = new_room_name; let should_display = should_display_room!(self, &room_id, invited_room); - let pos_in_list = self.displayed_invited_rooms.iter() + let pos_in_list = self + .displayed_invited_rooms + .iter() .position(|r| r == &room_id); if should_display { if pos_in_list.is_none() { @@ -640,7 +694,9 @@ impl RoomsList { pos_in_list.map(|i| self.displayed_invited_rooms.remove(i)); } } else { - warning!("Warning: couldn't find room {new_room_name} to update its name."); + warning!( + "Warning: couldn't find room {new_room_name} to update its name." + ); } } } @@ -651,7 +707,8 @@ impl RoomsList { continue; } enqueue_popup_notification( - format!("{} was changed from {} to {}.", + format!( + "{} was changed from {} to {}.", room.room_name_id, if room.is_direct { "direct" } else { "regular" }, if is_direct { "direct" } else { "regular" } @@ -666,7 +723,8 @@ impl RoomsList { } else { &mut self.displayed_regular_rooms }; - list_to_remove_from.iter() + list_to_remove_from + .iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); @@ -690,19 +748,23 @@ impl RoomsList { // and then options/buttons for the user to re-join it if desired. if let Some(removed) = self.all_joined_rooms.remove(&room_id) { - log!("Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}"); + log!( + "Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}" + ); let list_to_remove_from = if removed.is_direct { &mut self.displayed_direct_rooms } else { &mut self.displayed_regular_rooms }; - list_to_remove_from.iter() + list_to_remove_from + .iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); - } - else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { + } else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) + { log!("Removed room {room_id} from the list of all invited rooms"); - self.displayed_invited_rooms.iter() + self.displayed_invited_rooms + .iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); } @@ -723,7 +785,7 @@ impl RoomsList { } RoomsListUpdate::LoadedRooms { max_rooms } => { self.max_known_rooms = max_rooms; - }, + } RoomsListUpdate::Tags { room_id, new_tags } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.tags = new_tags; @@ -743,12 +805,16 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms.iter().position(|r| r == &room_id), + self.displayed_direct_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms.iter().position(|r| r == &room_id), + self.displayed_regular_rooms + .iter() + .position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -760,20 +826,32 @@ impl RoomsList { pos_in_list.map(|i| displayed_list.remove(i)); } } else { - warning!("Warning: couldn't find room {room_id} to update the tombstone status"); + warning!( + "Warning: couldn't find room {room_id} to update the tombstone status" + ); } } RoomsListUpdate::HideRoom { room_id } => { self.hidden_rooms.insert(room_id.clone()); // Hiding a regular room is the most common case (e.g., after its successor is joined), // so we check that list first. - if let Some(i) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { + if let Some(i) = self + .displayed_regular_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_regular_rooms.remove(i); - } - else if let Some(i) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { + } else if let Some(i) = self + .displayed_direct_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_direct_rooms.remove(i); - } - else if let Some(i) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { + } else if let Some(i) = self + .displayed_invited_rooms + .iter() + .position(|r| r == &room_id) + { self.displayed_invited_rooms.remove(i); } } @@ -782,75 +860,89 @@ impl RoomsList { self.recalculate_indexes(); let portal_list = self.view.portal_list(cx, ids!(list)); let speed = 50.0; - let portal_list_index = if let Some(regular_index) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { + let portal_list_index = if let Some(regular_index) = self + .displayed_regular_rooms + .iter() + .position(|r| r == &room_id) + { self.regular_rooms_indexes.first_room_index + regular_index - } - else if let Some(direct_index) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { + } else if let Some(direct_index) = self + .displayed_direct_rooms + .iter() + .position(|r| r == &room_id) + { self.direct_rooms_indexes.first_room_index + direct_index - } - else if let Some(invited_index) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { + } else if let Some(invited_index) = self + .displayed_invited_rooms + .iter() + .position(|r| r == &room_id) + { self.invited_rooms_indexes.first_room_index + invited_index - } - else { continue }; + } else { + continue; + }; // Scroll to just above the room to make it more obviously visible. - portal_list.smooth_scroll_to(cx, portal_list_index.saturating_sub(1), speed, Some(15)); + portal_list.smooth_scroll_to( + cx, + portal_list_index.saturating_sub(1), + speed, + Some(15), + ); } RoomsListUpdate::SpaceRequestSender(sender) => { self.space_request_sender = Some(sender); - num_updates -= 1; // this does not require a redraw. + num_updates -= 1; // this does not require a redraw. } - RoomsListUpdate::RoomOrderUpdate(diff) => { - match diff { - VecDiff::Append { values } => { - self.all_known_rooms_order.extend(values); - needs_sort = true; - } - VecDiff::Clear => { - self.all_known_rooms_order.clear(); - needs_sort = true; - } - VecDiff::PushFront { value } => { - self.all_known_rooms_order.push_front(value); - needs_sort = true; - } - VecDiff::PushBack { value } => { - self.all_known_rooms_order.push_back(value); - needs_sort = true; - } - VecDiff::PopFront => { - self.all_known_rooms_order.pop_front(); - needs_sort = true; - } - VecDiff::PopBack => { - self.all_known_rooms_order.pop_back(); + RoomsListUpdate::RoomOrderUpdate(diff) => match diff { + VecDiff::Append { values } => { + self.all_known_rooms_order.extend(values); + needs_sort = true; + } + VecDiff::Clear => { + self.all_known_rooms_order.clear(); + needs_sort = true; + } + VecDiff::PushFront { value } => { + self.all_known_rooms_order.push_front(value); + needs_sort = true; + } + VecDiff::PushBack { value } => { + self.all_known_rooms_order.push_back(value); + needs_sort = true; + } + VecDiff::PopFront => { + self.all_known_rooms_order.pop_front(); + needs_sort = true; + } + VecDiff::PopBack => { + self.all_known_rooms_order.pop_back(); + needs_sort = true; + } + VecDiff::Insert { index, value } => { + if index <= self.all_known_rooms_order.len() { + self.all_known_rooms_order.insert(index, value); needs_sort = true; } - VecDiff::Insert { index, value } => { - if index <= self.all_known_rooms_order.len() { - self.all_known_rooms_order.insert(index, value); - needs_sort = true; - } - } - VecDiff::Set { index, value } => { - if let Some(existing) = self.all_known_rooms_order.get_mut(index) { - if *existing != value { - *existing = value; - needs_sort = true; - } - } - } - VecDiff::Remove { index } => { - if index < self.all_known_rooms_order.len() { - self.all_known_rooms_order.remove(index); + } + VecDiff::Set { index, value } => { + if let Some(existing) = self.all_known_rooms_order.get_mut(index) { + if *existing != value { + *existing = value; needs_sort = true; } } - VecDiff::Truncate { length } => { - self.all_known_rooms_order.truncate(length); + } + VecDiff::Remove { index } => { + if index < self.all_known_rooms_order.len() { + self.all_known_rooms_order.remove(index); needs_sort = true; } } - } + VecDiff::Truncate { length } => { + self.all_known_rooms_order.truncate(length); + needs_sort = true; + } + }, } } if needs_sort { @@ -875,9 +967,9 @@ impl RoomsList { + self.displayed_regular_rooms.len(); let mut text = match (self.display_filter.is_none(), num_rooms) { - (true, 0) => "No joined or invited rooms found".to_string(), - (true, 1) => "Loaded 1 room".to_string(), - (true, n) => format!("Loaded {n} rooms"), + (true, 0) => "No joined or invited rooms found".to_string(), + (true, 1) => "Loaded 1 room".to_string(), + (true, n) => format!("Loaded {n} rooms"), (false, 0) => "No matching rooms found".to_string(), (false, 1) => "Found 1 matching room".to_string(), (false, n) => format!("Found {n} matching rooms"), @@ -926,7 +1018,6 @@ impl RoomsList { self.redraw(cx); } - /// Generates a tuple of three kinds of displayed rooms (accounting for the current `display_filter`): /// 1. displayed_invited_rooms /// 2. displayed_regular_rooms @@ -934,7 +1025,7 @@ impl RoomsList { /// /// If `self.sort_fn` is `Some`, the rooms are ordered based on that function. /// Otherwise, the rooms are ordered based on `self.all_known_rooms_order` (the default). - fn generate_displayed_rooms(&self) -> (Vec,Vec, Vec) { + fn generate_displayed_rooms(&self) -> (Vec, Vec, Vec) { let mut new_displayed_invited_rooms = Vec::new(); let mut new_displayed_regular_rooms = Vec::new(); let mut new_displayed_direct_rooms = Vec::new(); @@ -952,7 +1043,9 @@ impl RoomsList { // If a sort function was provided, use it. if let Some(sort_fn) = self.sort_fn.as_deref() { - let mut filtered_joined_rooms = self.all_joined_rooms.iter() + let mut filtered_joined_rooms = self + .all_joined_rooms + .iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_joined_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -960,7 +1053,8 @@ impl RoomsList { push_joined_room(room_id, jr) } - let mut filtered_invited_rooms = invited_rooms_ref.iter() + let mut filtered_invited_rooms = invited_rooms_ref + .iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_invited_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -983,7 +1077,11 @@ impl RoomsList { } } - (new_displayed_invited_rooms, new_displayed_regular_rooms, new_displayed_direct_rooms) + ( + new_displayed_invited_rooms, + new_displayed_regular_rooms, + new_displayed_direct_rooms, + ) } /// Calculates the indexes in the PortalList where the headers and rooms should be drawn. @@ -996,35 +1094,35 @@ impl RoomsList { // Based on the various displayed room lists and is_expanded state of each room header, // calculate the indexes in the PortalList where the headers and rooms should be drawn. let should_show_invited_rooms_header = !self.displayed_invited_rooms.is_empty(); - let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); + let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); let should_show_regular_rooms_header = !self.displayed_regular_rooms.is_empty(); let index_of_invited_rooms_header = should_show_invited_rooms_header.then_some(0); let index_of_first_invited_room = should_show_invited_rooms_header as usize; - let index_after_invited_rooms = index_of_first_invited_room + - if self.is_invited_rooms_header_expanded { + let index_after_invited_rooms = index_of_first_invited_room + + if self.is_invited_rooms_header_expanded { self.displayed_invited_rooms.len() } else { 0 }; - let index_of_direct_rooms_header = should_show_direct_rooms_header - .then_some(index_after_invited_rooms); - let index_of_first_direct_room = index_after_invited_rooms + - should_show_direct_rooms_header as usize; - let index_after_direct_rooms = index_of_first_direct_room + - if self.is_direct_rooms_header_expanded { + let index_of_direct_rooms_header = + should_show_direct_rooms_header.then_some(index_after_invited_rooms); + let index_of_first_direct_room = + index_after_invited_rooms + should_show_direct_rooms_header as usize; + let index_after_direct_rooms = index_of_first_direct_room + + if self.is_direct_rooms_header_expanded { self.displayed_direct_rooms.len() } else { 0 }; - let index_of_regular_rooms_header = should_show_regular_rooms_header - .then_some(index_after_direct_rooms); - let index_of_first_regular_room = index_after_direct_rooms + - should_show_regular_rooms_header as usize; - let index_after_regular_rooms = index_of_first_regular_room + - if self.is_regular_rooms_header_expanded { + let index_of_regular_rooms_header = + should_show_regular_rooms_header.then_some(index_after_direct_rooms); + let index_of_first_regular_room = + index_after_direct_rooms + should_show_regular_rooms_header as usize; + let index_after_regular_rooms = index_of_first_regular_room + + if self.is_regular_rooms_header_expanded { self.displayed_regular_rooms.len() } else { 0 @@ -1050,32 +1148,43 @@ impl RoomsList { /// Handle any incoming updates to spaces' room lists and pagination state. fn handle_space_room_list_action(&mut self, cx: &mut Cx, action: &SpaceRoomListAction) { match action { - SpaceRoomListAction::UpdatedChildren { space_id, parent_chain, direct_child_rooms, direct_subspaces } => { + SpaceRoomListAction::UpdatedChildren { + space_id, + parent_chain, + direct_child_rooms, + direct_subspaces, + } => { match self.space_map.entry(space_id.clone()) { Entry::Occupied(mut occ) => { let occ_mut = occ.get_mut(); occ_mut.parent_chain = parent_chain.clone(); occ_mut.direct_child_rooms = Arc::clone(direct_child_rooms); - occ_mut.direct_subspaces = Arc::clone(direct_subspaces); + occ_mut.direct_subspaces = Arc::clone(direct_subspaces); } Entry::Vacant(vac) => { vac.insert_entry(SpaceMapValue { is_fully_paginated: false, parent_chain: parent_chain.clone(), direct_child_rooms: Arc::clone(direct_child_rooms), - direct_subspaces: Arc::clone(direct_subspaces), + direct_subspaces: Arc::clone(direct_subspaces), }); } } - if self.selected_space.as_ref().is_some_and(|sel_space| - sel_space.room_id() == space_id - || parent_chain.contains(sel_space.room_id()) - ) { + if self.selected_space.as_ref().is_some_and(|sel_space| { + sel_space.room_id() == space_id || parent_chain.contains(sel_space.room_id()) + }) { self.update_displayed_rooms(cx, false); } } - SpaceRoomListAction::PaginationState { space_id, parent_chain, state } => { - let is_fully_paginated = matches!(state, SpaceRoomListPaginationState::Idle { end_reached: true }); + SpaceRoomListAction::PaginationState { + space_id, + parent_chain, + state, + } => { + let is_fully_paginated = matches!( + state, + SpaceRoomListPaginationState::Idle { end_reached: true } + ); // Only re-fetch the list of rooms in this space if it was not already fully paginated. let should_fetch_rooms: bool; match self.space_map.entry(space_id.clone()) { @@ -1094,15 +1203,22 @@ impl RoomsList { } } let Some(sender) = self.space_request_sender.as_ref() else { - error!("BUG: RoomsList: no space request sender was available after pagination state update."); + error!( + "BUG: RoomsList: no space request sender was available after pagination state update." + ); return; }; if should_fetch_rooms { - if sender.send(SpaceRequest::GetChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send GetRooms request for space {space_id}."); + if sender + .send(SpaceRequest::GetChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send GetRooms request for space {space_id}." + ); } } @@ -1112,11 +1228,16 @@ impl RoomsList { // all of its children, such that we can see if any of them are subspaces, // and then we'll paginate those as well. if !is_fully_paginated { - if sender.send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send pagination request for space {space_id}."); + if sender + .send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send pagination request for space {space_id}." + ); } } } @@ -1128,7 +1249,10 @@ impl RoomsList { None, ); } - SpaceRoomListAction::LeaveSpaceResult { space_name_id, result } => match result { + SpaceRoomListAction::LeaveSpaceResult { + space_name_id, + result, + } => match result { Ok(()) => { enqueue_popup_notification( format!("Successfully left space \"{}\".", space_name_id), @@ -1136,7 +1260,11 @@ impl RoomsList { Some(4.0), ); // If the space we left was the currently-selected one, go back to the main Home view. - if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { + if self + .selected_space + .as_ref() + .is_some_and(|s| s.room_id() == space_name_id.room_id()) + { cx.action(NavigationBarAction::GoToHome); } } @@ -1151,14 +1279,18 @@ impl RoomsList { }, // Details-related space actions are handled by SpaceLobbyScreen, not RoomsList. SpaceRoomListAction::DetailedChildren { .. } - | SpaceRoomListAction::TopLevelSpaceDetails(_) => { } + | SpaceRoomListAction::TopLevelSpaceDetails(_) => {} } } /// Returns whether the given target room or space is indirectly within the given parent space. /// /// This will recursively search all nested spaces within the given `parent_space`. - fn is_room_indirectly_in_space(&self, parent_space: &OwnedRoomId, target: &OwnedRoomId) -> bool { + fn is_room_indirectly_in_space( + &self, + parent_space: &OwnedRoomId, + target: &OwnedRoomId, + ) -> bool { if let Some(smv) = self.space_map.get(parent_space) { if smv.direct_child_rooms.contains(target) { return true; @@ -1186,12 +1318,14 @@ impl Widget for RoomsList { let props = RoomsListScopeProps { was_scrolling: self.view.portal_list(cx, ids!(list)).was_scrolling(), }; - let rooms_list_actions = cx.capture_actions( - |cx| self.view.handle_event(cx, event, &mut Scope::with_props(&props)) - ); + let rooms_list_actions = cx.capture_actions(|cx| { + self.view + .handle_event(cx, event, &mut Scope::with_props(&props)) + }); for action in rooms_list_actions { // Handle a regular room (joined or invited) being clicked. - if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() { + if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() + { let new_selected_room = if let Some(jr) = self.all_joined_rooms.get(&room_id) { SelectedRoom::JoinedRoom { room_name_id: jr.room_name_id.clone(), @@ -1207,48 +1341,59 @@ impl Widget for RoomsList { self.current_active_room = Some(new_selected_room.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_room), ); self.redraw(cx); } // Handle a room being right-clicked or long-pressed by opening the room context menu. - else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = action.as_widget_action().cast() { + else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = + action.as_widget_action().cast() + { // Determine details for the context menu let Some(jr) = self.all_joined_rooms.get(&room_id) else { error!("BUG: couldn't find right-clicked room details for room {room_id}"); continue; }; + let app_state = scope.data.get::().unwrap(); let details = RoomContextMenuDetails { room_name_id: jr.room_name_id.clone(), is_favorite: jr.tags.contains_key(&TagName::Favorite), is_low_priority: jr.tags.contains_key(&TagName::LowPriority), is_marked_unread: jr.is_marked_unread, + app_service_enabled: app_state.bot_settings.enabled, + is_bot_bound: app_state.bot_settings.is_room_bound(&room_id), }; cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::OpenRoomContextMenu { details, pos }, ); } // Handle the space lobby being clicked. else if let Some(SpaceLobbyAction::SpaceLobbyEntryClicked) = action.downcast_ref() { - let Some(space_name_id) = self.selected_space.clone() else { continue }; + let Some(space_name_id) = self.selected_space.clone() else { + continue; + }; let new_selected_space = SelectedRoom::Space { space_name_id }; self.current_active_room = Some(new_selected_space.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_space), ); self.redraw(cx); } // Handle a collapsible header being clicked. - else if let CollapsibleHeaderAction::Toggled { category } = action.as_widget_action().cast() { + else if let CollapsibleHeaderAction::Toggled { category } = + action.as_widget_action().cast() + { match category { HeaderCategory::Invites => { - self.is_invited_rooms_header_expanded = !self.is_invited_rooms_header_expanded; + self.is_invited_rooms_header_expanded = + !self.is_invited_rooms_header_expanded; } HeaderCategory::RegularRooms => { - self.is_regular_rooms_header_expanded = !self.is_regular_rooms_header_expanded; + self.is_regular_rooms_header_expanded = + !self.is_regular_rooms_header_expanded; } HeaderCategory::DirectRooms => { self.is_direct_rooms_header_expanded = @@ -1273,47 +1418,73 @@ impl Widget for RoomsList { if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { match tab { SelectedTab::Space { space_name_id } => { - if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { + if self + .selected_space + .as_ref() + .is_some_and(|s| s.room_id() == space_name_id.room_id()) + { continue; } self.selected_space = Some(space_name_id.clone()); - self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, true); + self.view + .space_lobby_entry(cx, ids!(space_lobby_entry)) + .set_visible(cx, true); // If we don't have the full list of children in this newly-selected space, then fetch it. - let (is_fully_paginated, parent_chain) = self.space_map + let (is_fully_paginated, parent_chain) = self + .space_map .get(space_name_id.room_id()) .map(|smv| (smv.is_fully_paginated, smv.parent_chain.clone())) .unwrap_or_default(); if !is_fully_paginated { let Some(sender) = self.space_request_sender.as_ref() else { - error!("BUG: RoomsList: no space request sender was available."); + error!( + "BUG: RoomsList: no space request sender was available." + ); continue; }; - if sender.send(SpaceRequest::SubscribeToSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}."); + if sender + .send(SpaceRequest::SubscribeToSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}." + ); } - if sender.send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }).is_err() { - error!("BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}."); + if sender + .send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}." + ); } - if sender.send(SpaceRequest::GetChildren { - space_id: space_name_id.room_id().clone(), - parent_chain, - }).is_err() { - error!("BUG: RoomsList: failed to send GetRooms request for space {space_name_id}."); + if sender + .send(SpaceRequest::GetChildren { + space_id: space_name_id.room_id().clone(), + parent_chain, + }) + .is_err() + { + error!( + "BUG: RoomsList: failed to send GetRooms request for space {space_name_id}." + ); } } } _ => { self.selected_space = None; - self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, false); + self.view + .space_lobby_entry(cx, ids!(space_lobby_entry)) + .set_visible(cx, false); } } @@ -1372,25 +1543,31 @@ impl Widget for RoomsList { let total_count = status_label_id + 1; let get_invited_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.invited_rooms_indexes.first_room_index) - .and_then(|index| self.is_invited_rooms_header_expanded - .then(|| self.displayed_invited_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.invited_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_invited_rooms_header_expanded + .then(|| self.displayed_invited_rooms.get(index)) + }) .flatten() }; let get_direct_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.direct_rooms_indexes.first_room_index) - .and_then(|index| self.is_direct_rooms_header_expanded - .then(|| self.displayed_direct_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.direct_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_direct_rooms_header_expanded + .then(|| self.displayed_direct_rooms.get(index)) + }) .flatten() }; let get_regular_room_id = |portal_list_index: usize| { - portal_list_index.checked_sub(self.regular_rooms_indexes.first_room_index) - .and_then(|index| self.is_regular_rooms_header_expanded - .then(|| self.displayed_regular_rooms.get(index)) - ) + portal_list_index + .checked_sub(self.regular_rooms_indexes.first_room_index) + .and_then(|index| { + self.is_regular_rooms_header_expanded + .then(|| self.displayed_regular_rooms.get(index)) + }) .flatten() }; @@ -1402,7 +1579,9 @@ impl Widget for RoomsList { portal_list_ref.set_first_id_and_scroll(status_label_id, 0.0); } // We only care about drawing the portal list. - let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; + let Some(mut list) = portal_list_ref.borrow_mut() else { + continue; + }; list.set_item_range(cx, 0, total_count); @@ -1418,12 +1597,13 @@ impl Widget for RoomsList { self.displayed_invited_rooms.len() as u64, ); item.draw_all(cx, &mut scope); - } - else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { + } else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { let mut invited_rooms_mut = self.invited_rooms.borrow_mut(); if let Some(invited_room) = invited_rooms_mut.get_mut(invited_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - invited_room.is_selected = self.current_active_room.as_ref() + invited_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == invited_room_id); // Pass the room info down to the RoomsListEntry widget via Scope. scope = Scope::with_props(&*invited_room); @@ -1432,8 +1612,7 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } - else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { + } else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1444,11 +1623,12 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } - else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { + } else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { if let Some(direct_room) = self.all_joined_rooms.get_mut(direct_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - direct_room.is_selected = self.current_active_room.as_ref() + direct_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == direct_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1469,8 +1649,7 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } - else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { + } else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1481,11 +1660,12 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } - else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { + } else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { if let Some(regular_room) = self.all_joined_rooms.get_mut(regular_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - regular_room.is_selected = self.current_active_room.as_ref() + regular_room.is_selected = self + .current_active_room + .as_ref() .is_some_and(|sel_room| sel_room.room_id() == regular_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1503,7 +1683,8 @@ impl Widget for RoomsList { scope = Scope::with_props(&*regular_room); item.draw_all(cx, &mut scope); } else { - list.item(cx, portal_list_index, id!(empty)).draw_all(cx, &mut scope); + list.item(cx, portal_list_index, id!(empty)) + .draw_all(cx, &mut scope); } } // Draw the status label as the bottom entry. @@ -1527,7 +1708,9 @@ impl Widget for RoomsList { impl RoomsListRef { /// See [`RoomsList::all_rooms_loaded()`]. pub fn all_rooms_loaded(&self) -> bool { - let Some(inner) = self.borrow() else { return false; }; + let Some(inner) = self.borrow() else { + return false; + }; inner.all_rooms_loaded() } @@ -1544,14 +1727,17 @@ impl RoomsListRef { /// Returns the name of the given room, if it is known and loaded. pub fn get_room_name(&self, room_id: &OwnedRoomId) -> Option { let inner = self.borrow()?; - inner.all_joined_rooms + inner + .all_joined_rooms .get(room_id) .map(|jr| jr.room_name_id.clone()) - .or_else(|| - inner.invited_rooms.borrow() + .or_else(|| { + inner + .invited_rooms + .borrow() .get(room_id) .map(|ir| ir.room_name_id.clone()) - ) + }) } /// Returns the currently-selected space (the one selected in the SpacesBar). @@ -1561,7 +1747,10 @@ impl RoomsListRef { /// Same as [`Self::get_selected_space()`], but only returns the space ID. pub fn get_selected_space_id(&self) -> Option { - self.borrow()?.selected_space.as_ref().map(|ss| ss.room_id().clone()) + self.borrow()? + .selected_space + .as_ref() + .map(|ss| ss.room_id().clone()) } /// Returns a clone of the space request sender channel, if available. diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 93b8d4a9d..292ebc23b 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -15,12 +15,33 @@ //! * A "cannot-send-message" notice, which is shown if the user cannot send messages to the room. //! - 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 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, +}; script_mod! { use mod.prelude.widgets.* @@ -161,14 +182,18 @@ script_mod! { /// Main component for message input with @mention support #[derive(Script, ScriptHook, Widget)] pub struct RoomInputBar { - #[source] source: ScriptObjectRef, - #[deref] view: View, + #[source] + source: ScriptObjectRef, + #[deref] + view: View, /// Whether the `ReplyingPreview` was visible when the `EditingPane` was shown. /// If true, when the `EditingPane` gets hidden, we need to re-show the `ReplyingPreview`. - #[rust] was_replying_preview_visible: bool, + #[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)>, + #[rust] + replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, } impl Widget for RoomInputBar { @@ -178,14 +203,21 @@ impl Widget for RoomInputBar { .get::() .expect("BUG: RoomScreenProps should be available in Scope::props for RoomInputBar"); - match event.hits(cx, self.view.view(cx, ids!(replying_preview.reply_preview_content)).area()) { + match event.hits( + cx, + self.view + .view(cx, ids!(replying_preview.reply_preview_content)) + .area(), + ) { // If the hit occurred on the replying message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { - if let Some(event_id) = self.replying_to.as_ref() + if let Some(event_id) = self + .replying_to + .as_ref() .and_then(|(event_tl_item, _)| event_tl_item.event_id().map(ToOwned::to_owned)) { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::JumpToEvent(event_id), ); } else { @@ -241,40 +273,56 @@ impl RoomInputBar { None, ); } - self.view.location_preview(cx, ids!(location_preview)).show(); + self.view + .location_preview(cx, ids!(location_preview)) + .show(); self.redraw(cx); } // Handle the send location button being clicked. - if self.button(cx, ids!(location_preview.send_location_button)).clicked(actions) { + if self + .button(cx, ids!(location_preview.send_location_button)) + .clicked(actions) + { let location_preview = self.location_preview(cx, ids!(location_preview)); if let Some((coords, _system_time_opt)) = location_preview.get_current_data() { - let geo_uri = format!("{}{},{}", utils::GEO_URI_SCHEME, coords.latitude, coords.longitude); - let message = RoomMessageEventContent::new( - MessageType::Location( - LocationMessageEventContent::new(geo_uri.clone(), geo_uri) - ) + let geo_uri = format!( + "{}{},{}", + utils::GEO_URI_SCHEME, + coords.latitude, + coords.longitude ); - let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } + let message = RoomMessageEventContent::new(MessageType::Location( + LocationMessageEventContent::new(geo_uri.clone(), geo_uri), + )); + let replied_to = self + .replying_to + .take() + .and_then(|(event_tl_item, _emb)| { + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } + }) }) - ).or_else(|| - room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| - Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - } - ) - ); + .or_else(|| { + room_screen_props.timeline_kind.thread_root_event_id().map( + |thread_root_event_id| Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + }, + ) + }); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -291,31 +339,53 @@ impl RoomInputBar { // Handle the send message button being clicked or Cmd/Ctrl + Return being pressed. if self.button(cx, ids!(send_message_button)).clicked(actions) - || text_input.returned(actions).is_some_and(|(_, m)| m.is_primary()) + || text_input + .returned(actions) + .is_some_and(|(_, m)| m.is_primary()) { let entered_text = mentionable_text_input.text().trim().to_string(); if !entered_text.is_empty() { + if self.try_handle_bot_shortcut(cx, &entered_text, room_screen_props) { + self.clear_replying_to(cx); + mentionable_text_input.set_text(cx, ""); + submit_async_request(MatrixRequest::SendTypingNotice { + room_id: room_screen_props.timeline_kind.room_id().clone(), + typing: false, + }); + self.enable_send_message_button(cx, false); + self.redraw(cx); + return; + } + let message = mentionable_text_input.create_message_with_mentions(&entered_text); - let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } + let replied_to = self + .replying_to + .take() + .and_then(|(event_tl_item, _emb)| { + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } + }) }) - ).or_else(|| - room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| - Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - } - ) - ); + .or_else(|| { + room_screen_props.timeline_kind.thread_root_event_id().map( + |thread_root_event_id| Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + }, + ) + }); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -349,18 +419,29 @@ impl RoomInputBar { if is_text_input_empty { if let Some(KeyEvent { key_code: KeyCode::ArrowUp, - modifiers: KeyModifiers { shift: false, control: false, alt: false, logo: false }, + modifiers: + KeyModifiers { + shift: false, + control: false, + alt: false, + logo: false, + }, .. - }) = text_input.key_down_unhandled(actions) { + }) = text_input.key_down_unhandled(actions) + { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::EditLatest, ); } } // If the EditingPane has been hidden, handle that. - if self.view.editing_pane(cx, ids!(editing_pane)).was_hidden(actions) { + if self + .view + .editing_pane(cx, ids!(editing_pane)) + .was_hidden(actions) + { self.on_editing_pane_hidden(cx); } } @@ -408,13 +489,15 @@ impl RoomInputBar { // 2. Hide other views that are irrelevant to a reply, e.g., // the `EditingPane` would improperly cover up the ReplyPreview. - self.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); + self.editing_pane(cx, ids!(editing_pane)) + .force_reset_hide(cx); self.on_editing_pane_hidden(cx); // 3. Automatically focus the keyboard on the message input box // so that the user can immediately start typing their reply // without having to manually click on the message input box. if grab_key_focus { - self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)).set_key_focus(cx); + self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + .set_key_focus(cx); } self.button(cx, ids!(cancel_reply_button)).reset_hover(cx); self.redraw(cx); @@ -444,7 +527,9 @@ impl RoomInputBar { let replying_preview = self.view.view(cx, ids!(replying_preview)); self.was_replying_preview_visible = replying_preview.visible(); replying_preview.set_visible(cx, false); - self.view.location_preview(cx, ids!(location_preview)).clear(); + self.view + .location_preview(cx, ids!(location_preview)) + .clear(); let editing_pane = self.view.editing_pane(cx, ids!(editing_pane)); match behavior { @@ -466,12 +551,14 @@ impl RoomInputBar { // Same goes for the replying_preview, if it was previously shown. self.view.view(cx, ids!(input_bar)).set_visible(cx, true); if self.was_replying_preview_visible && self.replying_to.is_some() { - self.view.view(cx, ids!(replying_preview)).set_visible(cx, true); + self.view + .view(cx, ids!(replying_preview)) + .set_visible(cx, true); } self.redraw(cx); // We don't need to do anything with the editing pane itself here, // because it has already been hidden by the time this function gets called. - } + } /// Updates (populates and shows or hides) this room's tombstone footer /// based on the given successor room details. @@ -489,7 +576,10 @@ impl RoomInputBar { input_bar.set_visible(cx, false); } else { tombstone_footer.hide(cx); - if !self.editing_pane(cx, ids!(editing_pane)).is_currently_shown(cx) { + if !self + .editing_pane(cx, ids!(editing_pane)) + .is_currently_shown(cx) + { input_bar.set_visible(cx, true); } } @@ -512,17 +602,69 @@ impl RoomInputBar { }); } + /// Intercepts `/bot` commands and opens the room-level app service actions UI instead + /// of sending the raw command text into the room. + fn try_handle_bot_shortcut( + &mut self, + cx: &mut Cx, + entered_text: &str, + room_screen_props: &RoomScreenProps, + ) -> bool { + if !(entered_text == "/bot" || entered_text.starts_with("/bot ")) { + return false; + } + + let popup_message = if room_screen_props + .timeline_kind + .thread_root_event_id() + .is_some() + { + Some(( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + )) + } else if entered_text != "/bot" { + Some(( + "Only `/bot` is supported right now. Use `/bot` and choose an action from the room panel.", + PopupKind::Info, + )) + } else if !room_screen_props.app_service_enabled { + Some(( + "Enable App Service in Settings before using /bot.", + PopupKind::Warning, + )) + } else if !room_screen_props.app_service_room_bound { + Some(( + "Bind BotFather to this room before using /bot.", + PopupKind::Warning, + )) + } else { + None + }; + + if let Some((message, kind)) = popup_message { + enqueue_popup_notification(message, kind, Some(4.0)); + } else { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + MessageAction::ToggleAppServiceActions, + ); + } + + true + } + /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - fn update_user_power_levels( - &mut self, - cx: &mut Cx, - user_power_levels: UserPowerLevels, - ) { + fn update_user_power_levels(&mut self, cx: &mut Cx, user_power_levels: UserPowerLevels) { let can_send = user_power_levels.can_send_message(); - self.view.view(cx, ids!(input_bar)).set_visible(cx, can_send); - self.view.view(cx, ids!(can_not_send_message_notice)).set_visible(cx, !can_send); + self.view + .view(cx, ids!(input_bar)) + .set_visible(cx, can_send); + self.view + .view(cx, ids!(can_not_send_message_notice)) + .set_visible(cx, !can_send); } /// Returns true if the TSP signing checkbox is checked, false otherwise. @@ -543,7 +685,9 @@ impl RoomInputBarRef { replying_to: (EventTimelineItem, EmbeddedEvent), timeline_kind: &TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_replying_to(cx, replying_to, timeline_kind, true); } @@ -554,7 +698,9 @@ impl RoomInputBarRef { event_tl_item: EventTimelineItem, timeline_kind: TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.show_editing_pane( cx, ShowEditingPaneBehavior::ShowNew { event_tl_item }, @@ -565,12 +711,10 @@ impl RoomInputBarRef { /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - pub fn update_user_power_levels( - &self, - cx: &mut Cx, - user_power_levels: UserPowerLevels, - ) { - let Some(mut inner) = self.borrow_mut() else { return }; + pub fn update_user_power_levels(&self, cx: &mut Cx, user_power_levels: UserPowerLevels) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.update_user_power_levels(cx, user_power_levels); } @@ -581,7 +725,9 @@ impl RoomInputBarRef { tombstoned_room_id: &OwnedRoomId, successor_room_details: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; inner.update_tombstone_footer(cx, tombstoned_room_id, successor_room_details); } @@ -593,22 +739,36 @@ impl RoomInputBarRef { timeline_event_item_id: TimelineEventItemId, edit_result: Result<(), matrix_sdk_ui::timeline::Error>, ) { - let Some(inner) = self.borrow_mut() else { return }; - inner.editing_pane(cx, ids!(editing_pane)) + let Some(inner) = self.borrow_mut() else { + return; + }; + inner + .editing_pane(cx, ids!(editing_pane)) .handle_edit_result(cx, timeline_event_item_id, edit_result); } /// Save a snapshot of the UI state of this `RoomInputBar`. pub fn save_state(&self) -> RoomInputBarState { - let Some(inner) = self.borrow() else { return Default::default() }; + let Some(inner) = self.borrow() else { + return Default::default(); + }; // Clear the location preview. We don't save this state because the // current location might change by the next time the user opens this same room. - inner.child_by_path(ids!(location_preview)).as_location_preview().clear(); + inner + .child_by_path(ids!(location_preview)) + .as_location_preview() + .clear(); RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), - editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), - text_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input.text_input)).as_text_input().save_state(), + editing_pane_state: inner + .child_by_path(ids!(editing_pane)) + .as_editing_pane() + .save_state(), + text_input_state: inner + .child_by_path(ids!(input_bar.mentionable_text_input.text_input)) + .as_text_input() + .save_state(), } } @@ -621,7 +781,9 @@ impl RoomInputBarRef { user_power_levels: UserPowerLevels, tombstone_info: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { return }; + let Some(mut inner) = self.borrow_mut() else { + return; + }; let RoomInputBarState { was_replying_preview_visible, text_input_state, @@ -637,7 +799,8 @@ impl RoomInputBarRef { inner.update_user_power_levels(cx, user_power_levels); // 1. Restore the state of the TextInput within the MentionableTextInput. - inner.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + inner + .text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) .restore_state(cx, text_input_state); // 2. Restore the state of the replying-to preview. @@ -656,7 +819,9 @@ impl RoomInputBarRef { timeline_kind.clone(), ); } else { - inner.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); + inner + .editing_pane(cx, ids!(editing_pane)) + .force_reset_hide(cx); inner.on_editing_pane_hidden(cx); } @@ -682,9 +847,7 @@ pub struct RoomInputBarState { /// Defines what to do when showing the `EditingPane` from the `RoomInputBar`. enum ShowEditingPaneBehavior { /// Show a new edit session, e.g., when first clicking "edit" on a message. - ShowNew { - event_tl_item: EventTimelineItem, - }, + ShowNew { event_tl_item: EventTimelineItem }, /// Restore the state of an `EditingPane` that already existed, e.g., when /// reopening a room that had an `EditingPane` open when it was closed. RestoreExisting { diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs new file mode 100644 index 000000000..c1fc6a837 --- /dev/null +++ b/src/settings/bot_settings.rs @@ -0,0 +1,187 @@ +use makepad_widgets::*; + +use crate::{ + app::{AppState, BotSettingsState}, + shared::popup_list::{PopupKind, enqueue_popup_notification}, +}; + +script_mod! { + use mod.prelude.widgets.* + use mod.widgets.* + + mod.widgets.BotSettingsInfoLabel = Label { + width: Fill + height: Fit + margin: Inset{left: 5, top: 2, bottom: 2} + draw_text +: { + wrap: Word + color: (MESSAGE_TEXT_COLOR) + text_style: REGULAR_TEXT { font_size: 10.5 } + } + text: "" + } + + mod.widgets.BotSettings = #(BotSettings::register_widget(vm)) { + width: Fill + height: Fit + flow: Down + spacing: 10 + + TitleLabel { + text: "App Service" + } + + description := mod.widgets.BotSettingsInfoLabel { + margin: Inset{left: 5, right: 8, bottom: 4} + text: "Enable Matrix app service support here. Robrix stays a normal Matrix client: it binds BotFather to a room and sends the matching slash commands." + } + + toggle_row := View { + width: Fill + height: Fit + flow: Right + align: Align{y: 0.5} + spacing: 12 + margin: Inset{left: 5, bottom: 2} + + enable_label := SubsectionLabel { + width: Fit + height: Fit + margin: 0 + text: "Enable App Service" + } + + toggle_button := RobrixNeutralIconButton { + width: Fit + height: Fit + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + draw_icon.svg: (ICON_HIERARCHY) + icon_walk: Walk{width: 16, height: 16} + text: "Enable App Service" + } + } + + bot_details := View { + visible: false + width: Fill + height: Fit + flow: Down + + SubsectionLabel { + text: "BotFather User ID:" + } + + bot_user_id_input := RobrixTextInput { + margin: Inset{top: 2, left: 5, right: 5, bottom: 8} + width: 280 + height: Fit + empty_text: "bot or @bot:server" + } + + buttons := View { + width: Fill + height: Fit + flow: Right + spacing: 10 + + save_button := RobrixPositiveIconButton { + width: Fit + height: Fit + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + margin: Inset{left: 5} + draw_icon.svg: (ICON_CHECKMARK) + icon_walk: Walk{width: 16, height: 16} + text: "Save" + } + } + } + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct BotSettings { + #[deref] + view: View, +} + +impl Widget for BotSettings { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + self.widget_match_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 WidgetMatchEvent for BotSettings { + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, _scope: &mut Scope) { + let toggle_button = self.view.button(cx, ids!(toggle_button)); + let bot_details = self.view.view(cx, ids!(bot_details)); + let bot_user_id_input = self.view.text_input(cx, ids!(bot_user_id_input)); + let save_button = self.view.button(cx, ids!(buttons.save_button)); + + let Some(app_state) = _scope.data.get_mut::() else { + return; + }; + + if toggle_button.clicked(actions) { + let enabled = !app_state.bot_settings.enabled; + app_state.bot_settings.enabled = enabled; + self.sync_ui(cx, &app_state.bot_settings); + bot_details.set_visible(cx, enabled); + self.view.redraw(cx); + } + + if save_button.clicked(actions) || bot_user_id_input.returned(actions).is_some() { + app_state.bot_settings.botfather_user_id = bot_user_id_input.text().trim().to_string(); + enqueue_popup_notification( + "Saved Matrix app service settings.", + PopupKind::Success, + Some(3.0), + ); + self.sync_ui(cx, &app_state.bot_settings); + } + } +} + +impl BotSettings { + fn sync_ui(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { + self.view + .view(cx, ids!(bot_details)) + .set_visible(cx, bot_settings.enabled); + self.view + .text_input(cx, ids!(bot_user_id_input)) + .set_text(cx, &bot_settings.botfather_user_id); + + let toggle_text = if bot_settings.enabled { + "Disable App Service" + } else { + "Enable App Service" + }; + self.view + .button(cx, ids!(toggle_button)) + .set_text(cx, toggle_text); + self.view.button(cx, ids!(toggle_button)).reset_hover(cx); + self.view + .button(cx, ids!(buttons.save_button)) + .reset_hover(cx); + self.view.redraw(cx); + } + + /// Populates the bot settings UI from the current persisted app state. + pub fn populate(&mut self, cx: &mut Cx, bot_settings: &BotSettingsState) { + self.sync_ui(cx, bot_settings); + } +} + +impl BotSettingsRef { + /// See [`BotSettings::populate()`]. + pub fn populate(&self, cx: &mut Cx, bot_settings: &BotSettingsState) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.populate(cx, bot_settings); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs index 579bf0849..3155e1186 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -2,8 +2,10 @@ use makepad_widgets::ScriptVm; pub mod settings_screen; pub mod account_settings; +pub mod bot_settings; pub fn script_mod(vm: &mut ScriptVm) { account_settings::script_mod(vm); + bot_settings::script_mod(vm); settings_screen::script_mod(vm); } diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 24baf849d..38246560c 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,7 +1,11 @@ - use makepad_widgets::*; -use crate::{home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, profile::user_profile::UserProfile, settings::account_settings::AccountSettingsWidgetExt}; +use crate::{ + app::BotSettingsState, + home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, + profile::user_profile::UserProfile, + settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}, +}; script_mod! { use mod.prelude.widgets.* @@ -58,6 +62,10 @@ script_mod! { LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + bot_settings := BotSettings {} + + LineH { width: 400, padding: 10, margin: Inset{top: 20, bottom: 5} } + // The TSP wallet settings section. tsp_settings_screen := TspSettingsScreen {} @@ -84,11 +92,11 @@ script_mod! { } } - /// The top-level widget showing all app and user settings/preferences. #[derive(Script, ScriptHook, Widget)] pub struct SettingsScreen { - #[deref] view: View, + #[deref] + view: View, } impl Widget for SettingsScreen { @@ -105,16 +113,15 @@ impl Widget for SettingsScreen { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) - ) - || event.back_pressed() - || match event.hits(cx, area) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(_fde) => { - cx.set_key_focus(area); - false + ) || event.back_pressed() + || match event.hits(cx, area) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false + } + _ => false, } - _ => false, - } }; if close_pane { cx.action(NavigationBarAction::CloseSettings); @@ -132,26 +139,30 @@ impl Widget for SettingsScreen { match action.downcast_ref() { Some(CreateWalletModalAction::Open) => { use crate::tsp::create_wallet_modal::CreateWalletModalWidgetExt; - self.view.create_wallet_modal(cx, ids!(create_wallet_modal_inner)).show(cx); + self.view + .create_wallet_modal(cx, ids!(create_wallet_modal_inner)) + .show(cx); self.view.modal(cx, ids!(create_wallet_modal)).open(cx); } Some(CreateWalletModalAction::Close) => { self.view.modal(cx, ids!(create_wallet_modal)).close(cx); } - None => { } + None => {} } // Handle the create DID modal being opened or closed. match action.downcast_ref() { Some(CreateDidModalAction::Open) => { use crate::tsp::create_did_modal::CreateDidModalWidgetExt; - self.view.create_did_modal(cx, ids!(create_did_modal_inner)).show(cx); + self.view + .create_did_modal(cx, ids!(create_did_modal_inner)) + .show(cx); self.view.modal(cx, ids!(create_did_modal)).open(cx); } Some(CreateDidModalAction::Close) => { self.view.modal(cx, ids!(create_did_modal)).close(cx); } - None => { } + None => {} } } } @@ -164,12 +175,22 @@ impl Widget for SettingsScreen { impl SettingsScreen { /// Fetches the current user's profile and uses it to populate the settings screen. - pub fn populate(&mut self, cx: &mut Cx, own_profile: Option) { + pub fn populate( + &mut self, + cx: &mut Cx, + own_profile: Option, + bot_settings: &BotSettingsState, + ) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { error!("Failed to get own profile for settings screen."); return; }; - self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); + self.view + .account_settings(cx, ids!(account_settings)) + .populate(cx, profile); + self.view + .bot_settings(cx, ids!(bot_settings)) + .populate(cx, bot_settings); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); @@ -178,8 +199,15 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. - pub fn populate(&self, cx: &mut Cx, own_profile: Option) { - let Some(mut inner) = self.borrow_mut() else { return; }; - inner.populate(cx, own_profile); + pub fn populate( + &self, + cx: &mut Cx, + own_profile: Option, + bot_settings: &BotSettingsState, + ) { + let Some(mut inner) = self.borrow_mut() else { + return; + }; + inner.populate(cx, own_profile, bot_settings); } } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index d50dd7842..1ffdf7de6 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,43 +8,110 @@ 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::{ - account::register::v3::Request as RegistrationRequest, - error::ErrorKind, - profile::{AvatarUrl, DisplayName}, - receipt::create_receipt::v3::ReceiptType, - uiaa::{AuthData, AuthType, Dummy}, - }}, events::{ + config::RequestConfig, + encryption::EncryptionSettings, + event_handler::EventHandlerDropGuard, + media::MediaRequestParameters, + room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, + ruma::{ + api::{ + Direction, + client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }, + }, + 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 + 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} + 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, + 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::{ + 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::{ + 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 + }, + 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)] @@ -92,7 +159,8 @@ impl From for Cli { Self { user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login.homeserver + homeserver: login + .homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -107,7 +175,8 @@ impl From for Cli { Self { user_id: registration.user_id.trim().to_owned(), password: registration.password, - homeserver: registration.homeserver + homeserver: registration + .homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -128,7 +197,8 @@ async fn finalize_authenticated_client( fallback_user_id: &str, ) -> Result<(Client, Option)> { if client.matrix_auth().logged_in() { - let logged_in_user_id = client.user_id() + let logged_in_user_id = client + .user_id() .map(ToString::to_string) .unwrap_or_else(|| fallback_user_id.to_owned()); log!("Logged in successfully."); @@ -145,7 +215,9 @@ async fn finalize_authenticated_client( "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); bail!(err_msg); } } @@ -161,7 +233,8 @@ fn registration_localpart(user_id: &str) -> Result { } let localpart = trimmed.trim_start_matches('@'); - if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) + { bail!("Please enter a valid username or full Matrix user ID."); } @@ -268,9 +341,14 @@ async fn reset_runtime_state_for_relogin() { 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() }); + Cx::post_action(LogoutAction::ClearAppState { + on_clear_appstate: on_clear_appstate.clone(), + }); - if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()) + .await + .is_err() + { warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); } } @@ -282,7 +360,6 @@ fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { ) } - /// Build a new client. async fn build_client( cli: &Cli, @@ -305,11 +382,13 @@ async fn build_client( }; let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); - let homeserver_url = cli.homeserver.as_deref() + let homeserver_url = cli + .homeserver + .as_deref() .filter(|homeserver| !homeserver.trim().is_empty()) .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); - // .unwrap_or("https://matrix.org/"); + // .unwrap_or("https://matrix.org/"); let mut builder = Client::builder() .server_name_or_homeserver_url(homeserver_url) @@ -337,13 +416,11 @@ async fn build_client( // 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)) - ); + 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(); + let homeserver_url = client.homeserver().to_string(); Ok(( client, ClientSessionPersisted { @@ -359,10 +436,7 @@ async fn build_client( /// 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)> { +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 { @@ -385,7 +459,9 @@ async fn login( if !client.matrix_auth().logged_in() { 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() }); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); bail!(err_msg); } finalize_authenticated_client(client, client_session, &cli.user_id).await @@ -441,7 +517,9 @@ async fn login( register_result.user_id, ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.clone(), + }); bail!(err_msg); } @@ -461,7 +539,6 @@ async fn login( } } - /// Which direction to paginate in. /// /// * `Forwards` will retrieve later events (towards the end of the timeline), @@ -530,7 +607,6 @@ pub type OnLinkPreviewFetchedFn = fn( Option>, ); - /// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. #[derive(Clone, Debug)] pub enum MatrixLinkAction { @@ -561,9 +637,7 @@ pub enum DirectMessageRoomAction { 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, - }, + DidNotExist { user_profile: UserProfile }, /// A direct message room didn't exist, but we successfully created a new one. NewlyCreated { user_profile: UserProfile, @@ -598,7 +672,10 @@ impl TimelineKind { 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), + TimelineKind::Thread { + thread_root_event_id, + .. + } => Some(thread_root_event_id), } } } @@ -606,7 +683,10 @@ 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 } => { + TimelineKind::Thread { + room_id, + thread_root_event_id, + } => { write!(f, "Thread({}, {})", room_id, thread_root_event_id) } } @@ -619,9 +699,7 @@ pub enum MatrixRequest { /// Request from the login screen to log in with the given credentials. Login(LoginRequest), /// Request to logout. - Logout { - is_desktop: bool, - }, + Logout { is_desktop: bool }, /// Request to paginate the older (or newer) events of a room or thread timeline. PaginateTimeline { timeline_kind: TimelineKind, @@ -653,9 +731,7 @@ pub enum MatrixRequest { /// /// 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, - }, + 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, @@ -673,14 +749,16 @@ pub enum MatrixRequest { room_id: OwnedRoomId, user_id: OwnedUserId, }, - /// Request to join the given room. - JoinRoom { + /// Request to bind or unbind the configured botfather for the given room. + SetRoomBotBinding { room_id: OwnedRoomId, + bound: bool, + bot_user_id: OwnedUserId, }, + /// Request to join the given room. + JoinRoom { room_id: OwnedRoomId }, /// Request to leave the given room. - LeaveRoom { - room_id: OwnedRoomId, - }, + 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. @@ -703,9 +781,7 @@ pub enum MatrixRequest { via: Vec, }, /// Request to fetch the full details (the room preview) of a tombstoned room. - GetSuccessorRoomDetails { - tombstoned_room_id: OwnedRoomId, - }, + 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 @@ -730,9 +806,7 @@ pub enum MatrixRequest { local_only: bool, }, /// Request to fetch the number of unread messages in the given room. - GetNumberUnreadMessages { - timeline_kind: TimelineKind, - }, + GetNumberUnreadMessages { timeline_kind: TimelineKind }, /// Request to set the unread flag for the given room. SetUnreadFlag { room_id: OwnedRoomId, @@ -817,15 +891,12 @@ pub enum MatrixRequest { /// 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, - }, + 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{ + SpawnSSOServer { brand: String, homeserver_url: String, identity_provider_id: String, @@ -870,9 +941,7 @@ pub enum MatrixRequest { /// /// 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, - }, + GetRoomPowerLevels { timeline_kind: TimelineKind }, /// Toggles the given reaction to the given event in the given room. ToggleReaction { timeline_kind: TimelineKind, @@ -898,7 +967,7 @@ pub enum MatrixRequest { /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. GetMatrixRoomLinkPillInfo { matrix_id: MatrixId, - via: Vec + via: Vec, }, /// Request to fetch URL preview from the Matrix homeserver. GetUrlPreview { @@ -912,19 +981,19 @@ pub enum MatrixRequest { /// 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) + 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{ +pub enum LoginRequest { LoginByPassword(LoginByPassword), Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), - } /// Information needed to log in to a Matrix homeserver. pub struct LoginByPassword { @@ -941,7 +1010,6 @@ pub struct RegisterAccount { 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 @@ -952,7 +1020,8 @@ async fn matrix_worker_task( ) -> 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(); + 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(); @@ -962,7 +1031,7 @@ async fn matrix_worker_task( 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." + "BUG: failed to send login request to login worker task.", ))); } } @@ -975,7 +1044,7 @@ async fn matrix_worker_task( match logout_with_state_machine(is_desktop).await { Ok(()) => { log!("Logout completed successfully via state machine"); - }, + } Err(e) => { error!("Logout failed: {e:?}"); } @@ -983,7 +1052,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::PaginateTimeline {timeline_kind, num_events, direction} => { + 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; @@ -1025,7 +1098,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::EditMessage { timeline_kind, timeline_event_item_id, edited_content } => { + 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; @@ -1047,7 +1124,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchDetailsForEvent { timeline_kind, event_id } => { + 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; @@ -1064,7 +1144,10 @@ async fn matrix_worker_task( // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); } } - if sender.send(TimelineUpdate::EventDetailsFetched { event_id, result }).is_err() { + 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(); @@ -1118,17 +1201,27 @@ async fn matrix_worker_task( }); } - MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { + 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}"); + 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) { + 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()); + let newly_pending = room_info + .pending_thread_timelines + .insert(thread_root_event_id.clone()); if !newly_pending { continue; } @@ -1200,11 +1293,18 @@ async fn matrix_worker_task( }); } - MatrixRequest::Knock { room_or_alias_id, reason, server_names } => { + 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 { + 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 { @@ -1228,23 +1328,21 @@ async fn matrix_worker_task( 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, - }), + Ok(_) => Cx::post_action(InviteResultAction::Sent { room_id, user_id }), Err(error) => Cx::post_action(InviteResultAction::Failed { room_id, user_id, error, }), } - } - else { + } 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()), + error: matrix_sdk::Error::UnknownError( + "Room/Space not found in client's known list.".into(), + ), }) } }); @@ -1265,8 +1363,7 @@ async fn matrix_worker_task( JoinRoomResultAction::Failed { room_id, error: e } } } - } - else { + } else { match client.join_room_by_id(&room_id).await { Ok(_room) => { log!("Successfully joined new unknown room {room_id}."); @@ -1301,14 +1398,20 @@ async fn matrix_worker_task( 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()), + 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 } => { + 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; @@ -1317,7 +1420,9 @@ async fn matrix_worker_task( 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(); + sender + .send(TimelineUpdate::RoomMembersListFetched { members }) + .unwrap(); SignalToUI::set_ui_signal(); }; @@ -1334,7 +1439,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetRoomPreview { room_or_alias_id, via } => { + 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; @@ -1342,12 +1450,80 @@ async fn matrix_worker_task( }); } + MatrixRequest::SetRoomBotBinding { + room_id, + bound, + bot_user_id, + } => { + let Some(client) = get_client() else { continue }; + let _bot_binding_task = Handle::current().spawn(async move { + let Some(room) = client.get_room(&room_id) else { + let error_message = + format!("Room {room_id} was not found for the bot binding request."); + error!("{error_message}"); + enqueue_popup_notification(error_message, PopupKind::Error, None); + return; + }; + + let membership_result = if bound { + room.invite_user_by_id(&bot_user_id).await + } else { + room.kick_user(&bot_user_id, Some("Robrix app service unbind")).await + }; + + match membership_result { + Ok(()) => { + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: None, + }); + } + Err(error) => { + let membership_exists = room + .get_member_no_sync(&bot_user_id) + .await + .ok() + .flatten() + .is_some(); + let should_mark_bound = if bound { membership_exists } else { false }; + + if should_mark_bound != bound { + error!( + "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", + if bound { "invite" } else { "remove" } + ); + enqueue_popup_notification( + format!( + "Failed to {} BotFather {bot_user_id}: {error}", + if bound { "invite" } else { "remove" } + ), + PopupKind::Error, + None, + ); + return; + } + + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: Some(error.to_string()), + }); + } + } + }); + } + 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"); + error!( + "BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request" + ); continue; }; ( @@ -1363,7 +1539,10 @@ async fn matrix_worker_task( ); } - MatrixRequest::OpenOrCreateDirectMessage { user_profile, allow_create } => { + 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) { @@ -1386,7 +1565,7 @@ async fn matrix_worker_task( 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 { @@ -1398,7 +1577,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUserProfile { user_id, room_id, local_only } => { + 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}, \ @@ -1492,7 +1675,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetUnreadFlag { room_id, mark_as_unread } => { + 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; @@ -1501,35 +1687,64 @@ async fn matrix_worker_task( 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), + Err(e) => error!( + "Failed to set unread flag to {} for room {}: {:?}", + mark_as_unread, room_id, e + ), } }); } - MatrixRequest::SetIsFavorite { room_id, is_favorite } => { + 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}"); + 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; + 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), + Err(e) => error!( + "Failed to set favorite to {} for room {}: {:?}", + is_favorite, room_id, e + ), } }); } - MatrixRequest::SetIsLowPriority { room_id, is_low_priority } => { + 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}"); + 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; + 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), + 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 + ), } }); } @@ -1538,15 +1753,24 @@ async fn matrix_worker_task( 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" }); + 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" }); + 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" }); + let err_msg = format!( + "Failed to {} avatar: {e}", + if is_removing { "remove" } else { "set" } + ); Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); } } @@ -1557,57 +1781,87 @@ async fn matrix_worker_task( 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{}...", + 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() + 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; + 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)); + 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" }); + 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 } => { + 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 + room.matrix_event_permalink(event_id) + .await .map(MatrixLinkAction::MatrixUri) } else { - room.matrix_permalink(join_on_click).await + 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 + room.matrix_to_event_permalink(event_id) + .await .map(MatrixLinkAction::MatrixToUri) } else { - room.matrix_to_permalink().await + 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"))); + Cx::post_action(MatrixLinkAction::Error(format!( + "Room {room_id} not found" + ))); } }); } - MatrixRequest::IgnoreUser { ignore, room_member, room_id } => { + 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(); @@ -1662,7 +1916,9 @@ async fn matrix_worker_task( 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}"); + log!( + "BUG: skipping send typing notice request for not-yet-known room {room_id}" + ); continue; }; let _typing_task = Handle::current().spawn(async move { @@ -1676,16 +1932,21 @@ async fn matrix_worker_task( 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}"); + 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."); + 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(); + let (drop_guard, receiver) = + main_timeline.room().subscribe_to_typing_notifications(); jrd.typing_notice_subscriber = Some(drop_guard); (main_timeline, receiver) } @@ -1694,7 +1955,11 @@ async fn matrix_worker_task( 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) + ( + main_timeline, + jrd.main_timeline.timeline_update_sender.clone(), + receiver, + ) }; let _typing_notices_task = Handle::current().spawn(async move { @@ -1721,15 +1986,22 @@ async fn matrix_worker_task( }); } - MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { + MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { + timeline_kind, + subscribe, + } => { if !subscribe { - if let Some(task_handler) = subscribers_own_user_read_receipts.remove(&timeline_kind) { + 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}"); + log!( + "BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}" + ); continue; }; @@ -1771,7 +2043,8 @@ async fn matrix_worker_task( } } }); - subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); + subscribers_own_user_read_receipts + .insert(timeline_kind_clone, subscribe_own_read_receipt_task); } MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { @@ -1781,9 +2054,13 @@ async fn matrix_worker_task( } continue; } - let kind = TimelineKind::MainRoom { room_id: room_id.clone() }; + 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}"); + log!( + "BUG: skipping subscribe to pinned events request for unknown room {room_id}" + ); continue; }; let subscribe_pinned_events_task = Handle::current().spawn(async move { @@ -1805,8 +2082,18 @@ async fn matrix_worker_task( 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::SpawnSSOServer { + brand, + homeserver_url, + identity_provider_id, + } => { + spawn_sso_server( + brand, + homeserver_url, + identity_provider_id, + login_sender.clone(), + ) + .await; } MatrixRequest::ResolveRoomAlias(room_alias) => { @@ -1819,7 +2106,10 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchAvatar { mxc_uri, on_fetched } => { + 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:?}..."); @@ -1829,13 +2119,21 @@ async fn matrix_worker_task( }; 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()) }); + on_fetched(AvatarUpdate { + mxc_uri, + avatar_data: res.map(|v| v.into()), + }); }); } - MatrixRequest::FetchMedia { media_request, on_fetched, destination, update_sender } => { + 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; @@ -1940,7 +2238,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::ReadReceipt { timeline_kind, event_id, receipt_type } => { + 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; @@ -1961,7 +2263,7 @@ async fn matrix_worker_task( }); } }); - }, + } MatrixRequest::GetRoomPowerLevels { timeline_kind } => { let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { @@ -1969,15 +2271,21 @@ async fn matrix_worker_task( continue; }; - let Some(user_id) = current_user_id() else { 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() { + 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(); @@ -1987,9 +2295,13 @@ async fn matrix_worker_task( } } }); - }, + } - MatrixRequest::ToggleReaction { timeline_kind, timeline_event_id, reaction } => { + 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; @@ -1997,17 +2309,26 @@ async fn matrix_worker_task( 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 { + 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:?}"), + } + Err(_e) => error!( + "Failed to send toggle reaction to {timeline_kind}; error: {_e:?}" + ), } }); - }, + } - MatrixRequest::RedactMessage { timeline_kind, timeline_event_id, reason } => { + 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; @@ -2026,9 +2347,13 @@ async fn matrix_worker_task( } } }); - }, + } - MatrixRequest::PinEvent { timeline_kind, event_id, pin } => { + 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; @@ -2040,7 +2365,11 @@ async fn matrix_worker_task( } else { timeline.unpin_event(&event_id).await }; - match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { + match sender.send(TimelineUpdate::PinResult { + event_id, + pin, + result, + }) { Ok(_) => SignalToUI::set_ui_signal(), Err(_) => log!("Failed to send UI update for pin event."), } @@ -2074,7 +2403,12 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUrlPreview { url, on_fetched, destination, update_sender } => { + 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 { @@ -2084,17 +2418,19 @@ async fn matrix_worker_task( // 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") + 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()) @@ -2107,20 +2443,20 @@ async fn matrix_worker_task( // 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]); @@ -2129,22 +2465,25 @@ async fn matrix_worker_task( // } // 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) - }); + 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{ + 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) => { @@ -2164,11 +2503,12 @@ async fn matrix_worker_task( // error!("Response body that failed to parse: {}", text); UrlPreviewError::Json(e) }) - }.await; + } + .await; // match &result { // Ok(preview_data) => { - // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", + // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", // url, preview_data.title, preview_data.site_name); // } // Err(e) => { @@ -2187,7 +2527,6 @@ async fn matrix_worker_task( 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); @@ -2200,7 +2539,8 @@ static REQUEST_SENDER: Mutex>> = Mutex::ne 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())); +static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = + LazyLock::new(|| Arc::new(Notify::new())); /// Blocks the current thread until the given future completes. /// @@ -2211,36 +2551,45 @@ 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(); + 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 - }) + 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(); + 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() + DEFAULT_SSO_CLIENT + .lock() + .unwrap() .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2257,7 +2606,6 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2324,13 +2672,13 @@ impl Drop for JoinedRoomDetails { } } - /// 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())); +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>( @@ -2340,7 +2688,10 @@ fn get_per_timeline_details<'a>( 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), + TimelineKind::Thread { + thread_root_event_id, + .. + } => room_info.thread_timelines.get_mut(thread_root_event_id), } } @@ -2351,14 +2702,22 @@ fn get_timeline(kind: &TimelineKind) -> Option> { } /// 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())) +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() + ALL_JOINED_ROOMS + .lock() + .unwrap() .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2372,15 +2731,16 @@ pub fn get_client() -> Option { /// 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()) - ) + 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() @@ -2389,7 +2749,8 @@ pub fn get_sync_service() -> Option> { /// 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())); +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 { @@ -2401,7 +2762,6 @@ 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. @@ -2415,7 +2775,10 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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)?, + 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 { @@ -2428,25 +2791,18 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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() - }) +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 @@ -2474,18 +2830,14 @@ struct RoomListServiceRoomInfo { 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 { + 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(), @@ -2527,26 +2879,26 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() + 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? {}", + 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") - ); + 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: Option<(Client, Option, bool)> = 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: {:?}", + 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.clone()).await { @@ -2563,7 +2915,10 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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); + 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(), @@ -2572,9 +2927,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok((client, sync_token)) => Some((client, sync_token, false)), 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}") - )); + 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:?}"), }); @@ -2599,34 +2954,30 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let (client, sync_service, logged_in_user_id) = 'login_loop: loop { let (client, _sync_token, validate_session) = 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, false), - 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())); + 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, false), + Err(e) => { + error!("Login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err, + status: format!("Login failed: {e}"), }); - return; } + }, + 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; } } - } + }, }; if validate_session { @@ -2634,7 +2985,8 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok(_) => {} Err(e) if is_invalid_token_http_error(&e) => { clear_persisted_session(client.user_id()).await; - let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; + let err_msg = + "Your login token is no longer valid.\n\nPlease log in again."; Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.to_string(), @@ -2642,7 +2994,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { continue 'login_loop; } Err(e) => { - warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); + warning!( + "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" + ); } } } @@ -2652,7 +3006,8 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let _ = client_opt.take(); } - let logged_in_user_id: OwnedUserId = client.user_id() + 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); @@ -2660,7 +3015,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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."); + error!( + "BUG: unexpectedly replaced an existing client when initializing the matrix client." + ); } // Listen for changes to our verification status and incoming verification requests. @@ -2684,7 +3041,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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}.") + format!( + "Please restart Robrix.\n\nFailed to create Matrix sync service: {e}." + ) }; if is_invalid_token_error(&e) { clear_persisted_session(client.user_id()).await; @@ -2722,7 +3081,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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."); + 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)); @@ -2839,7 +3200,6 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } - /// 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?; @@ -2853,13 +3213,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()), - ]) - )); + 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(); @@ -2875,7 +3235,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()); } + 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); @@ -2886,20 +3252,35 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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); } + 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); + 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; + })) + .await; // Send room order update with the new room IDs let (room_id_refs, room_ids) = { @@ -2913,43 +3294,57 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu }; if !room_ids.is_empty() { enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Append { values: room_ids } + 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"); } + 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; + 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 } + 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; + 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 } + 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 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)); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PopFront, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -2957,13 +3352,18 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .await?; } } remove_diff @ VectorDiff::PopBack => { - if LOG_ROOM_LIST_DIFFS { log!("room_list: diff 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)); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::PopBack, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -2971,38 +3371,61 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .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; + 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 } - )); + 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; + 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() } - )); + 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 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 })); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( + VecDiff::Remove { index }, + )); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3010,13 +3433,19 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ).await?; + ) + .await?; } else { - error!("BUG: room_list: diff Remove index {index} out of bounds, len {}", all_known_rooms.len()); + 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}"); } + 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() { @@ -3025,7 +3454,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } all_known_rooms.truncate(length); // sanity check enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Truncate { length } + VecDiff::Truncate { length }, )); } } @@ -3035,7 +3464,6 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 @@ -3055,48 +3483,58 @@ async fn optimize_remove_then_add_into_update( ) -> 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() => - { + 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); + 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; + 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() } - )); + 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() => - { + 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); + 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; + 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() } - )); + 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() => - { + 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); + 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; + 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() } - )); + 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; } @@ -3110,7 +3548,6 @@ async fn optimize_remove_then_add_into_update( Ok(()) } - /// Invoked when the room list service has received an update that changes an existing room. async fn update_room( old_room: &RoomListServiceRoomInfo, @@ -3121,18 +3558,29 @@ async fn update_room( 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); + 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); + 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); + 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 @@ -3142,11 +3590,17 @@ async fn update_room( return Ok(()); } RoomState::Joined => { - log!("update_room(): adding new Joined room: {:?} ({new_room_id})", new_room.display_name); + 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); + 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 => { @@ -3164,7 +3618,12 @@ async fn update_room( 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); + 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(), @@ -3174,12 +3633,15 @@ async fn update_room( // 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) { + 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()) { + 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, @@ -3188,9 +3650,13 @@ async fn update_room( 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); + 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(), @@ -3201,11 +3667,15 @@ async fn update_room( || 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 {} --> {}", + 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, + 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(), @@ -3216,7 +3686,8 @@ async fn update_room( } if old_room.is_direct != new_room.is_direct { - log!("Updating room {} is_direct from {} to {}", + log!( + "Updating room {} is_direct from {} to {}", new_room_id, old_room.is_direct, new_room.is_direct, @@ -3231,7 +3702,8 @@ async fn update_room( 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 = + Some(jrd.main_timeline.timeline_update_sender.clone()); } } __timeline_update_sender_opt.clone() @@ -3240,7 +3712,9 @@ async fn update_room( 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() }); + 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(), @@ -3249,7 +3723,9 @@ async fn update_room( timeline_update_sender, ); } else { - error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); + error!( + "BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}" + ); } } @@ -3260,37 +3736,38 @@ async fn update_room( 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}"), + 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."); + 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, + } 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, - } - ); + 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, @@ -3299,26 +3776,39 @@ async fn add_new_room( ) -> Result<()> { match new_room.state { RoomState::Knocked => { - log!("Got new Knocked room: {:?} ({})", new_room.display_name, new_room.room_id); + 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); + 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); + 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())); + 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) { @@ -3335,18 +3825,20 @@ async fn add_new_room( } 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, - })); + 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, @@ -3354,17 +3846,21 @@ async fn add_new_room( spawn_fetch_room_avatar(new_room); return Ok(()); } - RoomState::Joined => { } // Fall through to adding the joined room below. + 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; + room_list_service + .subscribe_to_rooms(&[&new_room.room_id]) + .await; } let timeline = Arc::new( - new_room.room.timeline_builder() + new_room + .room + .timeline_builder() .with_focus(TimelineFocus::Live { // we show threads as separate timelines in their own RoomScreen hide_threaded_events: true, @@ -3372,7 +3868,12 @@ async fn add_new_room( .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))?, + .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(); @@ -3388,7 +3889,11 @@ async fn add_new_room( // 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); + 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 { @@ -3409,7 +3914,8 @@ async fn add_new_room( let latest = get_latest_event_details( &new_room.room.latest_event().await, room_list_service.client(), - ).await; + ) + .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()); @@ -3440,7 +3946,8 @@ async fn add_new_room( #[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() + let ignored_users = client + .account() .account_data::() .await .ok()?? @@ -3504,7 +4011,9 @@ fn handle_load_app_state(user_id: OwnedUserId) { && !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)); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState( + app_state, + )); } } Err(_e) => { @@ -3523,12 +4032,12 @@ fn handle_load_app_state(user_id: OwnedUserId) { 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, + 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!( @@ -3610,14 +4119,12 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { 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 - ); - + 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); + 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 { @@ -3630,7 +4137,10 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { } fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { - log!("Initial room list loading state is {:?}", loading_state.get()); + 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:?}"); @@ -3638,8 +4148,12 @@ fn handle_room_list_service_loading_state(mut loading_state: Subscriber { enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); } - RoomListLoadingState::Loaded { maximum_number_of_rooms } => { - enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { max_rooms: maximum_number_of_rooms }); + 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; @@ -3662,12 +4176,12 @@ fn spawn_fetch_successor_room_preview( 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 }, + 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 }) @@ -3701,12 +4215,18 @@ async fn fetch_room_preview_with_avatar( }; 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); + 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 + 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()) } @@ -3726,7 +4246,10 @@ async fn fetch_room_preview_with_avatar( async fn fetch_thread_summary_details( room: &Room, thread_root_event_id: &EventId, -) -> (u32, Option) { +) -> ( + u32, + Option, +) { let mut num_replies = 0; let mut latest_reply_event = None; @@ -3784,10 +4307,7 @@ async fn fetch_latest_thread_reply_event( } /// 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 { +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; @@ -3800,7 +4320,10 @@ async fn count_thread_replies( ..Default::default() }; - let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; + let relations = room + .relations(thread_root_event_id.to_owned(), options) + .await + .ok()?; if relations.chunk.is_empty() { break; } @@ -3826,7 +4349,8 @@ async fn text_preview_of_latest_thread_reply( Ok(Some(rm)) => Some(rm), _ => room.get_member(&sender_id).await.ok().flatten(), }; - let sender_name = sender_room_member.as_ref() + 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(|| { @@ -3843,7 +4367,6 @@ async fn text_preview_of_latest_thread_reply( } } - /// 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 @@ -3867,29 +4390,37 @@ async fn get_latest_event_details( match latest_event_value { LatestEventValue::None => None, - LatestEventValue::Remote { timestamp, sender, is_own, profile, content } => { + 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); + 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: _ } => { + 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); + 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. @@ -3897,10 +4428,9 @@ async fn get_latest_event_details( /// 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 { + 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, @@ -3937,7 +4467,6 @@ async fn timeline_subscriber_handler( 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, @@ -3946,14 +4475,13 @@ async fn timeline_subscriber_handler( 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 + 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)) @@ -3962,11 +4490,13 @@ async fn timeline_subscriber_handler( } } - 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()); + 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(), @@ -3979,262 +4509,266 @@ async fn timeline_subscriber_handler( // 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()); + 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)) + ); - // 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}."); + target_event_id = new_request_details.as_ref().map(|(ev, ..)| ev.clone()); - // 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(), + // 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, } - }, - num_events: 50, - direction: PaginationDirection::Backwards, - }); + ).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)); + // 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; } - - 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. + 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(); } - // 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 { + 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; - } else { - index_of_first_change = min(index_of_first_change, index); - index_of_last_change = usize::MAX; + timeline_items.push_front(value); } - if index >= timeline_items.len() { + 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; } - - 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; + 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. } - } else { - found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) - .map(|(i, ev)| (i + index, ev)); + // This doesn't affect whether we should reobtain the latest event. } - - 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)); + 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}"); } } - 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); + 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}"); } } - 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; + 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; } - 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; - } + 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; + 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:?}!") - ); - } + 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(); + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); + } } - } - else => { - break; + else => { + break; + } } - } } + } - error!("Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}."); + 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. @@ -4259,8 +4793,13 @@ async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvata _ => { 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 { + 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()); } } @@ -4289,7 +4828,8 @@ async fn spawn_sso_server( // 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(), + status: "Please wait while Matrix builds and configures the client object for login." + .into(), }); // Wait for the notification that the client has been built @@ -4310,19 +4850,21 @@ async fn spawn_sso_server( // 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() + 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/") - ) { + && 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 { + ) + .await + { Ok(success) => client_and_session = Some(success), Err(e) => build_client_error = Some(e), } @@ -4331,10 +4873,12 @@ async fn spawn_sso_server( 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}") + 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. @@ -4346,7 +4890,8 @@ async fn spawn_sso_server( 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(), + status: "Please finish logging in using your browser, and then come back to Robrix." + .into(), }); match client .matrix_auth() @@ -4356,12 +4901,15 @@ async fn spawn_sso_server( if key == "redirectUrl" { let redirect_url = Url::parse(&value)?; Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); - break + break; } } - Uri::new(&sso_url).open().map_err(|err| - Error::Io(io::Error::other(format!("Unable to open SSO login url. Error: {:?}", err))) - ) + 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}")) @@ -4376,10 +4924,13 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session)).await { + 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." + "BUG: failed to send login request to matrix worker thread.", ))); } enqueue_rooms_list_update(RoomsListUpdate::Status { @@ -4405,7 +4956,6 @@ async fn spawn_sso_server( }); } - bitflags! { /// The powers that a user has in a given room. #[derive(Copy, Clone, PartialEq, Eq)] @@ -4483,14 +5033,38 @@ impl UserPowerLevels { 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.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 } @@ -4536,8 +5110,7 @@ impl UserPowerLevels { } pub fn can_send_message(self) -> bool { - self.contains(UserPowerLevels::RoomMessage) - || self.contains(UserPowerLevels::Message) + self.contains(UserPowerLevels::RoomMessage) || self.contains(UserPowerLevels::Message) } pub fn can_send_reaction(self) -> bool { @@ -4554,7 +5127,6 @@ impl UserPowerLevels { } } - /// 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() { @@ -4572,9 +5144,16 @@ pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { 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 { + 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(()) From 6a01687f14c52e481df3b05ed0a26f1c5b4607f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 01:16:55 +0800 Subject: [PATCH 028/283] Finish app service and botfather --- src/app.rs | 442 ++----- src/home/home_screen.rs | 633 +++++----- src/home/room_context_menu.rs | 98 +- src/home/room_screen.rs | 1860 +++++++++++---------------- src/home/rooms_list.rs | 682 ++++------ src/room/room_input_bar.rs | 296 ++--- src/settings/settings_screen.rs | 46 +- src/sliding_sync.rs | 2105 ++++++++++++------------------- 8 files changed, 2342 insertions(+), 3820 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0ed4de033..2897cffc8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,47 +4,17 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{ - RoomState, - ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}, -}; +use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}}; 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, - }, - 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::{VerificationModalAction, VerificationModalWidgetRefExt}, + 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 + }, 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::{ + VerificationModalAction, + VerificationModalWidgetRefExt, + } }; script_mod! { @@ -81,7 +51,7 @@ script_mod! { close +: { draw_bg +: {color: #0, color_hover: #E81123, color_down: #FF0015} } } } - + body +: { padding: 0, @@ -110,7 +80,7 @@ script_mod! { image_viewer_modal_inner := ImageViewer {} } } - + // Context menus should be shown in front of other UI elements, // but behind verification modals. new_message_context_menu := NewMessageContextMenu { } @@ -194,20 +164,16 @@ app_main!(App); #[derive(Script)] pub struct App { - #[live] - ui: WidgetRef, + #[live] ui: WidgetRef, /// The top-level app state, shared across various parts of the app. - #[rust] - app_state: AppState, + #[rust] app_state: AppState, /// The details of a room we're waiting on to be loaded so that we can navigate to it. /// 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)>, + #[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, + #[rust] mobile_room_nav_stack: Vec, } impl ScriptHook for App { @@ -232,27 +198,15 @@ impl MatchEvent for App { let _ = tracing_subscriber::fmt::try_init(); // Override Makepad's new default-JSON logger. We just want regular formatting. - fn regular_log( - file_name: &str, - line_start: u32, - column_start: u32, - _line_end: u32, - _column_end: u32, - message: String, - level: LogLevel, - ) { + fn regular_log(file_name: &str, line_start: u32, column_start: u32, _line_end: u32, _column_end: u32, message: String, level: LogLevel) { let l = match level { - LogLevel::Panic => "[!]", - LogLevel::Error => "[E]", + LogLevel::Panic => "[!]", + LogLevel::Error => "[E]", LogLevel::Warning => "[W]", - LogLevel::Log => "[I]", - LogLevel::Wait => "[.]", + LogLevel::Log => "[I]", + LogLevel::Wait => "[.]", }; - println!( - "{l} {file_name}:{}:{}: {message}", - line_start + 1, - column_start + 1 - ); + println!("{l} {file_name}:{}:{}: {message}", line_start + 1, column_start + 1); } *LOG_WITH_LEVEL.write().unwrap() = regular_log; @@ -267,10 +221,7 @@ impl MatchEvent for App { // Hide the caption bar on macOS and Linux, which use native window chrome. // On Windows (with custom chrome), the caption bar is needed. - if matches!( - cx.os_type(), - OsType::Macos | OsType::LinuxWindow(_) | OsType::LinuxDirect - ) { + if matches!(cx.os_type(), OsType::Macos | OsType::LinuxWindow(_) | OsType::LinuxDirect) { let mut window = self.ui.window(cx, ids!(main_window)); script_apply_eval!(cx, window, { show_caption_bar: false @@ -282,52 +233,41 @@ impl MatchEvent for App { log!("App::Startup: starting matrix sdk loop"); let _tokio_rt_handle = crate::sliding_sync::start_matrix_tokio().unwrap(); - #[cfg(feature = "tsp")] - { + #[cfg(feature = "tsp")] { log!("App::Startup: initializing TSP (Trust Spanning Protocol) module."); crate::tsp::tsp_init(_tokio_rt_handle).unwrap(); } } fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - let invite_confirmation_modal_inner = self - .ui - .confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); + let invite_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); if let Some(_accepted) = invite_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(invite_confirmation_modal)).close(cx); } - let delete_confirmation_modal_inner = self - .ui - .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); + let delete_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)); if let Some(_accepted) = delete_confirmation_modal_inner.closed(actions) { self.ui.modal(cx, ids!(delete_confirmation_modal)).close(cx); } - let positive_confirmation_modal_inner = self - .ui - .confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); + let positive_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(positive_confirmation_modal_inner)); if let Some(_accepted) = positive_confirmation_modal_inner.closed(actions) { - self.ui - .modal(cx, ids!(positive_confirmation_modal)) - .close(cx); + self.ui.modal(cx, ids!(positive_confirmation_modal)).close(cx); } for action in actions { match action.downcast_ref() { Some(LogoutConfirmModalAction::Open) => { - self.ui - .logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)) - .reset_state(cx); + self.ui.logout_confirm_modal(cx, ids!(logout_confirm_modal_inner)).reset_state(cx); self.ui.modal(cx, ids!(logout_confirm_modal)).open(cx); continue; - } + }, Some(LogoutConfirmModalAction::Close { was_internal, .. }) => { if *was_internal { self.ui.modal(cx, ids!(logout_confirm_modal)).close(cx); } continue; - } + }, _ => {} } @@ -339,8 +279,8 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } - Some(LogoutAction::ClearAppState { on_clear_appstate }) => { - // Clear user profile cache, invited_rooms timeline states + Some(LogoutAction::ClearAppState { on_clear_appstate }) => { + // Clear user profile cache, invited_rooms timeline states clear_all_app_state(cx); // Reset all app state to its default. self.app_state = Default::default(); @@ -363,9 +303,7 @@ impl MatchEvent for App { // When not yet logged in, the login_screen widget handles displaying the failure modal. if let Some(LoginAction::LoginFailure(_)) = action.downcast_ref() { if self.app_state.logged_in { - log!( - "Received LoginAction::LoginFailure while logged in; showing login screen." - ); + log!("Received LoginAction::LoginFailure while logged in; showing login screen."); self.app_state.logged_in = false; self.update_login_visibility(cx); self.ui.redraw(cx); @@ -374,13 +312,9 @@ impl MatchEvent for App { } // Handle an action requesting to open the new message context menu. - if let MessageAction::OpenMessageContextMenu { details, abs_pos } = - action.as_widget_action().cast() - { + if let MessageAction::OpenMessageContextMenu { details, abs_pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - let new_message_context_menu = self - .ui - .new_message_context_menu(cx, ids!(new_message_context_menu)); + let new_message_context_menu = self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)); let expected_dimensions = new_message_context_menu.show(cx, details); // Ensure the context menu does not spill over the window's bounds. let rect = self.ui.window(cx, ids!(main_window)).area().rect(cx); @@ -401,9 +335,7 @@ impl MatchEvent for App { } // Handle an action requesting to open the room context menu. - if let RoomsListAction::OpenRoomContextMenu { details, pos } = - action.as_widget_action().cast() - { + if let RoomsListAction::OpenRoomContextMenu { details, pos } = action.as_widget_action().cast() { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); let room_context_menu = self.ui.room_context_menu(cx, ids!(room_context_menu)); let expected_dimensions = room_context_menu.show(cx, details); @@ -437,9 +369,7 @@ impl MatchEvent for App { // 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(), - )); + cx.action(AppStateAction::UpgradedInviteToJoinedRoom(room_name_id.room_id().clone())); continue; } _ => {} @@ -489,9 +419,7 @@ impl MatchEvent for App { bot_user_id, warning, }) => { - self.app_state - .bot_settings - .set_room_bound(room_id.clone(), *bound); + self.app_state.bot_settings.set_room_bound(room_id.clone(), *bound); let kind = if warning.is_some() { PopupKind::Warning } else { @@ -499,33 +427,25 @@ impl MatchEvent for App { }; let message = match (*bound, bot_user_id.as_ref(), warning.as_deref()) { (true, Some(bot_user_id), Some(warning)) => { - format!( - "BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}" - ) + format!("BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}") } (true, Some(bot_user_id), None) => { format!("Bound room {room_id} to BotFather {bot_user_id}.") } (false, Some(bot_user_id), Some(warning)) => { - format!( - "Unbound BotFather {bot_user_id} from room {room_id}, with warning: {warning}" - ) + format!("Unbound BotFather {bot_user_id} from room {room_id}, with warning: {warning}") } (false, Some(bot_user_id), None) => { format!("Unbound BotFather {bot_user_id} from room {room_id}.") } (false, None, Some(warning)) => { - format!( - "Unbound room {room_id} from BotFather, with warning: {warning}" - ) + format!("Unbound room {room_id} from BotFather, with warning: {warning}") } (false, None, None) => { format!("Unbound room {room_id} from BotFather.") } (true, None, Some(warning)) => { - format!( - "BotFather is available for room {room_id}, with warning: {warning}" - ) + format!("BotFather is available for room {room_id}, with warning: {warning}") } (true, None, None) => { format!("Bound room {room_id} to BotFather.") @@ -535,25 +455,18 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } - Some(AppStateAction::NavigateToRoom { - room_to_close, - destination_room, - }) => { + Some(AppStateAction::NavigateToRoom { room_to_close, destination_room }) => { self.navigate_to_room(cx, room_to_close.as_ref(), destination_room); continue; } // If we successfully loaded a room that we were waiting on, // we can now navigate to it and optionally close a previous room. - Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) - if self - .waiting_to_navigate_to_room - .as_ref() + Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) if + self.waiting_to_navigate_to_room.as_ref() .is_some_and(|(dr, _)| dr.room_id() == room_name_id.room_id()) => { log!("Loaded awaited room {room_name_id:?}, navigating to it now..."); - if let Some((dest_room, room_to_close)) = - self.waiting_to_navigate_to_room.take() - { + if let Some((dest_room, room_to_close)) = self.waiting_to_navigate_to_room.take() { self.navigate_to_room(cx, room_to_close.as_ref(), &dest_room); } continue; @@ -563,22 +476,18 @@ impl MatchEvent for App { // Handle actions for showing or hiding the tooltip. match action.as_widget_action().cast() { - TooltipAction::HoverIn { - text, - widget_rect, - options, - } => { + TooltipAction::HoverIn { text, widget_rect, options } => { // Don't show any tooltips if the message context menu is currently shown. - if self - .ui - .new_message_context_menu(cx, ids!(new_message_context_menu)) - .is_currently_shown(cx) - { + if self.ui.new_message_context_menu(cx, ids!(new_message_context_menu)).is_currently_shown(cx) { self.ui.callout_tooltip(cx, ids!(app_tooltip)).hide(cx); - } else { - self.ui - .callout_tooltip(cx, ids!(app_tooltip)) - .show_with_options(cx, &text, widget_rect, options); + } + else { + self.ui.callout_tooltip(cx, ids!(app_tooltip)).show_with_options( + cx, + &text, + widget_rect, + options, + ); } continue; } @@ -612,8 +521,7 @@ impl MatchEvent for App { // // Note: other verification actions are handled by the verification modal itself. if let Some(VerificationAction::RequestReceived(state)) = action.downcast_ref() { - self.ui - .verification_modal(cx, ids!(verification_modal_inner)) + self.ui.verification_modal(cx, ids!(verification_modal_inner)) .initialize_with_data(cx, state.clone()); self.ui.modal(cx, ids!(verification_modal)).open(cx); continue; @@ -634,23 +542,12 @@ impl MatchEvent for App { _ => {} } // Handle actions to open/close the TSP verification modal. - #[cfg(feature = "tsp")] - { + #[cfg(feature = "tsp")] { use std::ops::Deref; - use crate::tsp::{ - tsp_verification_modal::{ - TspVerificationModalAction, TspVerificationModalWidgetRefExt, - }, - TspIdentityAction, - }; + use crate::tsp::{tsp_verification_modal::{TspVerificationModalAction, TspVerificationModalWidgetRefExt}, TspIdentityAction}; - if let Some(TspIdentityAction::ReceivedDidAssociationRequest { - details, - wallet_db, - }) = action.downcast_ref() - { - self.ui - .tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) + if let Some(TspIdentityAction::ReceivedDidAssociationRequest { details, wallet_db }) = action.downcast_ref() { + self.ui.tsp_verification_modal(cx, ids!(tsp_verification_modal_inner)) .initialize_with_details(cx, details.clone(), wallet_db.deref().clone()); self.ui.modal(cx, ids!(tsp_verification_modal)).open(cx); continue; @@ -662,9 +559,7 @@ impl MatchEvent for App { } // Handle a request to show the invite confirmation modal. - if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = - action.downcast_ref() - { + if let Some(InviteAction::ShowInviteConfirmationModal(content_opt)) = action.downcast_ref() { if let Some(content) = content_opt.borrow_mut().take() { invite_confirmation_modal_inner.show(cx, content); self.ui.modal(cx, ids!(invite_confirmation_modal)).open(cx); @@ -673,13 +568,10 @@ impl MatchEvent for App { } // Handle a request to show the generic positive confirmation modal. - if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() - { + if let Some(PositiveConfirmationModalAction::Show(content_opt)) = action.downcast_ref() { if let Some(content) = content_opt.borrow_mut().take() { positive_confirmation_modal_inner.show(cx, content); - self.ui - .modal(cx, ids!(positive_confirmation_modal)) - .open(cx); + self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); } continue; } @@ -687,9 +579,7 @@ impl MatchEvent for App { // Handle a request to show the delete confirmation modal. if let Some(ConfirmDeleteAction::Show(content_opt)) = action.downcast_ref() { if let Some(content) = content_opt.borrow_mut().take() { - self.ui - .confirmation_modal(cx, ids!(delete_confirmation_modal_inner)) - .show(cx, content); + self.ui.confirmation_modal(cx, ids!(delete_confirmation_modal_inner)).show(cx, content); self.ui.modal(cx, ids!(delete_confirmation_modal)).open(cx); } continue; @@ -698,10 +588,8 @@ impl MatchEvent for App { // Handle InviteModalAction to open/close the invite modal. match action.downcast_ref() { Some(InviteModalAction::Open(room_name_id)) => { - self.ui - .invite_modal(cx, ids!(invite_modal_inner)) - .show(cx, room_name_id.clone()); - self.ui.modal(cx, ids!(invite_modal)).open(cx); + self.ui.invite_modal(cx, ids!(invite_modal_inner)).show(cx, room_name_id.clone()); + self.ui.modal(cx, ids!(invite_modal)).open(cx); continue; } Some(InviteModalAction::Close) => { @@ -713,13 +601,8 @@ impl MatchEvent for App { // Handle EventSourceModalAction to open/close the event source modal. match action.downcast_ref() { - Some(EventSourceModalAction::Open { - room_id, - event_id, - original_json, - }) => { - self.ui - .event_source_modal(cx, ids!(event_source_modal_inner)) + Some(EventSourceModalAction::Open { room_id, event_id, original_json }) => { + self.ui.event_source_modal(cx, ids!(event_source_modal_inner)) .show(cx, room_id.clone(), event_id.clone(), original_json.clone()); self.ui.modal(cx, ids!(event_source_modal)).open(cx); continue; @@ -734,11 +617,7 @@ impl MatchEvent for App { // Handle DirectMessageRoomActions match action.downcast_ref() { Some(DirectMessageRoomAction::FoundExisting { room_name_id, .. }) => { - self.navigate_to_room( - cx, - None, - &BasicRoomDetails::RoomId(room_name_id.clone()), - ); + self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); } Some(DirectMessageRoomAction::DidNotExist { user_profile }) => { let user_profile = user_profile.clone(); @@ -746,7 +625,8 @@ impl MatchEvent for App { Some(un) if !un.is_empty() => format!( "You don't have an existing direct message room with {} ({}).\n\n\ Would you like to create one now?", - un, user_profile.user_id, + un, + user_profile.user_id, ), _ => format!( "You don't have an existing direct message room with {}.\n\n\ @@ -774,29 +654,17 @@ impl MatchEvent for App { ..Default::default() }, ); - self.ui - .modal(cx, ids!(positive_confirmation_modal)) - .open(cx); + self.ui.modal(cx, ids!(positive_confirmation_modal)).open(cx); } - Some(DirectMessageRoomAction::FailedToCreate { - user_profile, - error, - }) => { + Some(DirectMessageRoomAction::FailedToCreate { user_profile, error }) => { enqueue_popup_notification( - format!( - "Failed to create a new DM room with {}.\n\nError: {error}", - user_profile.displayable_name() - ), + format!("Failed to create a new DM room with {}.\n\nError: {error}", user_profile.displayable_name()), PopupKind::Error, None, ); } Some(DirectMessageRoomAction::NewlyCreated { room_name_id, .. }) => { - self.navigate_to_room( - cx, - None, - &BasicRoomDetails::RoomId(room_name_id.clone()), - ); + self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id.clone())); } _ => {} } @@ -805,7 +673,7 @@ impl MatchEvent for App { } /// Clears all thread-local UI caches (user profiles, invited rooms, and timeline states). -/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, +/// The `cx` parameter ensures that these thread-local caches are cleared on the main UI thread, fn clear_all_app_state(cx: &mut Cx) { clear_user_profile_cache(cx); clear_all_invited_rooms(cx); @@ -857,34 +725,27 @@ impl AppMain for App { error!("Failed to save app state. Error: {e}"); } } - #[cfg(feature = "tsp")] - { + #[cfg(feature = "tsp")] { // Save the TSP wallet state, if it exists, with a 3-second timeout. let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap()); let res = crate::sliding_sync::block_on_async_with_timeout( Some(std::time::Duration::from_secs(3)), async move { match tsp_state.close_and_serialize().await { - Ok(saved_state) => { - match persistence::save_tsp_state_async(saved_state).await { - Ok(_) => {} - Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), - } - } - Err(e) => { - error!("Failed to close and serialize TSP wallet state. Error: {e}") + Ok(saved_state) => match persistence::save_tsp_state_async(saved_state).await { + Ok(_) => { } + Err(e) => error!("Failed to save TSP wallet state. Error: {e}"), } + Err(e) => error!("Failed to close and serialize TSP wallet state. Error: {e}"), } }, ); if let Err(_e) = res { - error!( - "Failed to save TSP wallet state before app shutdown. Error: Timed Out." - ); + error!("Failed to save TSP wallet state before app shutdown. Error: Timed Out."); } } } - + // Forward events to the MatchEvent trait implementation. self.match_event(cx, event); let scope = &mut Scope::with_data(&mut self.app_state); @@ -932,12 +793,8 @@ impl App { .modal(cx, ids!(login_screen_view.login_screen.login_status_modal)) .close(cx); } - self.ui - .view(cx, ids!(login_screen_view)) - .set_visible(cx, show_login); - self.ui - .view(cx, ids!(home_screen_view)) - .set_visible(cx, !show_login); + self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, show_login); + self.ui.view(cx, ids!(home_screen_view)).set_visible(cx, !show_login); } /// Navigates to the given `destination_room`, optionally closing the `room_to_close`. @@ -952,17 +809,16 @@ impl App { let tab_id = LiveId::from_str(to_close.as_str()); let widget_uid = self.ui.widget_uid(); move |cx: &mut Cx| { - cx.widget_action(widget_uid, DockAction::TabCloseWasPressed(tab_id)); - enqueue_rooms_list_update(RoomsListUpdate::HideRoom { - room_id: to_close.clone(), - }); + cx.widget_action( + widget_uid, + DockAction::TabCloseWasPressed(tab_id), + ); + enqueue_rooms_list_update(RoomsListUpdate::HideRoom { room_id: to_close.clone() }); } }); let destination_room_id = destination_room.room_id(); - let room_state = cx - .get_global::() - .get_room_state(destination_room_id); + let room_state = cx.get_global::().get_room_state(destination_room_id); let new_selected_room = match room_state { Some(RoomState::Joined) => SelectedRoom::JoinedRoom { room_name_id: destination_room.room_name_id().clone(), @@ -972,12 +828,11 @@ impl App { }, // If the destination room is not yet loaded, show a join modal. _ => { - log!( - "Destination room {:?} not loaded, showing join modal...", - destination_room.room_name_id() - ); - self.waiting_to_navigate_to_room = - Some((destination_room.clone(), room_to_close.cloned())); + log!("Destination room {:?} not loaded, showing join modal...", destination_room.room_name_id()); + self.waiting_to_navigate_to_room = Some(( + destination_room.clone(), + room_to_close.cloned(), + )); cx.action(JoinLeaveRoomModalAction::Open { kind: JoinLeaveModalKind::JoinRoom { details: destination_room.clone(), @@ -989,8 +844,8 @@ impl App { } }; - log!( - "Navigating to destination room {:?}, closing room {:?}", + + log!("Navigating to destination room {:?}, closing room {:?}", destination_room.room_name_id(), room_to_close, ); @@ -1001,7 +856,7 @@ impl App { cx.action(NavigationBarAction::GoToHome); } cx.widget_action( - self.ui.widget_uid(), + self.ui.widget_uid(), RoomsListAction::Selected(new_selected_room), ); // Select and scroll to the destination room in the rooms list. @@ -1017,43 +872,27 @@ impl App { /// 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), + 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), + 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. @@ -1087,11 +926,7 @@ impl App { | 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 - { + let thread_root = if let SelectedRoom::Thread { thread_root_event_id, .. } = &selected_room { Some(thread_root_event_id.clone()) } else { None @@ -1117,16 +952,8 @@ impl App { }; // 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()); + 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() { @@ -1136,11 +963,10 @@ impl App { 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.stack_navigation(cx, ids!(view_stack)).push(cx, view_id); self.ui.redraw(cx); } + } /// App-wide state that is stored persistently across multiple app runs @@ -1208,8 +1034,7 @@ impl BotSettingsState { if bound { if !self.is_room_bound(&room_id) { self.bound_rooms.push(room_id); - self.bound_rooms - .sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + self.bound_rooms.sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); } } else { self.bound_rooms @@ -1219,10 +1044,7 @@ impl BotSettingsState { /// Returns the configured botfather user ID, resolving a localpart against /// the current user's homeserver when needed. - pub fn resolved_bot_user_id( - &self, - current_user_id: Option<&UserId>, - ) -> Result { + pub fn resolved_bot_user_id(&self, current_user_id: Option<&UserId>) -> Result { let raw = self.botfather_user_id.trim(); if raw.starts_with('@') || raw.contains(':') { let full_user_id = if raw.starts_with('@') { @@ -1267,6 +1089,7 @@ pub struct SavedDockState { pub selected_room: Option, } + /// Represents a room currently or previously selected by the user. /// /// ## PartialEq/Eq equality comparison behavior @@ -1323,7 +1146,9 @@ impl SelectedRoom { match self { SelectedRoom::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => { let name = room_name_id.clone(); - *self = SelectedRoom::JoinedRoom { room_name_id: name }; + *self = SelectedRoom::JoinedRoom { + room_name_id: name, + }; true } _ => false, @@ -1333,14 +1158,11 @@ impl SelectedRoom { /// Returns the `LiveId` of the room tab corresponding to this `SelectedRoom`. pub fn tab_id(&self) -> LiveId { match self { - SelectedRoom::Thread { - room_name_id, - thread_root_event_id, - } => LiveId::from_str(&format!( - "{}##{}", - room_name_id.room_id(), - thread_root_event_id - )), + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + LiveId::from_str( + &format!("{}##{}", room_name_id.room_id(), thread_root_event_id) + ) + } other => LiveId::from_str(other.room_id().as_str()), } } diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index c45c7309f..418c5214c 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,371 +1,365 @@ use makepad_widgets::*; -use crate::{ - app::AppState, - home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, - settings::settings_screen::SettingsScreenWidgetRefExt, -}; +use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; script_mod! { -use mod.prelude.widgets.* -use mod.widgets.* - - -// Defines the total height of the StackNavigationView's header. -// This has to be set in multiple places because of how StackNavigation -// uses an Overlay view internally. -mod.widgets.STACK_VIEW_HEADER_HEIGHT = 75 - -// A reusable base for StackNavigationView children in the mobile layout. -// Each specific content view (room, invite, space lobby) extends this -// and places its own screen widget inside the body. -mod.widgets.RobrixContentView = StackNavigationView { - width: Fill, height: Fill - draw_bg.color: (COLOR_PRIMARY) - header +: { - clip_x: false, - clip_y: false, - show_bg: true, - draw_bg +: { - color: instance((COLOR_PRIMARY_DARKER)) - color_dither: uniform(1.0) - gradient_border_horizontal: uniform(0.0) - gradient_fill_horizontal: uniform(0.0) - color_2: instance(vec4(-1)) - - border_radius: uniform(4.0) - border_size: uniform(0.0) - border_color: instance(#0000) - border_color_2: instance(vec4(-1)) - - shadow_color: instance(#0005) - shadow_radius: uniform(9.0) - shadow_offset: uniform(vec2(1.0, 0.0)) - - rect_size2: varying(vec2(0)) - rect_size3: varying(vec2(0)) - rect_pos2: varying(vec2(0)) - rect_shift: varying(vec2(0)) - sdf_rect_pos: varying(vec2(0)) - sdf_rect_size: varying(vec2(0)) - - vertex: fn() { - let min_offset = min(self.shadow_offset vec2(0)) - self.rect_size2 = self.rect_size + 2.0*vec2(self.shadow_radius) - self.rect_size3 = self.rect_size2 + abs(self.shadow_offset) - self.rect_pos2 = self.rect_pos - vec2(self.shadow_radius) + min_offset - self.sdf_rect_size = self.rect_size2 - vec2(self.shadow_radius * 2.0 + self.border_size * 2.0) - self.sdf_rect_pos = -min_offset + vec2(self.border_size + self.shadow_radius) - self.rect_shift = -min_offset - - return self.clip_and_transform_vertex(self.rect_pos2 self.rect_size3) - } + use mod.prelude.widgets.* + use mod.widgets.* + + + // Defines the total height of the StackNavigationView's header. + // This has to be set in multiple places because of how StackNavigation + // uses an Overlay view internally. + mod.widgets.STACK_VIEW_HEADER_HEIGHT = 75 + + // A reusable base for StackNavigationView children in the mobile layout. + // Each specific content view (room, invite, space lobby) extends this + // and places its own screen widget inside the body. + mod.widgets.RobrixContentView = StackNavigationView { + width: Fill, height: Fill + draw_bg.color: (COLOR_PRIMARY) + header +: { + clip_x: false, + clip_y: false, + show_bg: true, + draw_bg +: { + color: instance((COLOR_PRIMARY_DARKER)) + color_dither: uniform(1.0) + gradient_border_horizontal: uniform(0.0) + gradient_fill_horizontal: uniform(0.0) + color_2: instance(vec4(-1)) + + border_radius: uniform(4.0) + border_size: uniform(0.0) + border_color: instance(#0000) + border_color_2: instance(vec4(-1)) + + shadow_color: instance(#0005) + shadow_radius: uniform(9.0) + shadow_offset: uniform(vec2(1.0, 0.0)) + + rect_size2: varying(vec2(0)) + rect_size3: varying(vec2(0)) + rect_pos2: varying(vec2(0)) + rect_shift: varying(vec2(0)) + sdf_rect_pos: varying(vec2(0)) + sdf_rect_size: varying(vec2(0)) + + vertex: fn() { + let min_offset = min(self.shadow_offset vec2(0)) + self.rect_size2 = self.rect_size + 2.0*vec2(self.shadow_radius) + self.rect_size3 = self.rect_size2 + abs(self.shadow_offset) + self.rect_pos2 = self.rect_pos - vec2(self.shadow_radius) + min_offset + self.sdf_rect_size = self.rect_size2 - vec2(self.shadow_radius * 2.0 + self.border_size * 2.0) + self.sdf_rect_pos = -min_offset + vec2(self.border_size + self.shadow_radius) + self.rect_shift = -min_offset + + return self.clip_and_transform_vertex(self.rect_pos2 self.rect_size3) + } - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size3) + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size3) - let mut fill_color = self.color - if self.color_2.x > -0.5 { - let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither - let dir = if self.gradient_fill_horizontal > 0.5 self.pos.x else self.pos.y - fill_color = mix(self.color self.color_2 dir + dither) - } + let mut fill_color = self.color + if self.color_2.x > -0.5 { + let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither + let dir = if self.gradient_fill_horizontal > 0.5 self.pos.x else self.pos.y + fill_color = mix(self.color self.color_2 dir + dither) + } - let mut stroke_color = self.border_color - if self.border_color_2.x > -0.5 { - let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither - let dir = if self.gradient_border_horizontal > 0.5 self.pos.x else self.pos.y - stroke_color = mix(self.border_color self.border_color_2 dir + dither) - } + let mut stroke_color = self.border_color + if self.border_color_2.x > -0.5 { + let dither = Math.random_2d(self.pos.xy) * 0.04 * self.color_dither + let dir = if self.gradient_border_horizontal > 0.5 self.pos.x else self.pos.y + stroke_color = mix(self.border_color self.border_color_2 dir + dither) + } - sdf.box( - self.sdf_rect_pos.x - self.sdf_rect_pos.y - self.sdf_rect_size.x - self.sdf_rect_size.y - max(1.0 self.border_radius) - ) - if sdf.shape > -1.0 { - let m = self.shadow_radius - let o = self.shadow_offset + self.rect_shift - let v = GaussShadow.rounded_box_shadow(vec2(m) + o self.rect_size2+o self.pos * (self.rect_size3+vec2(m)) self.shadow_radius*0.5 self.border_radius*2.0) - sdf.clear(self.shadow_color*v) - } + sdf.box( + self.sdf_rect_pos.x + self.sdf_rect_pos.y + self.sdf_rect_size.x + self.sdf_rect_size.y + max(1.0 self.border_radius) + ) + if sdf.shape > -1.0 { + let m = self.shadow_radius + let o = self.shadow_offset + self.rect_shift + let v = GaussShadow.rounded_box_shadow(vec2(m) + o self.rect_size2+o self.pos * (self.rect_size3+vec2(m)) self.shadow_radius*0.5 self.border_radius*2.0) + sdf.clear(self.shadow_color*v) + } - sdf.fill_keep(fill_color) + sdf.fill_keep(fill_color) - if self.border_size > 0.0 { - sdf.stroke(stroke_color self.border_size) + if self.border_size > 0.0 { + sdf.stroke(stroke_color self.border_size) + } + return sdf.result } - return sdf.result } - } - - padding: Inset{top: 30, bottom: 0} - height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT), - content +: { - height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) - button_container +: { - padding: 0, - margin: 0 - left_button +: { - width: Fit, height: Fit, - padding: Inset{left: 20, right: 23, top: 10, bottom: 10} - margin: Inset{left: 8, right: 0, top: 0, bottom: 0} - draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } - icon_walk: Walk{width: 13, height: Fit} - spacing: 0 - text: "" + padding: Inset{top: 30, bottom: 0} + height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT), + + content +: { + height: (mod.widgets.STACK_VIEW_HEADER_HEIGHT) + button_container +: { + padding: 0, + margin: 0 + left_button +: { + width: Fit, height: Fit, + padding: Inset{left: 20, right: 23, top: 10, bottom: 10} + margin: Inset{left: 8, right: 0, top: 0, bottom: 0} + draw_icon +: { color: (ROOM_NAME_TEXT_COLOR) } + icon_walk: Walk{width: 13, height: Fit} + spacing: 0 + text: "" + } } - } - title_container +: { - padding: Inset{top: 8} - title +: { - draw_text +: { - color: (ROOM_NAME_TEXT_COLOR) + title_container +: { + padding: Inset{top: 8} + title +: { + draw_text +: { + color: (ROOM_NAME_TEXT_COLOR) + } } } } } + body +: { + margin: Inset{top: (mod.widgets.STACK_VIEW_HEADER_HEIGHT)} + } } - body +: { - margin: Inset{top: (mod.widgets.STACK_VIEW_HEADER_HEIGHT)} - } -} -// A wrapper view around the SpacesBar that lets us show/hide it via animation. -mod.widgets.SpacesBarWrapper = set_type_default() do #(SpacesBarWrapper::register_widget(vm)) { - ..mod.widgets.RoundedShadowView - - width: Fill, - height: (NAVIGATION_TAB_BAR_SIZE) - margin: Inset{left: 4, right: 4} - show_bg: true - draw_bg +: { - color: (COLOR_PRIMARY_DARKER) - border_radius: 4.0 - border_size: 0.0 - shadow_color: #0005 - shadow_radius: 15.0 - shadow_offset: vec2(1.0, 0.0) - } + // A wrapper view around the SpacesBar that lets us show/hide it via animation. + mod.widgets.SpacesBarWrapper = set_type_default() do #(SpacesBarWrapper::register_widget(vm)) { + ..mod.widgets.RoundedShadowView - CachedWidget { - root_spaces_bar := mod.widgets.SpacesBar {} - } - - animator: Animator{ - spaces_bar_animator: { - default: @hide - show: AnimatorState{ - redraw: true - from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } - apply: { height: (NAVIGATION_TAB_BAR_SIZE), draw_bg: { shadow_color: #x00000055 } } - } - hide: AnimatorState{ - redraw: true - from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } - apply: { height: 0, draw_bg: { shadow_color: (COLOR_TRANSPARENT) } } - } + width: Fill, + height: (NAVIGATION_TAB_BAR_SIZE) + margin: Inset{left: 4, right: 4} + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY_DARKER) + border_radius: 4.0 + border_size: 0.0 + shadow_color: #0005 + shadow_radius: 15.0 + shadow_offset: vec2(1.0, 0.0) } - } -} -// The home screen widget contains the main content: -// rooms list, room screens, and the settings screen as an overlay. -// It adapts to both desktop and mobile layouts. -mod.widgets.HomeScreen = #(HomeScreen::register_widget(vm)) { - AdaptiveView { - // NOTE: within each of these sub views, we used `CachedWidget` wrappers - // to ensure that there is only a single global instance of each - // of those widgets, which means they maintain their state - // across transitions between the Desktop and Mobile variant. - Desktop := SolidView { - width: Fill, height: Fill - flow: Right - align: Align{x: 0.0, y: 0.0} - padding: 0, - margin: 0, - - show_bg: true - draw_bg +: { - color: (COLOR_SECONDARY) - } + CachedWidget { + root_spaces_bar := mod.widgets.SpacesBar {} + } - // On the left, show the navigation tab bar vertically. - CachedWidget { - navigation_tab_bar := mod.widgets.NavigationTabBar {} + animator: Animator{ + spaces_bar_animator: { + default: @hide + show: AnimatorState{ + redraw: true + from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } + apply: { height: (NAVIGATION_TAB_BAR_SIZE), draw_bg: { shadow_color: #x00000055 } } + } + hide: AnimatorState{ + redraw: true + from: { all: Forward { duration: (mod.widgets.SPACES_BAR_ANIMATION_DURATION_SECS) } } + apply: { height: 0, draw_bg: { shadow_color: (COLOR_TRANSPARENT) } } + } } + } + } - // To the right of that, we use the PageFlip widget to show either - // the main desktop UI or the settings screen. - home_screen_page_flip := PageFlip { + // The home screen widget contains the main content: + // rooms list, room screens, and the settings screen as an overlay. + // It adapts to both desktop and mobile layouts. + mod.widgets.HomeScreen = #(HomeScreen::register_widget(vm)) { + AdaptiveView { + // NOTE: within each of these sub views, we used `CachedWidget` wrappers + // to ensure that there is only a single global instance of each + // of those widgets, which means they maintain their state + // across transitions between the Desktop and Mobile variant. + Desktop := SolidView { width: Fill, height: Fill + flow: Right + align: Align{x: 0.0, y: 0.0} + padding: 0, + margin: 0, + + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + } - lazy_init: true, - active_page: @home_page + // On the left, show the navigation tab bar vertically. + CachedWidget { + navigation_tab_bar := mod.widgets.NavigationTabBar {} + } - home_page := View { + // To the right of that, we use the PageFlip widget to show either + // the main desktop UI or the settings screen. + home_screen_page_flip := PageFlip { width: Fill, height: Fill - flow: Down - View { - width: Fill, - height: 39, - flow: Right - padding: Inset{top: 2, bottom: 2} - margin: Inset{right: 2} - spacing: 2 - align: Align{y: 0.5} + lazy_init: true, + active_page: @home_page - CachedWidget { - room_filter_input_bar := RoomFilterInputBar {} - } + home_page := View { + width: Fill, height: Fill + flow: Down - search_messages_button := SearchMessagesButton { - // make this button match/align with the RoomFilterInputBar - height: 32.5, + View { + width: Fill, + height: 39, + flow: Right + padding: Inset{top: 2, bottom: 2} margin: Inset{right: 2} + spacing: 2 + align: Align{y: 0.5} + + CachedWidget { + room_filter_input_bar := RoomFilterInputBar {} + } + + search_messages_button := SearchMessagesButton { + // make this button match/align with the RoomFilterInputBar + height: 32.5, + margin: Inset{right: 2} + } } - } - mod.widgets.MainDesktopUI {} - } + mod.widgets.MainDesktopUI {} + } - settings_page := SolidView { - width: Fill, height: Fill - show_bg: true, - draw_bg.color: (COLOR_PRIMARY) + settings_page := SolidView { + width: Fill, height: Fill + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) - CachedWidget { - settings_screen := mod.widgets.SettingsScreen {} + CachedWidget { + settings_screen := mod.widgets.SettingsScreen {} + } } - } - add_room_page := SolidView { - width: Fill, height: Fill - show_bg: true, - draw_bg.color: (COLOR_PRIMARY) + add_room_page := SolidView { + width: Fill, height: Fill + show_bg: true, + draw_bg.color: (COLOR_PRIMARY) - CachedWidget { - add_room_screen := mod.widgets.AddRoomScreen {} + CachedWidget { + add_room_screen := mod.widgets.AddRoomScreen {} + } } } } - } - - Mobile := SolidView { - width: Fill, height: Fill - flow: Down - show_bg: true - draw_bg.color: (COLOR_PRIMARY) + Mobile := SolidView { + width: Fill, height: Fill + flow: Down - view_stack := StackNavigation { - root_view +: { - flow: Down - width: Fill, height: Fill + show_bg: true + draw_bg.color: (COLOR_PRIMARY) - // At the top of the root view, we use the PageFlip widget to show either - // the main list of rooms or the settings screen. - home_screen_page_flip := PageFlip { + view_stack := StackNavigation { + root_view +: { + flow: Down width: Fill, height: Fill - lazy_init: true, - active_page: @home_page - - home_page := View { + // At the top of the root view, we use the PageFlip widget to show either + // the main list of rooms or the settings screen. + home_screen_page_flip := PageFlip { width: Fill, height: Fill - // Note: while the other page views have top padding, we do NOT add that here - // because it is added in the `RoomsSideBar`'s `RoundedShadowView` itself. - flow: Down - mod.widgets.RoomsSideBar {} - } + lazy_init: true, + active_page: @home_page - settings_page := View { - width: Fill, height: Fill - padding: Inset{top: 20} + home_page := View { + width: Fill, height: Fill + // Note: while the other page views have top padding, we do NOT add that here + // because it is added in the `RoomsSideBar`'s `RoundedShadowView` itself. + flow: Down - CachedWidget { - settings_screen := mod.widgets.SettingsScreen {} + mod.widgets.RoomsSideBar {} } - } - add_room_page := View { - width: Fill, height: Fill - padding: Inset{top: 20} + settings_page := View { + width: Fill, height: Fill + padding: Inset{top: 20} - CachedWidget { - add_room_screen := mod.widgets.AddRoomScreen {} + CachedWidget { + settings_screen := mod.widgets.SettingsScreen {} + } + } + + add_room_page := View { + width: Fill, height: Fill + padding: Inset{top: 20} + + CachedWidget { + add_room_screen := mod.widgets.AddRoomScreen {} + } } } - } - // Show the SpacesBar right above the navigation tab bar. - // We wrap it in the SpacesBarWrapper in order to animate it in or out, - // and wrap *that* in a CachedWidget in order to maintain its shown/hidden state - // across AdaptiveView transitions between Mobile view mode and Desktop view mode. - // - // ... Then we wrap *that* in a ... - CachedWidget { - spaces_bar_wrapper := mod.widgets.SpacesBarWrapper {} - } + // Show the SpacesBar right above the navigation tab bar. + // We wrap it in the SpacesBarWrapper in order to animate it in or out, + // and wrap *that* in a CachedWidget in order to maintain its shown/hidden state + // across AdaptiveView transitions between Mobile view mode and Desktop view mode. + // + // ... Then we wrap *that* in a ... + CachedWidget { + spaces_bar_wrapper := mod.widgets.SpacesBarWrapper {} + } - // At the bottom of the root view, show the navigation tab bar horizontally. - CachedWidget { - navigation_tab_bar := mod.widgets.NavigationTabBar {} + // At the bottom of the root view, show the navigation tab bar horizontally. + CachedWidget { + navigation_tab_bar := mod.widgets.NavigationTabBar {} + } } - } - // Room views: multiple instances to support deep stacking - // (e.g., room -> thread -> room -> thread -> ...). - // Each stack depth gets its own dedicated view widget, - // avoiding complex state save/restore when views are reused. - room_view_0 := mod.widgets.RobrixContentView { body +: { room_screen_0 := mod.widgets.RoomScreen {} } } - room_view_1 := mod.widgets.RobrixContentView { body +: { room_screen_1 := mod.widgets.RoomScreen {} } } - room_view_2 := mod.widgets.RobrixContentView { body +: { room_screen_2 := mod.widgets.RoomScreen {} } } - room_view_3 := mod.widgets.RobrixContentView { body +: { room_screen_3 := mod.widgets.RoomScreen {} } } - room_view_4 := mod.widgets.RobrixContentView { body +: { room_screen_4 := mod.widgets.RoomScreen {} } } - room_view_5 := mod.widgets.RobrixContentView { body +: { room_screen_5 := mod.widgets.RoomScreen {} } } - room_view_6 := mod.widgets.RobrixContentView { body +: { room_screen_6 := mod.widgets.RoomScreen {} } } - room_view_7 := mod.widgets.RobrixContentView { body +: { room_screen_7 := mod.widgets.RoomScreen {} } } - room_view_8 := mod.widgets.RobrixContentView { body +: { room_screen_8 := mod.widgets.RoomScreen {} } } - room_view_9 := mod.widgets.RobrixContentView { body +: { room_screen_9 := mod.widgets.RoomScreen {} } } - room_view_10 := mod.widgets.RobrixContentView { body +: { room_screen_10 := mod.widgets.RoomScreen {} } } - room_view_11 := mod.widgets.RobrixContentView { body +: { room_screen_11 := mod.widgets.RoomScreen {} } } - room_view_12 := mod.widgets.RobrixContentView { body +: { room_screen_12 := mod.widgets.RoomScreen {} } } - room_view_13 := mod.widgets.RobrixContentView { body +: { room_screen_13 := mod.widgets.RoomScreen {} } } - room_view_14 := mod.widgets.RobrixContentView { body +: { room_screen_14 := mod.widgets.RoomScreen {} } } - room_view_15 := mod.widgets.RobrixContentView { body +: { room_screen_15 := mod.widgets.RoomScreen {} } } - - invite_view := mod.widgets.RobrixContentView { - body +: { - invite_screen := mod.widgets.InviteScreen {} + // Room views: multiple instances to support deep stacking + // (e.g., room -> thread -> room -> thread -> ...). + // Each stack depth gets its own dedicated view widget, + // avoiding complex state save/restore when views are reused. + room_view_0 := mod.widgets.RobrixContentView { body +: { room_screen_0 := mod.widgets.RoomScreen {} } } + room_view_1 := mod.widgets.RobrixContentView { body +: { room_screen_1 := mod.widgets.RoomScreen {} } } + room_view_2 := mod.widgets.RobrixContentView { body +: { room_screen_2 := mod.widgets.RoomScreen {} } } + room_view_3 := mod.widgets.RobrixContentView { body +: { room_screen_3 := mod.widgets.RoomScreen {} } } + room_view_4 := mod.widgets.RobrixContentView { body +: { room_screen_4 := mod.widgets.RoomScreen {} } } + room_view_5 := mod.widgets.RobrixContentView { body +: { room_screen_5 := mod.widgets.RoomScreen {} } } + room_view_6 := mod.widgets.RobrixContentView { body +: { room_screen_6 := mod.widgets.RoomScreen {} } } + room_view_7 := mod.widgets.RobrixContentView { body +: { room_screen_7 := mod.widgets.RoomScreen {} } } + room_view_8 := mod.widgets.RobrixContentView { body +: { room_screen_8 := mod.widgets.RoomScreen {} } } + room_view_9 := mod.widgets.RobrixContentView { body +: { room_screen_9 := mod.widgets.RoomScreen {} } } + room_view_10 := mod.widgets.RobrixContentView { body +: { room_screen_10 := mod.widgets.RoomScreen {} } } + room_view_11 := mod.widgets.RobrixContentView { body +: { room_screen_11 := mod.widgets.RoomScreen {} } } + room_view_12 := mod.widgets.RobrixContentView { body +: { room_screen_12 := mod.widgets.RoomScreen {} } } + room_view_13 := mod.widgets.RobrixContentView { body +: { room_screen_13 := mod.widgets.RoomScreen {} } } + room_view_14 := mod.widgets.RobrixContentView { body +: { room_screen_14 := mod.widgets.RoomScreen {} } } + room_view_15 := mod.widgets.RobrixContentView { body +: { room_screen_15 := mod.widgets.RoomScreen {} } } + + invite_view := mod.widgets.RobrixContentView { + body +: { + invite_screen := mod.widgets.InviteScreen {} + } } - } - space_lobby_view := mod.widgets.RobrixContentView { - body +: { - space_lobby_screen := mod.widgets.SpaceLobbyScreen {} + space_lobby_view := mod.widgets.RobrixContentView { + body +: { + space_lobby_screen := mod.widgets.SpaceLobbyScreen {} + } } } } } } } -} + /// A simple wrapper around the SpacesBar that allows us to animate showing or hiding it. #[derive(Script, ScriptHook, Widget, Animator)] pub struct SpacesBarWrapper { - #[source] - source: ScriptObjectRef, - #[deref] - view: View, - #[apply_default] - animator: Animator, + #[source] source: ScriptObjectRef, + #[deref] view: View, + #[apply_default] animator: Animator, } impl Widget for SpacesBarWrapper { @@ -390,9 +384,7 @@ impl Widget for SpacesBarWrapper { impl SpacesBarWrapperRef { /// Shows or hides the spaces bar by animating it in or out. fn show_or_hide(&self, cx: &mut Cx, show: bool) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; if show { inner.animator_play(cx, ids!(spaces_bar_animator.show)); } else { @@ -402,20 +394,18 @@ impl SpacesBarWrapperRef { } } + #[derive(Script, ScriptHook, Widget)] pub struct HomeScreen { - #[deref] - view: View, + #[deref] view: View, /// The previously-selected navigation tab, used to determine which tab /// and top-level view we return to after closing the settings screen. /// /// Note that the current selected tap is stored in `AppState` so that /// other widgets can easily access it. - #[rust] - previous_selection: SelectedTab, - #[rust] - is_spaces_bar_shown: bool, + #[rust] previous_selection: SelectedTab, + #[rust] is_spaces_bar_shown: bool, } impl Widget for HomeScreen { @@ -428,9 +418,7 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Home) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Home; - cx.action(NavigationBarAction::TabSelected( - app_state.selected_tab.clone(), - )); + cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -439,23 +427,17 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::AddRoom) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::AddRoom; - cx.action(NavigationBarAction::TabSelected( - app_state.selected_tab.clone(), - )); + cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::GoToSpace { space_name_id }) => { - let new_space_selection = SelectedTab::Space { - space_name_id: space_name_id.clone(), - }; + let new_space_selection = SelectedTab::Space { space_name_id: space_name_id.clone() }; if app_state.selected_tab != new_space_selection { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = new_space_selection; - cx.action(NavigationBarAction::TabSelected( - app_state.selected_tab.clone(), - )); + cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } @@ -465,12 +447,8 @@ impl Widget for HomeScreen { if !matches!(app_state.selected_tab, SelectedTab::Settings) { self.previous_selection = app_state.selected_tab.clone(); app_state.selected_tab = SelectedTab::Settings; - cx.action(NavigationBarAction::TabSelected( - app_state.selected_tab.clone(), - )); - if let Some(settings_page) = - self.update_active_page_from_selection(cx, app_state) - { + cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); + if let Some(settings_page) = self.update_active_page_from_selection(cx, app_state) { settings_page .settings_screen(cx, ids!(settings_screen)) .populate(cx, None, &app_state.bot_settings); @@ -483,21 +461,19 @@ impl Widget for HomeScreen { Some(NavigationBarAction::CloseSettings) => { if matches!(app_state.selected_tab, SelectedTab::Settings) { app_state.selected_tab = self.previous_selection.clone(); - cx.action(NavigationBarAction::TabSelected( - app_state.selected_tab.clone(), - )); + cx.action(NavigationBarAction::TabSelected(app_state.selected_tab.clone())); self.update_active_page_from_selection(cx, app_state); self.view.redraw(cx); } } Some(NavigationBarAction::ToggleSpacesBar) => { self.is_spaces_bar_shown = !self.is_spaces_bar_shown; - self.view - .spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) + self.view.spaces_bar_wrapper(cx, ids!(spaces_bar_wrapper)) .show_or_hide(cx, self.is_spaces_bar_shown); } // We're the ones who emitted this action, so we don't need to handle it again. - Some(NavigationBarAction::TabSelected(_)) | None => {} + Some(NavigationBarAction::TabSelected(_)) + | None => { } } } } @@ -528,7 +504,8 @@ impl HomeScreen { .set_active_page( cx, match app_state.selected_tab { - SelectedTab::Space { .. } | SelectedTab::Home => id!(home_page), + SelectedTab::Space { .. } + | SelectedTab::Home => id!(home_page), SelectedTab::Settings => id!(settings_page), SelectedTab::AddRoom => id!(add_room_page), }, diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 9a048b91f..b2aaf90aa 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -75,7 +75,7 @@ script_mod! { } priority_button := mod.widgets.RoomContextMenuButton { - draw_icon +: { svg: (ICON_TOMBSTONE) } + draw_icon +: { svg: (ICON_TOMBSTONE) } text: "Set Low Priority" } @@ -83,7 +83,7 @@ script_mod! { draw_icon +: { svg: (ICON_LINK) } text: "Copy Link to Room" } - + divider1 := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -150,12 +150,9 @@ pub enum RoomContextMenuAction { #[derive(Script, ScriptHook, Widget)] pub struct RoomContextMenu { - #[deref] - view: View, - #[source] - source: ScriptObjectRef, - #[rust] - details: Option, + #[deref] view: View, + #[source] source: ScriptObjectRef, + #[rust] details: Option, } impl Widget for RoomContextMenu { @@ -167,25 +164,21 @@ impl Widget for RoomContextMenu { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { - if !self.visible { - return; - } + if !self.visible { return; } self.view.handle_event(cx, event, scope); // Close logic similar to NewMessageContextMenu let area = self.view.area(); let close_menu = { event.back_pressed() - || match event.hits_with_capture_overload(cx, area, true) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerUp(fue) if fue.is_over => !self - .view(cx, ids!(main_content)) - .area() - .rect(cx) - .contains(fue.abs), - Hit::FingerScroll(_) => true, - _ => false, + || match event.hits_with_capture_overload(cx, area, true) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerUp(fue) if fue.is_over => { + !self.view(cx, ids!(main_content)).area().rect(cx).contains(fue.abs) } + Hit::FingerScroll(_) => true, + _ => false, + } }; if close_menu { @@ -199,30 +192,31 @@ impl Widget for RoomContextMenu { impl WidgetMatchEvent for RoomContextMenu { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions, scope: &mut Scope) { - let Some(details) = self.details.as_ref() else { - return; - }; + let Some(details) = self.details.as_ref() else { return }; let mut close_menu = false; - + if self.button(cx, ids!(mark_unread_button)).clicked(actions) { submit_async_request(MatrixRequest::SetUnreadFlag { room_id: details.room_name_id.room_id().clone(), mark_as_unread: !details.is_marked_unread, }); close_menu = true; - } else if self.button(cx, ids!(favorite_button)).clicked(actions) { + } + else if self.button(cx, ids!(favorite_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsFavorite { room_id: details.room_name_id.room_id().clone(), is_favorite: !details.is_favorite, }); close_menu = true; - } else if self.button(cx, ids!(priority_button)).clicked(actions) { + } + else if self.button(cx, ids!(priority_button)).clicked(actions) { submit_async_request(MatrixRequest::SetIsLowPriority { room_id: details.room_name_id.room_id().clone(), is_low_priority: !details.is_low_priority, }); close_menu = true; - } else if self.button(cx, ids!(copy_link_button)).clicked(actions) { + } + else if self.button(cx, ids!(copy_link_button)).clicked(actions) { submit_async_request(MatrixRequest::GenerateMatrixLink { room_id: details.room_name_id.room_id().clone(), event_id: None, @@ -230,7 +224,8 @@ impl WidgetMatchEvent for RoomContextMenu { join_on_click: false, }); close_menu = true; - } else if self.button(cx, ids!(room_settings_button)).clicked(actions) { + } + else if self.button(cx, ids!(room_settings_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room settings page is not yet implemented.", @@ -238,7 +233,8 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } else if self.button(cx, ids!(notifications_button)).clicked(actions) { + } + else if self.button(cx, ids!(notifications_button)).clicked(actions) { // TODO: handle/implement this enqueue_popup_notification( "The room notifications page is not yet implemented.", @@ -246,16 +242,15 @@ impl WidgetMatchEvent for RoomContextMenu { Some(5.0), ); close_menu = true; - } else if self.button(cx, ids!(invite_button)).clicked(actions) { + } + else if self.button(cx, ids!(invite_button)).clicked(actions) { cx.action(InviteModalAction::Open(details.room_name_id.clone())); close_menu = true; - } else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { + } + else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { if let Some(app_state) = scope.data.get::() { let room_id = details.room_name_id.room_id().clone(); - match app_state - .bot_settings - .resolved_bot_user_id(current_user_id().as_deref()) - { + match app_state.bot_settings.resolved_bot_user_id(current_user_id().as_deref()) { Ok(bot_user_id) => { if details.is_bot_bound { submit_async_request(MatrixRequest::SetRoomBotBinding { @@ -293,7 +288,8 @@ impl WidgetMatchEvent for RoomContextMenu { ); } close_menu = true; - } else if self.button(cx, ids!(leave_button)).clicked(actions) { + } + else if self.button(cx, ids!(leave_button)).clicked(actions) { use crate::join_leave_room_modal::{JoinLeaveRoomModalAction, JoinLeaveModalKind}; use crate::room::BasicRoomDetails; let room_details = BasicRoomDetails::Name(details.room_name_id.clone()); @@ -322,7 +318,7 @@ impl RoomContextMenu { cx.set_key_focus(self.view.area()); dvec2(MENU_WIDTH, height) } - + fn update_buttons(&mut self, cx: &mut Cx, details: &RoomContextMenuDetails) -> f64 { let mark_unread_button = self.button(cx, ids!(mark_unread_button)); if details.is_marked_unread { @@ -330,12 +326,12 @@ impl RoomContextMenu { } else { mark_unread_button.set_text(cx, "Mark as Unread"); } - + let favorite_button = self.button(cx, ids!(favorite_button)); if details.is_favorite { favorite_button.set_text(cx, "Un-favorite"); } else { - favorite_button.set_text(cx, "Favorite"); + favorite_button.set_text(cx, "Favorite"); } let priority_button = self.button(cx, ids!(priority_button)); @@ -352,7 +348,7 @@ impl RoomContextMenu { } else { bot_binding_button.set_text(cx, "Bind BotFather"); } - + // Reset hover states mark_unread_button.reset_hover(cx); favorite_button.reset_hover(cx); @@ -363,16 +359,12 @@ impl RoomContextMenu { self.button(cx, ids!(invite_button)).reset_hover(cx); bot_binding_button.reset_hover(cx); self.button(cx, ids!(leave_button)).reset_hover(cx); - + self.redraw(cx); - - // Calculate height (rudimentary) - sum of visible buttons + padding. - let button_count = if details.app_service_enabled { - 9.0 - } else { - 8.0 - }; - (button_count * BUTTON_HEIGHT) + 20.0 + 10.0 // approx + + // Calculate height (rudimentary) - sum of visible buttons + padding + // 8 or 9 buttons * 35.0 + 2 dividers * ~10.0 + padding + ((if details.app_service_enabled { 9.0 } else { 8.0 }) * BUTTON_HEIGHT) + 20.0 + 10.0 // approx } fn close(&mut self, cx: &mut Cx) { @@ -385,16 +377,12 @@ impl RoomContextMenu { impl RoomContextMenuRef { pub fn is_currently_shown(&self, cx: &mut Cx) -> bool { - let Some(inner) = self.borrow() else { - return false; - }; + let Some(inner) = self.borrow() else { return false }; inner.is_currently_shown(cx) } pub fn show(&self, cx: &mut Cx, details: RoomContextMenuDetails) -> DVec2 { - let Some(mut inner) = self.borrow_mut() else { - return DVec2::default(); - }; + let Some(mut inner) = self.borrow_mut() else { return DVec2::default()}; inner.show(cx, details) } } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 1b4ddc171..e3e9d7ab0 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1,106 +1,40 @@ //! The `RoomScreen` widget is the UI view that displays a single room or thread's timeline //! of events (messages,state changes, etc.), along with an input bar at the bottom. -use std::{ - borrow::Cow, - cell::RefCell, - ops::{DerefMut, Range}, - sync::Arc, -}; +use std::{borrow::Cow, cell::RefCell, ops::{DerefMut, Range}, sync::Arc}; use bytesize::ByteSize; use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, RoomDisplayName, - media::{MediaFormat, MediaRequestParameters}, - room::RoomMember, - ruma::{ - EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, - events::{ + OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ + EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ receipt::Receipt, room::{ - ImageInfo, MediaSource, - message::{ - AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, - FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, - LocationMessageEventContent, MessageFormat, MessageType, - NoticeMessageEventContent, RoomMessageEventContent, TextMessageEventContent, - VideoMessageEventContent, - }, + ImageInfo, MediaSource, message::{ + AudioMessageEventContent, EmoteMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, KeyVerificationRequestEventContent, LocationMessageEventContent, MessageFormat, MessageType, NoticeMessageEventContent, RoomMessageEventContent, TextMessageEventContent, VideoMessageEventContent + } }, sticker::{StickerEventContent, StickerMediaSource}, - }, - matrix_uri::MatrixId, - uint, - }, + }, matrix_uri::MatrixId, uint + } }; use matrix_sdk_ui::timeline::{ - self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, - MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, - PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, - TimelineItemContent, TimelineItemKind, VirtualTimelineItem, -}; -use ruma::{ - OwnedUserId, - api::client::receipt::create_receipt::v3::ReceiptType, - events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, - owned_room_id, + self, EmbeddedEvent, EncryptedMessage, EventTimelineItem, InReplyToDetails, MemberProfileChange, MembershipChange, MsgLikeContent, MsgLikeKind, OtherMessageLike, PollState, RoomMembershipChange, TimelineDetails, TimelineEventItemId, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem }; +use ruma::{OwnedUserId, api::client::receipt::create_receipt::v3::ReceiptType, events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent}, owned_room_id}; use crate::{ - app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, - avatar_cache, - event_preview::{ - plaintext_body_of_timeline_item, text_preview_of_encrypted_message, - text_preview_of_member_profile_change, text_preview_of_other_message_like, - text_preview_of_other_state, text_preview_of_room_membership_change, - text_preview_of_timeline_item, - }, - home::{ - create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, - delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, - edited_indicator::EditedIndicatorWidgetRefExt, - link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, - loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, - room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, - rooms_list::{RoomsListAction, RoomsListRef}, - tombstone_footer::SuccessorRoomDetails, - }, - media_cache::{MediaCache, MediaCacheEntry}, - profile::{ - user_profile::{ - ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, - UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt, - }, + app::{AppState, AppStateAction, ConfirmDeleteAction, SelectedRoom}, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{create_bot_modal::{CreateBotModalAction, CreateBotModalWidgetExt}, delete_bot_modal::{DeleteBotModalAction, DeleteBotModalWidgetExt}, edited_indicator::EditedIndicatorWidgetRefExt, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, room_image_viewer::{get_image_name_and_filesize, populate_matrix_image_modal}, rooms_list::{RoomsListAction, RoomsListRef}, tombstone_footer::SuccessorRoomDetails}, media_cache::{MediaCache, MediaCacheEntry}, profile::{ + user_profile::{ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt}, user_profile_cache, }, - room::{ - BasicRoomDetails, - room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, - typing_notice::TypingNoticeWidgetExt, - }, + room::{BasicRoomDetails, room_input_bar::{RoomInputBarState, RoomInputBarWidgetRefExt}, typing_notice::TypingNoticeWidgetExt}, shared::{ - avatar::{AvatarState, AvatarWidgetRefExt}, - confirmation_modal::ConfirmationModalContent, - html_or_plaintext::{ - HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction, - }, - image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, - jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, - popup_list::{PopupKind, enqueue_popup_notification}, - restore_status_view::RestoreStatusViewWidgetExt, - styles::*, - text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, - timestamp::TimestampWidgetRefExt, - }, - sliding_sync::{ - BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, - TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, - submit_async_request, take_timeline_endpoints, + avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::ConfirmationModalContent, html_or_plaintext::{HtmlOrPlaintextRef, HtmlOrPlaintextWidgetRefExt, RobrixHtmlLinkAction}, image_viewer::{ImageViewerAction, ImageViewerMetaData, LoadState}, jump_to_bottom_button::{JumpToBottomButtonWidgetExt, UnreadMessageCount}, popup_list::{PopupKind, enqueue_popup_notification}, restore_status_view::RestoreStatusViewWidgetExt, styles::*, text_or_image::{TextOrImageAction, TextOrImageRef, TextOrImageWidgetRefExt}, timestamp::TimestampWidgetRefExt }, - utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime}, + sliding_sync::{BackwardsPaginateUntilEventRequest, MatrixRequest, PaginationDirection, TimelineEndpoints, TimelineKind, TimelineRequestSender, UserPowerLevels, current_user_id, get_client, submit_async_request, take_timeline_endpoints}, utils::{self, ImageFormat, MEDIA_THUMBNAIL_FORMAT, RoomNameId, unix_time_millis_to_datetime} }; use crate::home::event_reaction_list::ReactionListWidgetRefExt; use crate::home::room_read_receipt::AvatarRowWidgetRefExt; @@ -109,12 +43,7 @@ use crate::shared::mentionable_text_input::MentionableTextInputAction; use rangemap::RangeSet; -use super::{ - event_reaction_list::ReactionData, - loading_pane::LoadingPaneRef, - new_message_context_menu::{MessageAbilities, MessageDetails}, - room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}, -}; +use super::{event_reaction_list::ReactionData, loading_pane::LoadingPaneRef, new_message_context_menu::{MessageAbilities, MessageDetails}, room_read_receipt::{self, populate_read_receipts, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT}}; /// The maximum number of timeline items to search through /// when looking for a particular event. @@ -190,6 +119,7 @@ fn resolve_delete_bot_user_id( .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")) } + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -934,30 +864,22 @@ script_mod! { /// The main widget that displays a single Matrix room. #[derive(Script, Widget)] pub struct RoomScreen { - #[deref] - view: View, + #[deref] view: View, /// The name and ID of the currently-shown room, if any. - #[rust] - room_name_id: Option, + #[rust] room_name_id: Option, /// The timeline currently displayed by this RoomScreen, if any. - #[rust] - timeline_kind: Option, + #[rust] timeline_kind: Option, /// The persistent UI-relevant states for the room that this widget is currently displaying. - #[rust] - tl_state: Option, + #[rust] tl_state: Option, /// The set of pinned events in this room. - #[rust] - pinned_events: Vec, + #[rust] pinned_events: Vec, /// Whether this room has been successfully loaded (received from the homeserver). - #[rust] - is_loaded: bool, + #[rust] is_loaded: bool, /// Whether or not all rooms have been loaded (received from the homeserver). - #[rust] - all_rooms_loaded: bool, + #[rust] all_rooms_loaded: bool, /// Whether the in-room app service quick actions card is currently visible. - #[rust] - show_app_service_actions: bool, + #[rust] show_app_service_actions: bool, } impl Drop for RoomScreen { @@ -989,8 +911,7 @@ impl Widget for RoomScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { let room_screen_widget_uid = self.widget_uid(); let portal_list = self.portal_list(cx, ids!(timeline.list)); - let user_profile_sliding_pane = - self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); + let user_profile_sliding_pane = self.user_profile_sliding_pane(cx, ids!(user_profile_sliding_pane)); let loading_pane = self.loading_pane(cx, ids!(loading_pane)); // Handle actions here before processing timeline updates. @@ -1005,13 +926,9 @@ impl Widget for RoomScreen { if let RoomScreenTooltipActions::HoverInReactionButton { widget_rect, reaction_data, - } = reaction_list.hovered_in(actions) - { - let Some(_tl_state) = self.tl_state.as_ref() else { - continue; - }; - let tooltip_text_arr: Vec = reaction_data - .reaction_senders + } = reaction_list.hovered_in(actions) { + let Some(_tl_state) = self.tl_state.as_ref() else { continue }; + let tooltip_text_arr: Vec = reaction_data.reaction_senders .iter() .map(|(sender, _react_info)| { user_profile_cache::get_user_display_name_for_room( @@ -1025,13 +942,10 @@ impl Widget for RoomScreen { }) .collect(); - let mut tooltip_text = utils::human_readable_list( - &tooltip_text_arr, - MAX_VISIBLE_AVATARS_IN_READ_RECEIPT, - ); + let mut tooltip_text = utils::human_readable_list(&tooltip_text_arr, MAX_VISIBLE_AVATARS_IN_READ_RECEIPT); tooltip_text.push_str(&format!(" reacted with: {}", reaction_data.reaction)); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -1045,23 +959,24 @@ impl Widget for RoomScreen { // Handle a hover-out action on the reaction list or avatar row. let avatar_row_ref = wr.avatar_row(cx, ids!(avatar_row)); - if reaction_list.hovered_out(actions) || avatar_row_ref.hover_out(actions) { - cx.widget_action(room_screen_widget_uid, TooltipAction::HoverOut); + if reaction_list.hovered_out(actions) + || avatar_row_ref.hover_out(actions) + { + cx.widget_action( + room_screen_widget_uid, + TooltipAction::HoverOut, + ); } // Handle a hover-in action on the avatar row: show a read receipts summary. if let RoomScreenTooltipActions::HoverInReadReceipt { widget_rect, - read_receipts, - } = avatar_row_ref.hover_in(actions) - { - let Some(room_id) = self.room_id() else { - return; - }; - let tooltip_text = - room_read_receipt::populate_tooltip(cx, read_receipts, room_id); + read_receipts + } = avatar_row_ref.hover_in(actions) { + let Some(room_id) = self.room_id() else { return; }; + let tooltip_text= room_read_receipt::populate_tooltip(cx, read_receipts, room_id); cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, TooltipAction::HoverIn { text: tooltip_text, widget_rect, @@ -1075,27 +990,23 @@ impl Widget for RoomScreen { // Handle an image within the message being clicked. let content_message = wr.text_or_image(cx, ids!(content.message)); - if let TextOrImageAction::Clicked(mxc_uri) = actions - .find_widget_action(content_message.widget_uid()) - .cast() - { + if let TextOrImageAction::Clicked(mxc_uri) = actions.find_widget_action(content_message.widget_uid()).cast() { let texture = content_message.get_texture(cx); - self.handle_image_click(cx, mxc_uri, texture, index); + self.handle_image_click( + cx, + mxc_uri, + texture, + index, + ); continue; } // Handle the invite_user_button (in a SmallStateEvent) being clicked. if wr.button(cx, ids!(invite_user_button)).clicked(actions) { - let Some(tl) = self.tl_state.as_ref() else { - continue; - }; - if let Some(event_tl_item) = - tl.items.get(index).and_then(|item| item.as_event()) - { + let Some(tl) = self.tl_state.as_ref() else { continue }; + if let Some(event_tl_item) = tl.items.get(index).and_then(|item| item.as_event()) { let user_id = event_tl_item.sender().to_owned(); - let username = if let TimelineDetails::Ready(profile) = - event_tl_item.sender_profile() - { + let username = if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { profile.display_name.as_deref().unwrap_or(user_id.as_str()) } else { user_id.as_str() @@ -1103,22 +1014,14 @@ impl Widget for RoomScreen { let room_id = tl.kind.room_id().clone(); let content = ConfirmationModalContent { title_text: "Send Invitation".into(), - body_text: format!( - "Are you sure you want to invite {username} to this room?" - ) - .into(), + body_text: format!("Are you sure you want to invite {username} to this room?").into(), accept_button_text: Some("Invite".into()), on_accept_clicked: Some(Box::new(move |_cx| { - submit_async_request(MatrixRequest::InviteUser { - room_id, - user_id, - }); + submit_async_request(MatrixRequest::InviteUser { room_id, user_id }); })), ..Default::default() }; - cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new( - Some(content), - ))); + cx.action(InviteAction::ShowInviteConfirmationModal(RefCell::new(Some(content)))); } } } @@ -1127,19 +1030,11 @@ impl Widget for RoomScreen { for action in actions { // Handle actions related to restoring the previously-saved state of rooms. - if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, .. }) = - action.downcast_ref() - { - if self - .room_name_id - .as_ref() - .is_some_and(|rn| rn.room_id() == room_name_id.room_id()) - { + if let Some(AppStateAction::RoomLoadedSuccessfully { room_name_id, ..}) = action.downcast_ref() { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_name_id.room_id()) { // `set_displayed_room()` does nothing if the room_name_id is unchanged, so we clear it first. self.room_name_id = None; - let thread_root_event_id = self - .timeline_kind - .as_ref() + let thread_root_event_id = self.timeline_kind.as_ref() .and_then(|k| k.thread_root_event_id().cloned()); self.set_displayed_room(cx, room_name_id, thread_root_event_id); return; @@ -1149,11 +1044,7 @@ impl Widget for RoomScreen { // Handle InviteResultAction to show popup notifications. if let Some(InviteResultAction::Sent { room_id, .. }) = action.downcast_ref() { // Only handle if this is for the current room. - if self - .room_name_id - .as_ref() - .is_some_and(|rn| rn.room_id() == room_id) - { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { enqueue_popup_notification( "Sent invite successfully.", PopupKind::Success, @@ -1161,15 +1052,9 @@ impl Widget for RoomScreen { ); } } - if let Some(InviteResultAction::Failed { room_id, error, .. }) = - action.downcast_ref() - { + if let Some(InviteResultAction::Failed { room_id, error, .. }) = action.downcast_ref() { // Only handle if this is for the current room. - if self - .room_name_id - .as_ref() - .is_some_and(|rn| rn.room_id() == room_id) - { + if self.room_name_id.as_ref().is_some_and(|rn| rn.room_id() == room_id) { enqueue_popup_notification( format!("Failed to send invite.\n\nError: {error}"), PopupKind::Error, @@ -1179,15 +1064,11 @@ impl Widget for RoomScreen { } // Handle the highlight animation for a message. - let Some(tl) = self.tl_state.as_mut() else { - continue; - }; - if let MessageHighlightAnimationState::Pending { item_id } = - tl.message_highlight_animation_state - { + let Some(tl) = self.tl_state.as_mut() else { continue }; + if let MessageHighlightAnimationState::Pending { item_id } = tl.message_highlight_animation_state { if portal_list.smooth_scroll_reached(actions) { cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, MessageAction::HighlightMessage(item_id), ); tl.message_highlight_animation_state = MessageHighlightAnimationState::Off; @@ -1211,25 +1092,22 @@ impl Widget for RoomScreen { self.send_user_read_receipts_based_on_scroll_pos(cx, actions, &portal_list); // Handle the jump to bottom button: update its visibility, and handle clicks. - self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)) - .update_from_actions(cx, &portal_list, actions); + self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)).update_from_actions( + cx, + &portal_list, + actions, + ); } // Currently, a Signal event is only used to tell this widget: // 1. to check if the room has been loaded from the homeserver yet, or // 2. that its timeline events have been updated in the background. if let Event::Signal = event { - if let (false, Some(room_name_id), true) = ( - self.is_loaded, - self.room_name_id.as_ref(), - cx.has_global::(), - ) { + if let (false, Some(room_name_id), true) = (self.is_loaded, self.room_name_id.as_ref(), cx.has_global::()) { let rooms_list_ref = cx.get_global::(); if rooms_list_ref.is_room_loaded(room_name_id.room_id()) { let room_name_clone = room_name_id.clone(); - let thread_root_event_id = self - .timeline_kind - .as_ref() + let thread_root_event_id = self.timeline_kind.as_ref() .and_then(|k| k.thread_root_event_id().cloned()); // This room has been loaded now, so we call `set_displayed_room()`. // We first clear the `room_name_id`, otherwise that function will do nothing. @@ -1271,12 +1149,14 @@ impl Widget for RoomScreen { if is_interactive_hit { loading_pane.handle_event(cx, event, scope); } - } else if user_profile_sliding_pane.is_currently_shown(cx) { + } + else if user_profile_sliding_pane.is_currently_shown(cx) { is_pane_shown = true; if is_interactive_hit { user_profile_sliding_pane.handle_event(cx, event, scope); } - } else { + } + else { is_pane_shown = false; } @@ -1305,12 +1185,10 @@ impl Widget for RoomScreen { // Fetch room data once to avoid duplicate expensive lookups let (room_display_name, room_avatar_url) = get_client() .and_then(|client| client.get_room(&room_id)) - .map(|room| { - ( - room.cached_display_name().unwrap_or(RoomDisplayName::Empty), - room.avatar_url(), - ) - }) + .map(|room| ( + room.cached_display_name().unwrap_or(RoomDisplayName::Empty), + room.avatar_url() + )) .unwrap_or((RoomDisplayName::Empty, None)); RoomScreenProps { @@ -1327,9 +1205,7 @@ impl Widget for RoomScreen { RoomScreenProps { room_screen_widget_uid, room_name_id: room_name.clone(), - timeline_kind: self - .timeline_kind - .clone() + timeline_kind: self.timeline_kind.clone() .expect("BUG: room_name_id was set but timeline_kind was missing"), room_members: None, room_avatar_url: None, @@ -1341,9 +1217,7 @@ impl Widget for RoomScreen { if !is_pane_shown || !is_interactive_hit { return; } - log!( - "RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling" - ); + log!("RoomScreen handling event with no room_name_id and no tl_state, skipping room-dependent event handling"); // Use a dummy room props for non-room-specific events let room_id = owned_room_id!("!dummy:matrix.org"); RoomScreenProps { @@ -1358,11 +1232,13 @@ impl Widget for RoomScreen { }; let mut room_scope = Scope::with_props(&room_props); + // Forward the event to the inner timeline view, but capture any actions it produces // such that we can handle the ones relevant to only THIS RoomScreen widget right here and now, // ensuring they are not mistakenly handled by other RoomScreen widget instances. - let mut actions_generated_within_this_room_screen = - cx.capture_actions(|cx| self.view.handle_event(cx, event, &mut room_scope)); + let mut actions_generated_within_this_room_screen = cx.capture_actions(|cx| + self.view.handle_event(cx, event, &mut room_scope) + ); // Here, we handle and remove any general actions that are relevant to only this RoomScreen. // Removing the handled actions ensures they are not mistakenly handled by other RoomScreen widget instances. actions_generated_within_this_room_screen.retain(|action| { @@ -1649,6 +1525,7 @@ impl Widget for RoomScreen { } } + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { // If the room isn't loaded yet, we show the restore status label only. if !self.is_loaded { @@ -1656,8 +1533,7 @@ impl Widget for RoomScreen { // No room selected yet, nothing to show. return DrawStep::done(); }; - let mut restore_status_view = - self.view.restore_status_view(cx, ids!(restore_status_view)); + let mut restore_status_view = self.view.restore_status_view(cx, ids!(restore_status_view)); restore_status_view.set_content(cx, self.all_rooms_loaded, room_name); return restore_status_view.draw(cx, scope); } @@ -1667,14 +1543,13 @@ impl Widget for RoomScreen { return DrawStep::done(); } + let room_screen_widget_uid = self.widget_uid(); while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // Here, we only need to handle drawing the portal list. let portal_list_ref = subview.as_portal_list(); let Some(mut list_ref) = portal_list_ref.borrow_mut() else { - error!( - "!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else" - ); + error!("!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else"); continue; }; let Some(tl_state) = self.tl_state.as_mut() else { @@ -1694,170 +1569,143 @@ impl Widget for RoomScreen { if self.show_app_service_actions && tl_idx == tl_items.len() { list.item(cx, item_id, id!(AppServicePanel)) } else { - let Some(timeline_item) = tl_items.get(tl_idx) else { - // This shouldn't happen (unless the timeline gets corrupted or some other weird error), - // but we can always safely fill the item with an empty widget that takes up no space. - list.item(cx, item_id, id!(Empty)); - continue; - }; + let Some(timeline_item) = tl_items.get(tl_idx) else { + // This shouldn't happen (unless the timeline gets corrupted or some other weird error), + // but we can always safely fill the item with an empty widget that takes up no space. + list.item(cx, item_id, id!(Empty)); + continue; + }; - // Determine whether this item's content and profile have been drawn since the last update. - // Pass this state to each of the `populate_*` functions so they can attempt to re-use - // an item in the timeline's portallist that was previously populated, if one exists. - let item_drawn_status = ItemDrawnStatus { - content_drawn: tl_state - .content_drawn_since_last_update - .contains(&tl_idx), - profile_drawn: tl_state - .profile_drawn_since_last_update - .contains(&tl_idx), - }; - let (item, item_new_draw_status) = match timeline_item.kind() { - TimelineItemKind::Event(event_tl_item) => match event_tl_item.content() - { - TimelineItemContent::MsgLike(msg_like_content) => { - if tl_state.kind.thread_root_event_id().is_none() - && msg_like_content.thread_root.is_some() - { - // Hide threaded replies from the main room timeline UI. - ( - list.item(cx, item_id, id!(Empty)), - ItemDrawnStatus::both_drawn(), - ) - } else { - match &msg_like_content.kind { - MsgLikeKind::Message(_) - | MsgLikeKind::Sticker(_) - | MsgLikeKind::Redacted => { - let prev_event = tl_idx - .checked_sub(1) - .and_then(|i| tl_items.get(i)); - populate_message_view( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - msg_like_content, - prev_event, - &mut tl_state.media_cache, - &mut tl_state.link_preview_cache, - &tl_state.fetched_thread_summaries, - &mut tl_state.pending_thread_summary_fetches, - &tl_state.user_power, - &self.pinned_events, - item_drawn_status, - room_screen_widget_uid, - ) - } - // TODO: properly implement `Poll` as a regular Message-like timeline item. - MsgLikeKind::Poll(poll_state) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - poll_state, - item_drawn_status, - ) - } - MsgLikeKind::UnableToDecrypt(utd) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - utd, - item_drawn_status, - ) - } - MsgLikeKind::Other(other) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - other, - item_drawn_status, - ) - } - } + // Determine whether this item's content and profile have been drawn since the last update. + // Pass this state to each of the `populate_*` functions so they can attempt to re-use + // an item in the timeline's portallist that was previously populated, if one exists. + let item_drawn_status = ItemDrawnStatus { + content_drawn: tl_state.content_drawn_since_last_update.contains(&tl_idx), + profile_drawn: tl_state.profile_drawn_since_last_update.contains(&tl_idx), + }; + let (item, item_new_draw_status) = match timeline_item.kind() { + TimelineItemKind::Event(event_tl_item) => match event_tl_item.content() { + TimelineItemContent::MsgLike(msg_like_content) => { + if tl_state.kind.thread_root_event_id().is_none() + && msg_like_content.thread_root.is_some() + { + // Hide threaded replies from the main room timeline UI. + (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::both_drawn()) + } else { + match &msg_like_content.kind { + MsgLikeKind::Message(_) + | MsgLikeKind::Sticker(_) + | MsgLikeKind::Redacted => { + let prev_event = tl_idx.checked_sub(1).and_then(|i| tl_items.get(i)); + populate_message_view( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + msg_like_content, + prev_event, + &mut tl_state.media_cache, + &mut tl_state.link_preview_cache, + &tl_state.fetched_thread_summaries, + &mut tl_state.pending_thread_summary_fetches, + &tl_state.user_power, + &self.pinned_events, + item_drawn_status, + room_screen_widget_uid, + ) + }, + // TODO: properly implement `Poll` as a regular Message-like timeline item. + MsgLikeKind::Poll(poll_state) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + poll_state, + item_drawn_status, + ), + MsgLikeKind::UnableToDecrypt(utd) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + utd, + item_drawn_status, + ), + MsgLikeKind::Other(other) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + other, + item_drawn_status, + ), } } - TimelineItemContent::MembershipChange(membership_change) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - membership_change, - item_drawn_status, - ) - } - TimelineItemContent::ProfileChange(profile_change) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - profile_change, - item_drawn_status, - ) - } - TimelineItemContent::OtherState(other) => { - populate_small_state_event( - cx, - list, - item_id, - &tl_state.kind, - event_tl_item, - other, - item_drawn_status, - ) - } - unhandled => { - let item = list.item(cx, item_id, id!(SmallStateEvent)); - item.label(cx, ids!(content)) - .set_text(cx, &format!("[Unsupported] {:?}", unhandled)); - (item, ItemDrawnStatus::both_drawn()) - } }, - TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(millis)) => { - let item = list.item(cx, item_id, id!(DateDivider)); - let text = unix_time_millis_to_datetime(*millis) - // format the time as a shortened date (Sat, Sept 5, 2021) - .map(|dt| format!("{}", dt.date_naive().format("%a %b %-d, %Y"))) - .unwrap_or_else(|| format!("{:?}", millis)); - item.label(cx, ids!(date)).set_text(cx, &text); - (item, ItemDrawnStatus::both_drawn()) - } - TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { - let item = list.item(cx, item_id, id!(ReadMarker)); - (item, ItemDrawnStatus::both_drawn()) - } - TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { - let item = list.item(cx, item_id, id!(Empty)); + TimelineItemContent::MembershipChange(membership_change) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + membership_change, + item_drawn_status, + ), + TimelineItemContent::ProfileChange(profile_change) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + profile_change, + item_drawn_status, + ), + TimelineItemContent::OtherState(other) => populate_small_state_event( + cx, + list, + item_id, + &tl_state.kind, + event_tl_item, + other, + item_drawn_status, + ), + unhandled => { + let item = list.item(cx, item_id, id!(SmallStateEvent)); + item.label(cx, ids!(content)).set_text(cx, &format!("[Unsupported] {:?}", unhandled)); (item, ItemDrawnStatus::both_drawn()) } - }; - - // Now that we've drawn the item, add its index to the set of drawn items. - if item_new_draw_status.content_drawn { - tl_state - .content_drawn_since_last_update - .insert(tl_idx..tl_idx + 1); } - if item_new_draw_status.profile_drawn { - tl_state - .profile_drawn_since_last_update - .insert(tl_idx..tl_idx + 1); + TimelineItemKind::Virtual(VirtualTimelineItem::DateDivider(millis)) => { + let item = list.item(cx, item_id, id!(DateDivider)); + let text = unix_time_millis_to_datetime(*millis) + // format the time as a shortened date (Sat, Sept 5, 2021) + .map(|dt| format!("{}", dt.date_naive().format("%a %b %-d, %Y"))) + .unwrap_or_else(|| format!("{:?}", millis)); + item.label(cx, ids!(date)).set_text(cx, &text); + (item, ItemDrawnStatus::both_drawn()) + } + TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker) => { + let item = list.item(cx, item_id, id!(ReadMarker)); + (item, ItemDrawnStatus::both_drawn()) } - item + TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => { + let item = list.item(cx, item_id, id!(Empty)); + (item, ItemDrawnStatus::both_drawn()) + } + }; + + // Now that we've drawn the item, add its index to the set of drawn items. + if item_new_draw_status.content_drawn { + tl_state.content_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); + } + if item_new_draw_status.profile_drawn { + tl_state.profile_drawn_since_last_update.insert(tl_idx .. tl_idx + 1); + } + item } }; item.draw_all(cx, scope); @@ -1866,10 +1714,7 @@ impl Widget for RoomScreen { // If the list is not filling the viewport, we need to back paginate the timeline // until we have enough events items to fill the viewport. if !tl_state.fully_paginated && !list.is_filling_viewport() { - log!( - "Automatically paginating timeline to fill viewport for room {:?}", - self.room_name_id - ); + log!("Automatically paginating timeline to fill viewport for room {:?}", self.room_name_id); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -2072,9 +1917,7 @@ impl RoomScreen { let jump_to_bottom_button = self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)); let curr_first_id = portal_list.first_id(); let ui = self.widget_uid(); - let Some(tl) = self.tl_state.as_mut() else { - return; - }; + let Some(tl) = self.tl_state.as_mut() else { return }; let mut done_loading = false; let mut should_continue_backwards_pagination = false; @@ -2095,19 +1938,10 @@ impl RoomScreen { tl.items = initial_items; done_loading = true; } - TimelineUpdate::NewItems { - new_items, - changed_indices, - is_append, - clear_cache, - } => { + TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { if new_items.is_empty() { if !tl.items.is_empty() { - log!( - "process_timeline_updates(): timeline (had {} items) was cleared for room {}", - tl.items.len(), - tl.kind.room_id() - ); + log!("process_timeline_updates(): timeline (had {} items) was cleared for room {}", tl.items.len(), tl.kind.room_id()); // For now, we paginate a cleared timeline in order to be able to show something at least. // A proper solution would be what's described below, which would be to save a few event IDs // and then either focus on them (if we're not close to the end of the timeline) @@ -2141,12 +1975,9 @@ impl RoomScreen { if new_items.len() == tl.items.len() { // log!("process_timeline_updates(): no jump necessary for updated timeline of same length: {}", items.len()); - } else if curr_first_id > new_items.len() { - log!( - "process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", - curr_first_id, - new_items.len() - ); + } + else if curr_first_id > new_items.len() { + log!("process_timeline_updates(): jumping to bottom: curr_first_id {} is out of bounds for {} new items", curr_first_id, new_items.len()); portal_list.set_first_id_and_scroll(new_items.len().saturating_sub(1), 0.0); portal_list.set_tail_range(true); jump_to_bottom_button.update_visibility(cx, true); @@ -2155,28 +1986,19 @@ impl RoomScreen { // in the timeline viewport so that we can maintain the scroll position of that item, // which ensures that the timeline doesn't jump around unexpectedly and ruin the user's experience. else if let Some((curr_item_idx, new_item_idx, new_item_scroll, _event_id)) = - prior_items_changed - .then(|| { - find_new_item_matching_current_item( - cx, - portal_list, - curr_first_id, - &tl.items, - &new_items, - ) - }) - .flatten() + prior_items_changed.then(|| + find_new_item_matching_current_item(cx, portal_list, curr_first_id, &tl.items, &new_items) + ) + .flatten() { if curr_item_idx != new_item_idx { - log!( - "process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}" - ); + log!("process_timeline_updates(): jumping view from event index {curr_item_idx} to new index {new_item_idx}, scroll {new_item_scroll}, event ID {_event_id}"); portal_list.set_first_id_and_scroll(new_item_idx, new_item_scroll); tl.prev_first_index = Some(new_item_idx); // Set scrolled_past_read_marker false when we jump to a new event tl.scrolled_past_read_marker = false; // Hide the tooltip when the timeline jumps, as a hover-out event won't occur. - cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); + cx.widget_action(ui, RoomScreenTooltipActions::HoverOut); } } // @@ -2192,9 +2014,8 @@ impl RoomScreen { // because the matrix SDK doesn't currently support querying unread message counts for threads. if matches!(tl.kind, TimelineKind::MainRoom { .. }) { // Immediately show the unread badge with no count while we fetch the actual count in the background. - jump_to_bottom_button - .show_unread_message_badge(cx, UnreadMessageCount::Unknown); - submit_async_request(MatrixRequest::GetNumberUnreadMessages { + jump_to_bottom_button.show_unread_message_badge(cx, UnreadMessageCount::Unknown); + submit_async_request(MatrixRequest::GetNumberUnreadMessages{ timeline_kind: tl.kind.clone(), }); } @@ -2208,15 +2029,10 @@ impl RoomScreen { let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); let mut loading_pane_state = loading_pane.take_state(); if let LoadingPaneState::BackwardsPaginateUntilEvent { - events_paginated, - target_event_id, - .. - } = &mut loading_pane_state - { + events_paginated, target_event_id, .. + } = &mut loading_pane_state { *events_paginated += new_items.len().saturating_sub(tl.items.len()); - log!( - "While finding target event {target_event_id}, we have now loaded {events_paginated} messages..." - ); + log!("While finding target event {target_event_id}, we have now loaded {events_paginated} messages..."); // Here, we assume that we have not yet found the target event, // so we need to continue paginating backwards. // If the target event has already been found, it will be handled @@ -2233,10 +2049,8 @@ impl RoomScreen { tl.profile_drawn_since_last_update.clear(); tl.fully_paginated = false; } else { - tl.content_drawn_since_last_update - .remove(changed_indices.clone()); - tl.profile_drawn_since_last_update - .remove(changed_indices.clone()); + tl.content_drawn_since_last_update.remove(changed_indices.clone()); + tl.profile_drawn_since_last_update.remove(changed_indices.clone()); // log!("process_timeline_updates(): changed_indices: {changed_indices:?}, items len: {}\ncontent drawn: {:#?}\nprofile drawn: {:#?}", items.len(), tl.content_drawn_since_last_update, tl.profile_drawn_since_last_update); } tl.items = new_items; @@ -2249,10 +2063,7 @@ impl RoomScreen { jump_to_bottom_button.show_unread_message_badge(cx, unread_messages_count); } } - TimelineUpdate::TargetEventFound { - target_event_id, - index, - } => { + TimelineUpdate::TargetEventFound { target_event_id, index } => { // log!("Target event found in room {}: {target_event_id}, index: {index}", tl.kind.room_id()); tl.request_sender.send_if_modified(|requests| { requests.retain(|r| &r.room_id != tl.kind.room_id()); @@ -2262,10 +2073,10 @@ impl RoomScreen { // sanity check: ensure the target event is in the timeline at the given `index`. let item = tl.items.get(index); - let is_valid = item.is_some_and(|item| { + let is_valid = item.is_some_and(|item| item.as_event() .is_some_and(|ev| ev.event_id() == Some(&target_event_id)) - }); + ); let loading_pane = self.view.loading_pane(cx, ids!(loading_pane)); // log!("TargetEventFound: is_valid? {is_valid}. room {}, event {target_event_id}, index {index} of {}\n --> item: {item:?}", tl.kind.room_id(), tl.items.len()); @@ -2284,24 +2095,19 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = - MessageHighlightAnimationState::Pending { item_id: index }; - } else { + tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { + item_id: index + }; + } + else { // Here, the target event was not found in the current timeline, // or we found it previously but it is no longer in the timeline (or has moved), // which means we encountered an error and are unable to jump to the target event. - error!( - "Target event index {index} of {} is out of bounds for room {}", - tl.items.len(), - tl.kind.room_id() - ); + error!("Target event index {index} of {} is out of bounds for room {}", tl.items.len(), tl.kind.room_id()); // Show this error in the loading pane, which should already be open. - loading_pane.set_state( - cx, - LoadingPaneState::Error(String::from( - "Unable to find related message; it may have been deleted.", - )), - ); + loading_pane.set_state(cx, LoadingPaneState::Error( + String::from("Unable to find related message; it may have been deleted.") + )); } should_continue_backwards_pagination = false; @@ -2318,25 +2124,16 @@ impl RoomScreen { } } TimelineUpdate::PaginationError { error, direction } => { - error!( - "Pagination error ({direction}) in {:?}: {error:?}", - self.room_name_id - ); + error!("Pagination error ({direction}) in {:?}: {error:?}", self.room_name_id); let room_name = self.room_name_id.as_ref().map(|r| r.to_string()); enqueue_popup_notification( - utils::stringify_pagination_error( - &error, - room_name.as_deref().unwrap_or(UNNAMED_ROOM), - ), + utils::stringify_pagination_error(&error, room_name.as_deref().unwrap_or(UNNAMED_ROOM)), PopupKind::Error, Some(10.0), ); done_loading = true; } - TimelineUpdate::PaginationIdle { - fully_paginated, - direction, - } => { + TimelineUpdate::PaginationIdle { fully_paginated, direction } => { if direction == PaginationDirection::Backwards { // Don't set `done_loading` to `true` here, because we want to keep the top space visible // (with the "loading" message) until the corresponding `NewItems` update is received. @@ -2348,12 +2145,9 @@ impl RoomScreen { error!("Unexpected PaginationIdle update in the Forwards direction"); } } - TimelineUpdate::EventDetailsFetched { event_id, result } => { + TimelineUpdate::EventDetailsFetched {event_id, result } => { if let Err(_e) = result { - error!( - "Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", - tl.kind.room_id() - ); + error!("Failed to fetch details fetched for event {event_id} in room {}. Error: {_e:?}", tl.kind.room_id()); } // Here, to be most efficient, we could redraw only the updated event, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. @@ -2364,8 +2158,7 @@ impl RoomScreen { num_replies, latest_reply_preview_text, } => { - tl.pending_thread_summary_fetches - .remove(&thread_root_event_id); + tl.pending_thread_summary_fetches.remove(&thread_root_event_id); tl.fetched_thread_summaries.insert( thread_root_event_id.clone(), FetchedThreadSummary { @@ -2373,15 +2166,14 @@ impl RoomScreen { latest_reply_preview_text, }, ); - let event_id_matches_at_index = tl - .items + let event_id_matches_at_index = tl.items .get(timeline_item_index) .and_then(|item| item.as_event()) .and_then(|ev| ev.event_id()) .is_some_and(|id| id == thread_root_event_id); if event_id_matches_at_index { tl.content_drawn_since_last_update - .remove(timeline_item_index..timeline_item_index + 1); + .remove(timeline_item_index .. timeline_item_index + 1); } else { tl.content_drawn_since_last_update.clear(); } @@ -2394,12 +2186,9 @@ impl RoomScreen { TimelineUpdate::RoomMembersListFetched { members } => { // Store room members directly in TimelineUiState tl.room_members = Some(Arc::new(members)); - } + }, TimelineUpdate::MediaFetched(request) => { - log!( - "process_timeline_updates(): media fetched for room {}", - tl.kind.room_id() - ); + log!("process_timeline_updates(): media fetched for room {}", tl.kind.room_id()); // Set Image to image viewer modal if the media is not a thumbnail. if let (MediaFormat::File, media_source) = (request.format, request.source) { populate_matrix_image_modal(cx, media_source, &mut tl.media_cache); @@ -2407,39 +2196,26 @@ impl RoomScreen { // Here, to be most efficient, we could redraw only the media items in the timeline, // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. } - TimelineUpdate::MessageEdited { - timeline_event_item_id: timeline_event_id, - result, - } => { - self.view - .room_input_bar(cx, ids!(room_input_bar)) + TimelineUpdate::MessageEdited { timeline_event_item_id: timeline_event_id, result } => { + self.view.room_input_bar(cx, ids!(room_input_bar)) .handle_edit_result(cx, timeline_event_id, result); } TimelineUpdate::PinResult { result, pin, .. } => { let (message, auto_dismissal_duration, kind) = match &result { Ok(true) => ( - format!( - "Successfully {} event.", - if pin { "pinned" } else { "unpinned" } - ), + format!("Successfully {} event.", if pin { "pinned" } else { "unpinned" }), Some(4.0), - PopupKind::Success, + PopupKind::Success ), Ok(false) => ( - format!( - "Message was already {}.", - if pin { "pinned" } else { "unpinned" } - ), + format!("Message was already {}.", if pin { "pinned" } else { "unpinned" }), Some(4.0), - PopupKind::Info, + PopupKind::Info ), Err(e) => ( - format!( - "Failed to {} event. Error: {e}", - if pin { "pin" } else { "unpin" } - ), + format!("Failed to {} event. Error: {e}", if pin { "pin" } else { "unpin" }), None, - PopupKind::Error, + PopupKind::Error ), }; enqueue_popup_notification(message, kind, auto_dismissal_duration); @@ -2463,8 +2239,7 @@ impl RoomScreen { } TimelineUpdate::UserPowerLevels(user_power_levels) => { tl.user_power = user_power_levels; - self.view - .room_input_bar(cx, ids!(room_input_bar)) + self.view.room_input_bar(cx, ids!(room_input_bar)) .update_user_power_levels(cx, user_power_levels); // Update the @room mention capability based on the user's power level cx.action(MentionableTextInputAction::PowerLevelsUpdated { @@ -2480,13 +2255,8 @@ impl RoomScreen { tl.latest_own_user_receipt = Some(receipt); } TimelineUpdate::Tombstoned(successor_room_details) => { - self.view - .room_input_bar(cx, ids!(room_input_bar)) - .update_tombstone_footer( - cx, - tl.kind.room_id(), - Some(&successor_room_details), - ); + self.view.room_input_bar(cx, ids!(room_input_bar)) + .update_tombstone_footer(cx, tl.kind.room_id(), Some(&successor_room_details)); tl.tombstone_info = Some(successor_room_details); } TimelineUpdate::LinkPreviewFetched => {} @@ -2517,6 +2287,7 @@ impl RoomScreen { } } + /// Handles a link being clicked in any child widgets of this RoomScreen. /// /// Returns `true` if the given `action` was handled as a link click. @@ -2560,11 +2331,7 @@ impl RoomScreen { true } MatrixId::Room(room_id) => { - if self - .room_name_id - .as_ref() - .is_some_and(|r| r.room_id() == room_id) - { + if self.room_name_id.as_ref().is_some_and(|r| r.room_id() == room_id) { enqueue_popup_notification( "You are already viewing that room.", PopupKind::Info, @@ -2572,9 +2339,7 @@ impl RoomScreen { ); return true; } - if let Some(room_name_id) = - cx.get_global::().get_room_name(room_id) - { + if let Some(room_name_id) = cx.get_global::().get_room_name(room_id) { cx.action(AppStateAction::NavigateToRoom { room_to_close: None, destination_room: BasicRoomDetails::Name(room_name_id), @@ -2608,7 +2373,8 @@ impl RoomScreen { let mut link_was_handled = false; if let Ok(matrix_to_uri) = MatrixToUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_to_uri.id(), matrix_to_uri.via()); - } else if let Ok(matrix_uri) = MatrixUri::parse(&url) { + } + else if let Ok(matrix_uri) = MatrixUri::parse(&url) { link_was_handled |= handle_matrix_link(matrix_uri.id(), matrix_uri.via()); } @@ -2624,13 +2390,8 @@ impl RoomScreen { } } true - } else if let RobrixHtmlLinkAction::ClickedMatrixLink { - url, - matrix_id, - via, - .. - } = action.as_widget_action().cast() - { + } + else if let RobrixHtmlLinkAction::ClickedMatrixLink { url, matrix_id, via, .. } = action.as_widget_action().cast() { let link_was_handled = handle_matrix_link(&matrix_id, &via); if !link_was_handled { log!("Opening URL \"{}\"", url); @@ -2644,7 +2405,8 @@ impl RoomScreen { } } true - } else { + } + else { false } } @@ -2660,13 +2422,8 @@ impl RoomScreen { let Some(media_source) = mxc_uri else { return; }; - let Some(tl_state) = self.tl_state.as_mut() else { - return; - }; - let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) - else { - return; - }; + let Some(tl_state) = self.tl_state.as_mut() else { return }; + let Some(event_tl_item) = tl_state.items.get(item_id).and_then(|item| item.as_event()) else { return }; let timestamp_millis = event_tl_item.timestamp(); let (image_name, image_file_size) = get_image_name_and_filesize(event_tl_item); @@ -2676,7 +2433,10 @@ impl RoomScreen { image_name, image_file_size, timestamp: unix_time_millis_to_datetime(timestamp_millis), - avatar_parameter: Some((tl_state.kind.clone(), event_tl_item.clone())), + avatar_parameter: Some(( + tl_state.kind.clone(), + event_tl_item.clone(), + )), }), ))); @@ -2697,15 +2457,13 @@ impl RoomScreen { details: &MessageDetails, ) -> Option<&'a EventTimelineItem> { let target_event_id = details.event_id()?; - if let Some(event) = items - .get(details.item_id) + if let Some(event) = items.get(details.item_id) .and_then(|item| item.as_event()) .filter(|ev| ev.event_id().is_some_and(|id| id == target_event_id)) { return Some(event); } - items - .iter() + items.iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .filter_map(|item| item.as_event()) @@ -2722,15 +2480,9 @@ impl RoomScreen { ) { let room_screen_widget_uid = self.widget_uid(); for action in actions { - match action - .as_widget_action() - .widget_uid_eq(room_screen_widget_uid) - .cast_ref() - { + match action.as_widget_action().widget_uid_eq(room_screen_widget_uid).cast_ref() { MessageAction::React { details, reaction } => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; submit_async_request(MatrixRequest::ToggleReaction { timeline_kind: tl.kind.clone(), timeline_event_id: details.timeline_event_id.clone(), @@ -2738,24 +2490,19 @@ impl RoomScreen { }); } MessageAction::Reply(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; - if let Some(event_tl_item) = - Self::find_event_in_timeline(&tl.items, details).cloned() - { + let Some(tl) = self.tl_state.as_ref() else { return }; + if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details).cloned() { let replied_to_info = EmbeddedEvent::from_timeline_item(&event_tl_item); - self.view - .room_input_bar(cx, ids!(room_input_bar)) + self.view.room_input_bar(cx, ids!(room_input_bar)) .show_replying_to(cx, (event_tl_item, replied_to_info), &tl.kind); - } else { + } + else { enqueue_popup_notification( "Could not find message in timeline to reply to. Please try again.", PopupKind::Error, Some(5.0), ); - error!( - "MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", + error!("MessageAction::Reply: couldn't find event [{}] {:?} to reply to in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -2763,21 +2510,22 @@ impl RoomScreen { } } MessageAction::Edit(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { - self.view - .room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane(cx, event_tl_item.clone(), tl.kind.clone()); - } else { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane( + cx, + event_tl_item.clone(), + tl.kind.clone(), + ); + } + else { enqueue_popup_notification( "Could not find message in timeline to edit. Please try again.", PopupKind::Error, Some(5.0), ); - error!( - "MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", + error!("MessageAction::Edit: couldn't find event [{}] {:?} to edit in room {:?}", details.item_id, details.timeline_event_id, self.room_id(), @@ -2785,20 +2533,21 @@ impl RoomScreen { } } MessageAction::EditLatest => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; - if let Some(latest_sent_msg) = tl - .items + let Some(tl) = self.tl_state.as_ref() else { return }; + if let Some(latest_sent_msg) = tl.items .iter() .rev() .take(MAX_ITEMS_TO_SEARCH_THROUGH) .find_map(|item| item.as_event().filter(|ev| ev.is_editable()).cloned()) { - self.view - .room_input_bar(cx, ids!(room_input_bar)) - .show_editing_pane(cx, latest_sent_msg, tl.kind.clone()); - } else { + self.view.room_input_bar(cx, ids!(room_input_bar)) + .show_editing_pane( + cx, + latest_sent_msg, + tl.kind.clone(), + ); + } + else { enqueue_popup_notification( "No recent message available to edit. Please manually select a message to edit.", PopupKind::Warning, @@ -2807,9 +2556,7 @@ impl RoomScreen { } } MessageAction::Pin(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -2825,9 +2572,7 @@ impl RoomScreen { } } MessageAction::Unpin(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_id) = details.event_id() { submit_async_request(MatrixRequest::PinEvent { timeline_kind: tl.kind.clone(), @@ -2843,19 +2588,17 @@ impl RoomScreen { } } MessageAction::CopyText(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { cx.copy_to_clipboard(&plaintext_body_of_timeline_item(event_tl_item)); - } else { + } + else { enqueue_popup_notification( "Could not find message in timeline to copy text from. Please try again.", PopupKind::Error, Some(5.0), ); - error!( - "MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", + error!("MessageAction::CopyText: couldn't find event [{}] {:?} to copy text from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -2863,49 +2606,22 @@ impl RoomScreen { } } MessageAction::CopyHtml(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; // The logic for getting the formatted body of a message is the same // as the logic used in `populate_message_view()`. let mut success = false; if let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) { if let Some(message) = event_tl_item.content().as_message() { match message.msgtype() { - MessageType::Text(TextMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::Notice(NoticeMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::Emote(EmoteMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::Image(ImageMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::File(FileMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::Audio(AudioMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::Video(VideoMessageEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }) - | MessageType::VerificationRequest( - KeyVerificationRequestEventContent { - formatted: Some(FormattedBody { body, .. }), - .. - }, - ) => { + MessageType::Text(TextMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::Notice(NoticeMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::Emote(EmoteMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::Image(ImageMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::File(FileMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::Audio(AudioMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::Video(VideoMessageEventContent { formatted: Some(FormattedBody { body, .. }), .. }) + | MessageType::VerificationRequest(KeyVerificationRequestEventContent { formatted: Some(FormattedBody { body, .. }), .. }) => + { cx.copy_to_clipboard(body); success = true; } @@ -2919,8 +2635,7 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!( - "MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", + error!("MessageAction::CopyHtml: couldn't find event [{}] {:?} to copy HTML from in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -2928,9 +2643,7 @@ impl RoomScreen { } } MessageAction::CopyLink(details) => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; if let Some(event_id) = details.event_id() { let matrix_to_uri = tl.kind.room_id().matrix_to_event_uri(event_id.clone()); cx.copy_to_clipboard(&matrix_to_uri.to_string()); @@ -2940,8 +2653,7 @@ impl RoomScreen { PopupKind::Error, Some(5.0), ); - error!( - "MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", + error!("MessageAction::CopyLink: no `event_id`: [{}] {:?} in room {}", details.item_id, details.timeline_event_id, tl.kind.room_id(), @@ -2949,11 +2661,8 @@ impl RoomScreen { } } MessageAction::ViewSource(details) => { - let Some(tl) = self.tl_state.as_ref() else { - continue; - }; - let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) - else { + let Some(tl) = self.tl_state.as_ref() else { continue }; + let Some(event_tl_item) = Self::find_event_in_timeline(&tl.items, details) else { enqueue_popup_notification( "Could not find message in timeline to view source.", PopupKind::Error, @@ -2977,9 +2686,7 @@ impl RoomScreen { } MessageAction::JumpToRelated(details) => { let Some(related_event_id) = details.related_event_id.as_ref() else { - error!( - "BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}" - ); + error!("BUG: MessageAction::JumpToRelated had no related event ID.\n{details:#?}"); enqueue_popup_notification( "Could not find related message or event in timeline.", PopupKind::Error, @@ -2992,21 +2699,25 @@ impl RoomScreen { related_event_id, Some(details.item_id), portal_list, - loading_pane, + loading_pane ); } MessageAction::JumpToEvent(event_id) => { - self.jump_to_event(cx, event_id, None, portal_list, loading_pane); + self.jump_to_event( + cx, + event_id, + None, + portal_list, + loading_pane + ); } MessageAction::OpenThread(thread_root_event_id) => { let Some(room_name_id) = self.room_name_id.as_ref().cloned() else { - error!( - "### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!" - ); - continue; + error!("### ERROR: MessageAction::OpenThread: thread_root_event_id: {thread_root_event_id}, but room_name_id was None!"); + continue }; cx.widget_action( - room_screen_widget_uid, + room_screen_widget_uid, RoomsListAction::Selected(SelectedRoom::Thread { room_name_id, thread_root_event_id: thread_root_event_id.clone(), @@ -3014,17 +2725,13 @@ impl RoomScreen { ); } MessageAction::Redact { details, reason } => { - let Some(tl) = self.tl_state.as_ref() else { - return; - }; + let Some(tl) = self.tl_state.as_ref() else { return }; let timeline_event_id = details.timeline_event_id.clone(); let timeline_kind = tl.kind.clone(); let reason = reason.clone(); let content = ConfirmationModalContent { title_text: "Delete Message".into(), - body_text: - "Are you sure you want to delete this message? This cannot be undone." - .into(), + body_text: "Are you sure you want to delete this message? This cannot be undone.".into(), accept_button_text: Some("Delete".into()), on_accept_clicked: Some(Box::new(move |_cx| { submit_async_request(MatrixRequest::RedactMessage { @@ -3042,15 +2749,15 @@ impl RoomScreen { // } // This is handled within the Message widget itself. - MessageAction::HighlightMessage(..) => {} + MessageAction::HighlightMessage(..) => { } // This is handled by the top-level App itself. - MessageAction::OpenMessageContextMenu { .. } => {} + MessageAction::OpenMessageContextMenu { .. } => { } // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarOpen { .. } => {} + MessageAction::ActionBarOpen { .. } => { } // This isn't yet handled, as we need to completely redesign it. - MessageAction::ActionBarClose => {} - MessageAction::ToggleAppServiceActions => {} - MessageAction::None => {} + MessageAction::ActionBarClose => { } + MessageAction::ToggleAppServiceActions => { } + MessageAction::None => { } } } } @@ -3068,17 +2775,14 @@ impl RoomScreen { portal_list: &PortalListRef, loading_pane: &LoadingPaneRef, ) { - let Some(tl) = self.tl_state.as_mut() else { - return; - }; + let Some(tl) = self.tl_state.as_mut() else { return }; let max_tl_idx = max_tl_idx.unwrap_or_else(|| tl.items.len()); // Attempt to find the index of replied-to message in the timeline. // Start from the current item's index (`tl_idx`) and search backwards, // since we know the related message must come before the current item. let mut num_items_searched = 0; - let related_msg_tl_index = tl - .items + let related_msg_tl_index = tl.items .focus() .narrow(..max_tl_idx) .into_iter() @@ -3101,13 +2805,11 @@ impl RoomScreen { // appear beneath the top of the viewport. portal_list.smooth_scroll_to(cx, index.saturating_sub(1), speed, None); // start highlight animation. - tl.message_highlight_animation_state = - MessageHighlightAnimationState::Pending { item_id: index }; + tl.message_highlight_animation_state = MessageHighlightAnimationState::Pending { + item_id: index + }; } else { - log!( - "The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", - tl.kind.room_id() - ); + log!("The related event {target_event_id} wasn't immediately available in room {}, searching for it in the background...", tl.kind.room_id()); // Here, we set the state of the loading pane and display it to the user. // The main logic will be handled in `process_timeline_updates()`, which is the only // place where we can receive updates to the timeline from the background tasks. @@ -3160,9 +2862,7 @@ impl RoomScreen { /// Invoke this when this timeline is being shown, /// e.g., when the user navigates to this timeline. fn show_timeline(&mut self, cx: &mut Cx) { - let kind = self - .timeline_kind - .clone() + let kind = self.timeline_kind.clone() .expect("BUG: Timeline::show_timeline(): no timeline_kind was set."); let room_id = kind.room_id().clone(); @@ -3179,10 +2879,8 @@ impl RoomScreen { return; } if !self.is_loaded && self.all_rooms_loaded { - panic!( - "BUG: timeline {kind} is not loaded, but its RoomScreen \ - was not waiting for its timeline to be loaded either." - ); + panic!("BUG: timeline {kind} is not loaded, but its RoomScreen \ + was not waiting for its timeline to be loaded either."); } return; }; @@ -3255,19 +2953,14 @@ impl RoomScreen { self.is_loaded = is_loaded_now; } - self.view - .restore_status_view(cx, ids!(restore_status_view)) - .set_visible(cx, !self.is_loaded); + self.view.restore_status_view(cx, ids!(restore_status_view)).set_visible(cx, !self.is_loaded); // Kick off a back pagination request if it's the first time loading this room, // because we want to show the user some messages as soon as possible // when they first open the room, and there might not be any messages yet. if is_first_time_being_loaded { if !tl_state.fully_paginated { - log!( - "Sending a first-time backwards pagination request for {}", - tl_state.kind - ); + log!("Sending a first-time backwards pagination request for {}", tl_state.kind); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), num_events: 50, @@ -3336,9 +3029,7 @@ impl RoomScreen { /// Invoke this when this RoomScreen/timeline is being hidden or no longer being shown. fn hide_timeline(&mut self) { - let Some(timeline_kind) = self.timeline_kind.clone() else { - return; - }; + let Some(timeline_kind) = self.timeline_kind.clone() else { return }; self.save_state(); @@ -3370,23 +3061,13 @@ impl RoomScreen { /// Note: after calling this function, the widget's `tl_state` will be `None`. fn save_state(&mut self) { let Some(mut tl) = self.tl_state.take() else { - error!( - "Timeline::save_state(): skipping due to missing state, room {:?}, {:?}", - self.timeline_kind, - self.room_name_id.as_ref().map(|r| r.display_name()) - ); + error!("Timeline::save_state(): skipping due to missing state, room {:?}, {:?}", self.timeline_kind, self.room_name_id.as_ref().map(|r| r.display_name())); return; }; let portal_list = self.child_by_path(ids!(timeline.list)).as_portal_list(); let room_input_bar = self.child_by_path(ids!(room_input_bar)).as_room_input_bar(); - log!( - "Saving state for room {:?}\n\t{:?}\n\tfirst_id: {:?}, scroll: {}", - self.room_name_id.as_ref().map(|r| r.display_name()), - self.timeline_kind, - portal_list.first_id(), - portal_list.scroll_position() - ); + log!("Saving state for room {:?}\n\t{:?}\n\tfirst_id: {:?}, scroll: {}", self.room_name_id.as_ref().map(|r| r.display_name()), self.timeline_kind, portal_list.first_id(), portal_list.scroll_position()); let state = SavedState { first_index_and_scroll: Some((portal_list.first_id(), portal_list.scroll_position())), room_input_bar_state: room_input_bar.save_state(), @@ -3411,12 +3092,7 @@ impl RoomScreen { // 1. Restore the position of the timeline. let portal_list = self.portal_list(cx, ids!(timeline.list)); if let Some((first_index, scroll_from_first_id)) = first_index_and_scroll { - log!( - "Restoring state for room {:?}: first_id: {:?}, scroll: {}", - self.room_name_id, - first_index, - scroll_from_first_id - ); + log!("Restoring state for room {:?}: first_id: {:?}, scroll: {}", self.room_name_id, first_index, scroll_from_first_id); portal_list.set_first_id_and_scroll(*first_index, *scroll_from_first_id); portal_list.set_tail_range(false); } else { @@ -3425,10 +3101,7 @@ impl RoomScreen { // The explicit reset is necessary when the same RoomScreen widget is reused for a // different room (e.g., via stack navigation view alternation), otherwise the portal list // would retain the previous room's scroll position which may be out of bounds. - log!( - "Restoring state for room {:?}: first_id: None, scroll: None", - self.room_name_id - ); + log!("Restoring state for room {:?}: first_id: None, scroll: None", self.room_name_id); portal_list.set_first_id_and_scroll(0, 0.0); portal_list.set_tail_range(true); } @@ -3465,11 +3138,7 @@ impl RoomScreen { // If this timeline is already displayed, we don't need to do anything major, // but we do need update the `room_name_id` in case it has changed, or it has been cleared. - if self - .timeline_kind - .as_ref() - .is_some_and(|kind| kind == &timeline_kind) - { + if self.timeline_kind.as_ref().is_some_and(|kind| kind == &timeline_kind) { self.room_name_id = Some(room_name_id.clone()); return; } @@ -3505,9 +3174,7 @@ impl RoomScreen { return; } let first_index = portal_list.first_id(); - let Some(tl_state) = self.tl_state.as_mut() else { - return; - }; + let Some(tl_state) = self.tl_state.as_mut() else { return }; if let Some(ref mut index) = tl_state.prev_first_index { // to detect change of scroll when scroll ends @@ -3518,7 +3185,7 @@ impl RoomScreen { .items .get(std::cmp::min( first_index + portal_list.visible_items(), - tl_state.items.len().saturating_sub(1), + tl_state.items.len().saturating_sub(1) )) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) @@ -3538,20 +3205,17 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } else { - if let Some(own_user_receipt_timestamp) = &tl_state - .latest_own_user_receipt - .clone() - .and_then(|receipt| receipt.ts) - { + if let Some(own_user_receipt_timestamp) = &tl_state.latest_own_user_receipt.clone() + .and_then(|receipt| receipt.ts) { let Some((_first_event_id, first_timestamp)) = tl_state .items .get(first_index) .and_then(|f| f.as_event()) .and_then(|f| f.event_id().map(|e| (e, f.timestamp()))) - else { - *index = first_index; - return; - }; + else { + *index = first_index; + return; + }; if own_user_receipt_timestamp >= &first_timestamp && own_user_receipt_timestamp <= &last_timestamp { @@ -3562,6 +3226,7 @@ impl RoomScreen { receipt_type: ReceiptType::FullyRead, }); } + } } } @@ -3580,22 +3245,14 @@ impl RoomScreen { actions: &ActionsBuf, portal_list: &PortalListRef, ) { - let Some(tl) = self.tl_state.as_mut() else { - return; - }; - if tl.fully_paginated { - return; - }; - if !portal_list.scrolled(actions) { - return; - }; + let Some(tl) = self.tl_state.as_mut() else { return }; + if tl.fully_paginated { return }; + if !portal_list.scrolled(actions) { return }; let first_index = portal_list.first_id(); if first_index == 0 && tl.last_scrolled_index > 0 { - log!( - "Scrolled up from item {} --> 0, sending back pagination request for room {}", - tl.last_scrolled_index, - tl.kind, + log!("Scrolled up from item {} --> 0, sending back pagination request for room {}", + tl.last_scrolled_index, tl.kind, ); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl.kind.clone(), @@ -3615,9 +3272,7 @@ impl RoomScreenRef { room_name_id: &RoomNameId, thread_root_event_id: Option, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; inner.set_displayed_room(cx, room_name_id, thread_root_event_id); } } @@ -3634,6 +3289,7 @@ pub struct RoomScreenProps { pub app_service_room_bound: bool, } + /// Actions for the room screen's tooltip. #[derive(Clone, Debug, Default)] pub enum RoomScreenTooltipActions { @@ -3732,7 +3388,9 @@ pub enum TimelineUpdate { /// includes a complete list of room members that can be shared across components. /// This is different from RoomMembersSynced which only indicates members were fetched /// but doesn't provide the actual data. - RoomMembersListFetched { members: Vec }, + RoomMembersListFetched { + members: Vec, + }, /// A notice with an option of Media Request Parameters that one or more requested media items (images, videos, etc.) /// that should be displayed in this timeline have now been fetched and are available. MediaFetched(MediaRequestParameters), @@ -3764,7 +3422,7 @@ thread_local! { /// The global set of all timeline states, one entry per room. /// /// This is only useful when accessed from the main UI thread. - static TIMELINE_STATES: RefCell> = + static TIMELINE_STATES: RefCell> = RefCell::new(HashMap::new()); } @@ -3880,9 +3538,7 @@ struct TimelineUiState { #[derive(Default, Debug)] enum MessageHighlightAnimationState { - Pending { - item_id: usize, - }, + Pending { item_id: usize }, #[default] Off, } @@ -3919,8 +3575,9 @@ fn find_new_item_matching_current_item( ) -> Option<(usize, usize, f64, OwnedEventId)> { let mut curr_item_focus = curr_items.focus(); let mut idx_curr = starting_at_curr_idx; - let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = - Vec::with_capacity(portal_list.visible_items()); + let mut curr_items_with_ids: Vec<(usize, OwnedEventId)> = Vec::with_capacity( + portal_list.visible_items() + ); // Find all items with real event IDs that are currently visible in the portal list. // TODO: if this is slow, we could limit it to 3-5 events at the most. @@ -3949,9 +3606,7 @@ fn find_new_item_matching_current_item( // some may be zeroed-out, so we need to account for that possibility by only // using events that have a real non-zero area if let Some(pos_offset) = portal_list.position_of_item(cx, *idx_curr) { - log!( - "Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}" - ); + log!("Found matching event ID {event_id} at index {idx_new} in new items list, corresponding to current item index {idx_curr} at pos offset {pos_offset}"); return Some((*idx_curr, idx_new, pos_offset, event_id.to_owned())); } } @@ -4025,8 +3680,7 @@ fn populate_message_view( TimelineItemContent::MsgLike(_msg_like_content) => { let prev_msg_sender = prev_event_tl_item.sender(); prev_msg_sender == event_tl_item.sender() - && ts_millis - .0 + && ts_millis.0 .checked_sub(prev_event_tl_item.timestamp().0) .is_some_and(|d| d < uint!(600000)) // 10 mins in millis } @@ -4043,12 +3697,8 @@ fn populate_message_view( let (item, used_cached_item) = match &msg_like_content.kind { MsgLikeKind::Message(msg) => { match msg.msgtype() { - MessageType::Text(TextMessageEventContent { - body, formatted, .. - }) => { - has_html_body = formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { + has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4076,13 +3726,9 @@ fn populate_message_view( } // A notice message is just a message sent by an automated bot, // so we treat it just like a message but use a different font color. - MessageType::Notice(NoticeMessageEventContent { - body, formatted, .. - }) => { + MessageType::Notice(NoticeMessageEventContent{body, formatted, ..}) => { is_notice = true; - has_html_body = formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4092,8 +3738,7 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = - item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); // Apply gray color to all text styles for notice messages. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -4123,8 +3768,7 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - let html_or_plaintext_ref = - item.html_or_plaintext(cx, ids!(content.message)); + let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); // Apply red color to all text styles for server notices. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -4139,12 +3783,10 @@ fn populate_message_view( "Server notice: {}\n\nNotice type:: {}{}{}", sn.body, sn.server_notice_type.as_str(), - sn.limit_type - .as_ref() + sn.limit_type.as_ref() .map(|l| format!("\nLimit type: {}", l.as_str())) .unwrap_or_default(), - sn.admin_contact - .as_ref() + sn.admin_contact.as_ref() .map(|c| format!("\nAdmin contact: {}", c)) .unwrap_or_default(), ); @@ -4167,12 +3809,8 @@ fn populate_message_view( } // An emote is just like a message but is prepended with the user's name // to indicate that it's an "action" that the user is performing. - MessageType::Emote(EmoteMessageEventContent { - body, formatted, .. - }) => { - has_html_body = formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { + has_html_body = formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4183,16 +3821,14 @@ fn populate_message_view( (item, true) } else { // Draw the profile up front here because we need the username for the emote body. - let (username, profile_drawn) = item - .avatar(cx, ids!(profile.avatar)) - .set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ); + let (username, profile_drawn) = item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ); // Prepend a "* " to the emote body, as suggested by the Matrix spec. let (body, formatted) = if let Some(fb) = formatted.as_ref() { @@ -4201,7 +3837,7 @@ fn populate_message_view( Some(FormattedBody { format: fb.format.clone(), body: format!("* {} {}", &username, &fb.body), - }), + }) ) } else { (Cow::from(format!("* {} {}", &username, body)), None) @@ -4225,9 +3861,7 @@ fn populate_message_view( } } MessageType::Image(image) => { - has_html_body = image - .formatted - .as_ref() + has_html_body = image.formatted.as_ref() .is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedImageMessage) @@ -4265,17 +3899,17 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - let is_location_fully_drawn = - populate_location_message_content(cx, &html_or_plaintext_ref, location); + let is_location_fully_drawn = populate_location_message_content( + cx, + &html_or_plaintext_ref, + location, + ); new_drawn_status.content_drawn = is_location_fully_drawn; (item, false) } } MessageType::File(file_content) => { - has_html_body = file_content - .formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = file_content.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4287,16 +3921,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = - populate_file_message_content(cx, &html_or_plaintext_ref, file_content); + new_drawn_status.content_drawn = populate_file_message_content( + cx, + &html_or_plaintext_ref, + file_content, + ); (item, false) } } MessageType::Audio(audio) => { - has_html_body = audio - .formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = audio.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4308,16 +3942,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = - populate_audio_message_content(cx, &html_or_plaintext_ref, audio); + new_drawn_status.content_drawn = populate_audio_message_content( + cx, + &html_or_plaintext_ref, + audio, + ); (item, false) } } MessageType::Video(video) => { - has_html_body = video - .formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = video.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = if use_compact_view { id!(CondensedMessage) } else { @@ -4329,16 +3963,16 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); - new_drawn_status.content_drawn = - populate_video_message_content(cx, &html_or_plaintext_ref, video); + new_drawn_status.content_drawn = populate_video_message_content( + cx, + &html_or_plaintext_ref, + video, + ); (item, false) } } MessageType::VerificationRequest(verification) => { - has_html_body = verification - .formatted - .as_ref() - .is_some_and(|f| f.format == MessageFormat::Html); + has_html_body = verification.formatted.as_ref().is_some_and(|f| f.format == MessageFormat::Html); let template = id!(Message); let (item, existed) = list.item_with_existed(cx, item_id, template); if existed && item_drawn_status.content_drawn { @@ -4350,8 +3984,7 @@ fn populate_message_view( body: format!( "Sent a verification request to {}.
(Supported methods: {})
", verification.to, - verification - .methods + verification.methods .iter() .map(|m| m.as_str()) .collect::>() @@ -4381,8 +4014,10 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)) - .set_text(cx, &format!("[Unsupported {:?}]", msg_like_content.kind)); + item.label(cx, ids!(content.message)).set_text( + cx, + &format!("[Unsupported {:?}]", msg_like_content.kind), + ); new_drawn_status.content_drawn = true; (item, false) } @@ -4392,9 +4027,7 @@ fn populate_message_view( // Handle sticker messages that are static images. MsgLikeKind::Sticker(sticker) => { has_html_body = false; - let StickerEventContent { - body, info, source, .. - } = sticker.content(); + let StickerEventContent { body, info, source, .. } = sticker.content(); let template = if use_compact_view { id!(CondensedImageMessage) @@ -4423,7 +4056,7 @@ fn populate_message_view( (item, true) } } - } + } // Handle messages that have been redacted (deleted). MsgLikeKind::Redacted => { has_html_body = false; @@ -4462,8 +4095,10 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { - item.label(cx, ids!(content.message)) - .set_text(cx, &format!("[Unsupported {:?}] ", other)); + item.label(cx, ids!(content.message)).set_text( + cx, + &format!("[Unsupported {:?}] ", other), + ); new_drawn_status.content_drawn = true; (item, false) } @@ -4475,14 +4110,13 @@ fn populate_message_view( // If we didn't use a cached item, we need to draw all other message content: // the reactions, the read receipts avatar row, the reply preview. if !used_cached_item { - item.reaction_list(cx, ids!(content.reaction_list)) - .set_list( - cx, - event_tl_item.content().reactions(), - timeline_kind.clone(), - timeline_event_id.clone(), - item_id, - ); + item.reaction_list(cx, ids!(content.reaction_list)).set_list( + cx, + event_tl_item.content().reactions(), + timeline_kind.clone(), + timeline_event_id.clone(), + item_id, + ); populate_read_receipts(&item, cx, timeline_kind, event_tl_item); let is_reply_fully_drawn = draw_replied_to_message( cx, @@ -4509,21 +4143,17 @@ fn populate_message_view( new_drawn_status.content_drawn &= is_thread_summary_fully_drawn; } + // We must always re-set the message details, even when re-using a cached portallist item, // because the item type might be the same but for a different message entirely. let message_details = MessageDetails { thread_root_event_id: msg_like_content.thread_root.clone().or_else(|| { - msg_like_content - .thread_summary - .as_ref() + msg_like_content.thread_summary.as_ref() .and_then(|_| event_tl_item.event_id().map(|id| id.to_owned())) }), timeline_event_id, item_id, - related_event_id: msg_like_content - .in_reply_to - .as_ref() - .map(|r| r.event_id.clone()), + related_event_id: msg_like_content.in_reply_to.as_ref().map(|r| r.event_id.clone()), room_screen_widget_uid, abilities: MessageAbilities::from_user_power_and_event( user_power_levels, @@ -4536,6 +4166,7 @@ fn populate_message_view( }; item.as_message().set_data(message_details); + // If `used_cached_item` is false, we should always redraw the profile, even if profile_drawn is true. let skip_draw_profile = use_compact_view || (used_cached_item && item_drawn_status.profile_drawn); @@ -4546,20 +4177,17 @@ fn populate_message_view( // log!("\t --> populate_message_view(): DRAWING profile draw for item_id: {item_id}"); let mut username_label = item.label(cx, ids!(content.username)); - if !is_server_notice { - // the normal case - let (username, profile_drawn) = - set_username_and_get_avatar_retval.unwrap_or_else(|| { - item.avatar(cx, ids!(profile.avatar)) - .set_avatar_and_get_username( - cx, - timeline_kind, - event_tl_item.sender(), - Some(event_tl_item.sender_profile()), - event_tl_item.event_id(), - true, - ) - }); + if !is_server_notice { // the normal case + let (username, profile_drawn) = set_username_and_get_avatar_retval.unwrap_or_else(|| + item.avatar(cx, ids!(profile.avatar)).set_avatar_and_get_username( + cx, + timeline_kind, + event_tl_item.sender(), + Some(event_tl_item.sender_profile()), + event_tl_item.event_id(), + true, + ) + ); if is_notice { script_apply_eval!(cx, username_label, { draw_text +: { @@ -4569,7 +4197,8 @@ fn populate_message_view( } username_label.set_text(cx, &username); new_drawn_status.profile_drawn = profile_drawn; - } else { + } + else { // Server notices are drawn with a red color avatar background and username. let avatar = item.avatar(cx, ids!(profile.avatar)); avatar.show_text(cx, Some(COLOR_FG_DANGER_RED), None, "⚠"); @@ -4590,46 +4219,33 @@ fn populate_message_view( // Set the timestamp. if let Some(dt) = unix_time_millis_to_datetime(ts_millis) { - item.timestamp(cx, ids!(profile.timestamp)) - .set_date_time(cx, dt); + item.timestamp(cx, ids!(profile.timestamp)).set_date_time(cx, dt); } // Set the "edited" indicator if this message was edited. if msg_like_content.as_message().is_some_and(|m| m.is_edited()) { - item.edited_indicator(cx, ids!(profile.edited_indicator)) - .set_latest_edit(cx, event_tl_item); + item.edited_indicator(cx, ids!(profile.edited_indicator)).set_latest_edit( + cx, + event_tl_item, + ); } - #[cfg(feature = "tsp")] - { + #[cfg(feature = "tsp")] { use matrix_sdk::ruma::serde::Base64; - use crate::tsp::{ - self, - tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}, - }; + use crate::tsp::{self, tsp_sign_indicator::{TspSignState, TspSignIndicatorWidgetRefExt}}; - if let Some(mut tsp_sig) = event_tl_item - .latest_json() + if let Some(mut tsp_sig) = event_tl_item.latest_json() .and_then(|raw| raw.get_field::("content").ok()) .flatten() .and_then(|content_obj| content_obj.get("org.robius.tsp_signature").cloned()) .and_then(|tsp_sig_value| serde_json::from_value::(tsp_sig_value).ok()) .map(|b64| b64.into_inner()) { - log!( - "Found event {:?} with TSP signature.", - event_tl_item.event_id() - ); - let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref() - .lock() - .unwrap() + log!("Found event {:?} with TSP signature.", event_tl_item.event_id()); + let tsp_sign_state = if let Some(sender_vid) = tsp::tsp_state_ref().lock().unwrap() .get_verified_vid_for(event_tl_item.sender()) { - log!( - "Found verified VID for sender {}: \"{}\"", - event_tl_item.sender(), - sender_vid.identifier() - ); + log!("Found verified VID for sender {}: \"{}\"", event_tl_item.sender(), sender_vid.identifier()); tsp_sdk::crypto::verify(&*sender_vid, &mut tsp_sig).map_or( TspSignState::WrongSignature, |(msg, msg_type)| { @@ -4641,11 +4257,7 @@ fn populate_message_view( TspSignState::Unknown }; - log!( - "TSP signature state for event {:?} is {:?}", - event_tl_item.event_id(), - tsp_sign_state - ); + log!("TSP signature state for event {:?} is {:?}", event_tl_item.event_id(), tsp_sign_state); item.tsp_sign_indicator(cx, ids!(profile.tsp_sign_indicator)) .show_with_state(cx, tsp_sign_state); } @@ -4668,8 +4280,7 @@ fn populate_text_message_content( ) -> bool { // The message was HTML-formatted rich text. let mut links = Vec::new(); - if let Some(fb) = formatted_body - .as_ref() + if let Some(fb) = formatted_body.as_ref() .and_then(|fb| (fb.format == MessageFormat::Html).then_some(fb)) { let linkified_html = utils::linkify_get_urls( @@ -4689,7 +4300,7 @@ fn populate_text_message_content( }; // Populate link previews if all required parameters are provided - if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = + if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) = (link_preview_ref, media_cache, link_preview_cache) { link_preview_ref.populate_below_message( @@ -4717,8 +4328,7 @@ fn populate_image_message_content( ) -> bool { // We don't use thumbnails, as their resolution is too low to be visually useful. // We also don't trust the provided mimetype, as it can be incorrect. - let (mimetype, _width, _height) = image_info_source - .as_ref() + let (mimetype, _width, _height) = image_info_source.as_ref() .map(|info| (info.mimetype.as_deref(), info.width, info.height)) .unwrap_or_default(); @@ -4726,7 +4336,10 @@ fn populate_image_message_content( // then show a message about it being unsupported (e.g., for animated gifs). if let Some(mime) = mimetype.as_ref() { if ImageFormat::from_mimetype(mime).is_none() { - text_or_image_ref.show_text(cx, format!("{body}\n\nUnsupported type {mime:?}")); + text_or_image_ref.show_text( + cx, + format!("{body}\n\nUnsupported type {mime:?}"), + ); return true; // consider this as fully drawn } } @@ -4735,132 +4348,102 @@ fn populate_image_message_content( // A closure that fetches and shows the image from the given `mxc_uri`, // marking it as fully drawn if the image was available. - let mut fetch_and_show_image_uri = - |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { - match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { - (MediaCacheEntry::Loaded(data), _media_format) => { - let show_image_result = text_or_image_ref.show_image( - cx, - Some(MediaSource::Plain(mxc_uri)), - |cx, img| { - utils::load_png_or_jpg(&img, cx, &data) - .map(|()| img.size_in_pixels(cx).unwrap_or_default()) - }, - ); + let mut fetch_and_show_image_uri = |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box| { + match media_cache.try_get_media_or_fetch(&mxc_uri, MEDIA_THUMBNAIL_FORMAT.into()) { + (MediaCacheEntry::Loaded(data), _media_format) => { + let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)),|cx, img| { + utils::load_png_or_jpg(&img, cx, &data) + .map(|()| img.size_in_pixels(cx).unwrap_or_default()) + }); + if let Err(e) = show_image_result { + let err_str = format!("{body}\n\nFailed to display image: {e:?}"); + error!("{err_str}"); + text_or_image_ref.show_text(cx, &err_str); + } + + // We're done drawing the image, so mark it as fully drawn. + fully_drawn = true; + } + (MediaCacheEntry::Requested, _media_format) => { + // If the image is being fetched, we try to show its blurhash. + if let (Some(ref blurhash), Some(width), Some(height)) = (image_info.blurhash.clone(), image_info.width, image_info.height) { + let show_image_result = text_or_image_ref.show_image(cx, Some(MediaSource::Plain(mxc_uri)), |cx, img| { + let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) else { + return Err(image_cache::ImageError::EmptyData) + }; + let (width, height): (u32, u32) = (width, height); + if width == 0 || height == 0 { + warning!("Image had an invalid aspect ratio (width or height of 0)."); + return Err(image_cache::ImageError::EmptyData); + } + let aspect_ratio: f32 = width as f32 / height as f32; + // Cap the blurhash to a max size of 500 pixels in each dimension + // because the `blurhash::decode()` function can be rather expensive. + let (mut capped_width, mut capped_height) = (width, height); + if capped_height > BLURHASH_IMAGE_MAX_SIZE { + capped_height = BLURHASH_IMAGE_MAX_SIZE; + capped_width = (capped_height as f32 * aspect_ratio).floor() as u32; + } + if capped_width > BLURHASH_IMAGE_MAX_SIZE { + capped_width = BLURHASH_IMAGE_MAX_SIZE; + capped_height = (capped_width as f32 / aspect_ratio).floor() as u32; + } + + match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { + Ok(data) => { + ImageBuffer::new(&data, capped_width as usize, capped_height as usize).map(|img_buff| { + let texture = Some(img_buff.into_new_texture(cx)); + img.set_texture(cx, texture); + img.size_in_pixels(cx).unwrap_or_default() + }) + } + Err(e) => { + error!("Failed to decode blurhash {e:?}"); + Err(image_cache::ImageError::EmptyData) + } + } + }); if let Err(e) = show_image_result { let err_str = format!("{body}\n\nFailed to display image: {e:?}"); error!("{err_str}"); text_or_image_ref.show_text(cx, &err_str); } - - // We're done drawing the image, so mark it as fully drawn. - fully_drawn = true; - } - (MediaCacheEntry::Requested, _media_format) => { - // If the image is being fetched, we try to show its blurhash. - if let (Some(ref blurhash), Some(width), Some(height)) = ( - image_info.blurhash.clone(), - image_info.width, - image_info.height, - ) { - let show_image_result = text_or_image_ref.show_image( - cx, - Some(MediaSource::Plain(mxc_uri)), - |cx, img| { - let (Ok(width), Ok(height)) = (width.try_into(), height.try_into()) - else { - return Err(image_cache::ImageError::EmptyData); - }; - let (width, height): (u32, u32) = (width, height); - if width == 0 || height == 0 { - warning!( - "Image had an invalid aspect ratio (width or height of 0)." - ); - return Err(image_cache::ImageError::EmptyData); - } - let aspect_ratio: f32 = width as f32 / height as f32; - // Cap the blurhash to a max size of 500 pixels in each dimension - // because the `blurhash::decode()` function can be rather expensive. - let (mut capped_width, mut capped_height) = (width, height); - if capped_height > BLURHASH_IMAGE_MAX_SIZE { - capped_height = BLURHASH_IMAGE_MAX_SIZE; - capped_width = - (capped_height as f32 * aspect_ratio).floor() as u32; - } - if capped_width > BLURHASH_IMAGE_MAX_SIZE { - capped_width = BLURHASH_IMAGE_MAX_SIZE; - capped_height = - (capped_width as f32 / aspect_ratio).floor() as u32; - } - - match blurhash::decode(blurhash, capped_width, capped_height, 1.0) { - Ok(data) => ImageBuffer::new( - &data, - capped_width as usize, - capped_height as usize, - ) - .map(|img_buff| { - let texture = Some(img_buff.into_new_texture(cx)); - img.set_texture(cx, texture); - img.size_in_pixels(cx).unwrap_or_default() - }), - Err(e) => { - error!("Failed to decode blurhash {e:?}"); - Err(image_cache::ImageError::EmptyData) - } - } - }, - ); - if let Err(e) = show_image_result { - let err_str = format!("{body}\n\nFailed to display image: {e:?}"); - error!("{err_str}"); - text_or_image_ref.show_text(cx, &err_str); - } - } - fully_drawn = false; } - (MediaCacheEntry::Failed(_status_code), _media_format) => { - if text_or_image_ref - .view(cx, ids!(default_image_view)) - .visible() - { - fully_drawn = true; - return; - } - text_or_image_ref.show_text( - cx, - format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri), - ); - // For now, we consider this as being "complete". In the future, we could support - // retrying to fetch thumbnail of the image on a user click/tap. + fully_drawn = false; + } + (MediaCacheEntry::Failed(_status_code), _media_format) => { + if text_or_image_ref.view(cx, ids!(default_image_view)).visible() { fully_drawn = true; + return; } + text_or_image_ref + .show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri)); + // For now, we consider this as being "complete". In the future, we could support + // retrying to fetch thumbnail of the image on a user click/tap. + fully_drawn = true; } - }; + } + }; - let mut fetch_and_show_media_source = - |cx: &mut Cx, media_source: MediaSource, image_info: Box| { - match media_source { - MediaSource::Encrypted(encrypted) => { - // We consider this as "fully drawn" since we don't yet support encryption. - text_or_image_ref.show_text( - cx, - format!( - "{body}\n\n[TODO] fetch encrypted image at {:?}", - encrypted.url - ), - ); - } - MediaSource::Plain(mxc_uri) => fetch_and_show_image_uri(cx, mxc_uri, image_info), + let mut fetch_and_show_media_source = |cx: &mut Cx, media_source: MediaSource, image_info: Box| { + match media_source { + MediaSource::Encrypted(encrypted) => { + // We consider this as "fully drawn" since we don't yet support encryption. + text_or_image_ref.show_text( + cx, + format!("{body}\n\n[TODO] fetch encrypted image at {:?}", encrypted.url) + ); + }, + MediaSource::Plain(mxc_uri) => { + fetch_and_show_image_uri(cx, mxc_uri, image_info) } - }; + } + }; match image_info_source { Some(image_info) => { // Use the provided thumbnail URI if it exists; otherwise use the original URI. - let media_source = image_info - .thumbnail_source - .clone() + let media_source = image_info.thumbnail_source.clone() .unwrap_or(original_source); fetch_and_show_media_source(cx, media_source, image_info); } @@ -4873,6 +4456,7 @@ fn populate_image_message_content( fully_drawn } + /// Draws a file message's content into the given `message_content_widget`. /// /// Returns whether the file message content was fully drawn. @@ -4889,8 +4473,7 @@ fn populate_file_message_content( .and_then(|info| info.size) .map(|bytes| format!(" ({})", ByteSize::b(bytes.into()))) .unwrap_or_default(); - let caption = file_content - .formatted_caption() + let caption = file_content.formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| file_content.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -4917,23 +4500,20 @@ fn populate_audio_message_content( let (duration, mime, size) = audio .info .as_ref() - .map(|info| { - ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - ) - }) + .map(|info| ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + )) .unwrap_or_default(); - let caption = audio - .formatted_caption() + let caption = audio.formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| audio.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -4947,6 +4527,7 @@ fn populate_audio_message_content( true } + /// Draws a video message's content into the given `message_content_widget`. /// /// Returns whether the video message content was fully drawn. @@ -4960,26 +4541,23 @@ fn populate_video_message_content( let (duration, mime, size, dimensions) = video .info .as_ref() - .map(|info| { - ( - info.duration - .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) - .unwrap_or_default(), - info.mimetype - .as_ref() - .map(|m| format!(" {m},")) - .unwrap_or_default(), - info.size - .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) - .unwrap_or_default(), - info.width - .and_then(|width| info.height.map(|height| format!(" {width}x{height},"))) - .unwrap_or_default(), - ) - }) + .map(|info| ( + info.duration + .map(|d| format!(" {:.2} sec,", d.as_secs_f64())) + .unwrap_or_default(), + info.mimetype + .as_ref() + .map(|m| format!(" {m},")) + .unwrap_or_default(), + info.size + .map(|bytes| format!(" ({}),", ByteSize::b(bytes.into()))) + .unwrap_or_default(), + info.width.and_then(|width| + info.height.map(|height| format!(" {width}x{height},")) + ).unwrap_or_default(), + )) .unwrap_or_default(); - let caption = video - .formatted_caption() + let caption = video.formatted_caption() .map(|fb| format!("
{}", fb.body)) .or_else(|| video.caption().map(|c| format!("
{c}"))) .unwrap_or_default(); @@ -4993,6 +4571,8 @@ fn populate_video_message_content( true } + + /// Draws the given location message's content into the `message_content_widget`. /// /// Returns whether the location message content was fully drawn. @@ -5001,9 +4581,8 @@ fn populate_location_message_content( message_content_widget: &HtmlOrPlaintextRef, location: &LocationMessageEventContent, ) -> bool { - let coords = location - .geo_uri - .get(utils::GEO_URI_SCHEME.len()..) + let coords = location.geo_uri + .get(utils::GEO_URI_SCHEME.len() ..) .and_then(|s| { let mut iter = s.split(','); if let (Some(lat), Some(long)) = (iter.next(), iter.next()) { @@ -5013,14 +4592,8 @@ fn populate_location_message_content( } }); if let Some((lat, long)) = coords { - let short_lat = lat - .find('.') - .and_then(|dot| lat.get(..dot + 7)) - .unwrap_or(lat); - let short_long = long - .find('.') - .and_then(|dot| long.get(..dot + 7)) - .unwrap_or(long); + let short_lat = lat.find('.').and_then(|dot| lat.get(..dot + 7)).unwrap_or(lat); + let short_long = long.find('.').and_then(|dot| long.get(..dot + 7)).unwrap_or(long); let safe_lat = htmlize::escape_attribute(lat); let safe_long = htmlize::escape_attribute(long); let safe_geo_uri = htmlize::escape_attribute(&location.geo_uri); @@ -5039,10 +4612,7 @@ fn populate_location_message_content( } else { message_content_widget.show_html( cx, - format!( - "[Location invalid] {}", - htmlize::escape_text(&location.body) - ), + format!("[Location invalid] {}", htmlize::escape_text(&location.body)) ); } @@ -5052,6 +4622,7 @@ fn populate_location_message_content( true } + /// Draws the given redacted message's content into the `message_content_widget`. /// /// Returns whether the redacted message content was fully drawn. @@ -5064,13 +4635,16 @@ fn populate_redacted_message_content( let fully_drawn: bool; let mut redactor_id_and_reason = None; if let Some(redacted_msg) = event_tl_item.latest_json() { - if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncMessageLikeEvent::Redacted(redaction), - ))) = redacted_msg.deserialize() - { + if let Ok(AnySyncTimelineEvent::MessageLike( + AnySyncMessageLikeEvent::RoomMessage( + SyncMessageLikeEvent::Redacted(redaction) + ) + )) = redacted_msg.deserialize() { if let Ok(redacted_because) = redaction.unsigned.redacted_because.deserialize() { - redactor_id_and_reason = - Some((redacted_because.sender, redacted_because.content.reason)); + redactor_id_and_reason = Some(( + redacted_because.sender, + redacted_because.content.reason, + )); } } } @@ -5079,10 +4653,7 @@ fn populate_redacted_message_content( if redactor == event_tl_item.sender() { fully_drawn = true; match reason { - Some(r) => format!( - "⛔ Deleted their own message. Reason: \"{}\".", - htmlize::escape_text(r) - ), + Some(r) => format!("⛔ Deleted their own message. Reason: \"{}\".", htmlize::escape_text(r)), None => String::from("⛔ Deleted their own message."), } } else { @@ -5094,11 +4665,9 @@ fn populate_redacted_message_content( true, ); fully_drawn = redactor_name.was_found(); - let redactor_name_esc = - htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); + let redactor_name_esc = htmlize::escape_text(redactor_name.as_deref().unwrap_or(redactor.as_str())); match reason { - Some(r) => format!( - "⛔ {} deleted this message. Reason: \"{}\".", + Some(r) => format!("⛔ {} deleted this message. Reason: \"{}\".", redactor_name_esc, htmlize::escape_text(r), ), @@ -5113,6 +4682,7 @@ fn populate_redacted_message_content( fully_drawn } + /// Draws a ReplyPreview above a message if it was in-reply to another message. /// /// ## Arguments @@ -5139,24 +4709,24 @@ fn draw_replied_to_message( show_reply = true; match &in_reply_to_details.event { TimelineDetails::Ready(replied_to_event) => { - let (in_reply_to_username, is_avatar_fully_drawn) = replied_to_message_view - .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) - .set_avatar_and_get_username( - cx, - timeline_kind, - &replied_to_event.sender, - Some(&replied_to_event.sender_profile), - Some(in_reply_to_details.event_id.as_ref()), - true, - ); + let (in_reply_to_username, is_avatar_fully_drawn) = + replied_to_message_view + .avatar(cx, ids!(replied_to_message_content.reply_preview_avatar)) + .set_avatar_and_get_username( + cx, + timeline_kind, + &replied_to_event.sender, + Some(&replied_to_event.sender_profile), + Some(in_reply_to_details.event_id.as_ref()), + true, + ); fully_drawn = is_avatar_fully_drawn; replied_to_message_view .label(cx, ids!(replied_to_message_content.reply_preview_username)) .set_text(cx, in_reply_to_username.as_str()); - let msg_body = - replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); + let msg_body = replied_to_message_view.html_or_plaintext(cx, ids!(reply_preview_body)); populate_preview_of_timeline_item( cx, &msg_body, @@ -5268,8 +4838,7 @@ fn populate_thread_root_summary( &embedded_event.content, &embedded_event.sender, sender_username, - ) - .format_with(sender_username, true); + ).format_with(sender_username, true); match utils::replace_linebreaks_separators(&preview, true) { Cow::Borrowed(_) => Cow::Owned(preview), Cow::Owned(replaced) => Cow::Owned(replaced), @@ -5280,11 +4849,9 @@ fn populate_thread_root_summary( if td.is_unavailable() && let Some(thread_root_event_id) = thread_root_event_id.clone() { - let needs_refresh = - fetched_summary.is_none_or(|fs| fs.latest_reply_preview_text.is_none()); - if needs_refresh - && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) - { + let needs_refresh = fetched_summary + .is_none_or(|fs| fs.latest_reply_preview_text.is_none()); + if needs_refresh && pending_thread_summary_fetches.insert(thread_root_event_id.clone()) { submit_async_request(MatrixRequest::FetchThreadSummaryDetails { timeline_kind: timeline_kind.clone(), thread_root_event_id, @@ -5292,8 +4859,7 @@ fn populate_thread_root_summary( }); } } - fetched_summary - .and_then(|fs| fs.latest_reply_preview_text.as_deref()) + fetched_summary.and_then(|fs| fs.latest_reply_preview_text.as_deref()) .unwrap_or("Loading latest reply...") .into() } @@ -5305,7 +4871,7 @@ fn populate_thread_root_summary( let replies_count_text = match replies_count { 1 => Cow::Borrowed("1 reply"), - n => Cow::Owned(format!("{n} replies")), + n => Cow::Owned(format!("{n} replies")) }; item.label(cx, ids!(thread_summary_count)) .set_text(cx, &replies_count_text); @@ -5325,32 +4891,23 @@ pub fn populate_preview_of_timeline_item( ) { if let Some(m) = timeline_item_content.as_message() { match m.msgtype() { - MessageType::Text(TextMessageEventContent { - body, formatted, .. - }) - | MessageType::Notice(NoticeMessageEventContent { - body, formatted, .. - }) => { - let _ = populate_text_message_content( - cx, - widget_out, - body, - formatted.as_ref(), - None, - None, - None, - ); + MessageType::Text(TextMessageEventContent { body, formatted, .. }) + | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { + let _ = populate_text_message_content(cx, widget_out, body, formatted.as_ref(), None, None, None); return; } - _ => {} // fall through to the general case for all timeline items below. + _ => { } // fall through to the general case for all timeline items below. } } - let html = - text_preview_of_timeline_item(timeline_item_content, sender_user_id, sender_username) - .format_with(sender_username, true); + let html = text_preview_of_timeline_item( + timeline_item_content, + sender_user_id, + sender_username, + ).format_with(sender_username, true); widget_out.show_html(cx, html); } + /// A trait for abstracting over the different types of timeline events /// that can be displayed in a `SmallStateEvent` widget. trait SmallStateEventContent { @@ -5441,9 +4998,7 @@ impl SmallStateEventContent for PollState { ) -> (WidgetRef, ItemDrawnStatus) { item.label(cx, ids!(content)).set_text( cx, - self.fallback_text() - .unwrap_or_else(|| self.results().question) - .as_str(), + self.fallback_text().unwrap_or_else(|| self.results().question).as_str(), ); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -5512,15 +5067,20 @@ impl SmallStateEventContent for RoomMembershipChange { ) -> (WidgetRef, ItemDrawnStatus) { let Some(preview) = text_preview_of_room_membership_change(self, false) else { // Don't actually display anything for nonexistent/unimportant membership changes. - return (list.item(cx, item_id, id!(Empty)), ItemDrawnStatus::new()); + return ( + list.item(cx, item_id, id!(Empty)), + ItemDrawnStatus::new(), + ); }; item.label(cx, ids!(content)) .set_text(cx, &preview.format_with(username, false)); // The invite_user_button is only used for "Knocked" membership change events. - item.button(cx, ids!(invite_user_button)) - .set_visible(cx, matches!(self.change(), Some(MembershipChange::Knocked))); + item.button(cx, ids!(invite_user_button)).set_visible( + cx, + matches!(self.change(), Some(MembershipChange::Knocked)), + ); new_drawn_status.content_drawn = true; (item, new_drawn_status) @@ -5572,8 +5132,7 @@ fn populate_small_state_event( ); // Draw the timestamp as part of the profile. if let Some(dt) = unix_time_millis_to_datetime(event_tl_item.timestamp()) { - item.timestamp(cx, ids!(left_container.timestamp)) - .set_date_time(cx, dt); + item.timestamp(cx, ids!(left_container.timestamp)).set_date_time(cx, dt); } new_drawn_status.profile_drawn = profile_drawn; username @@ -5592,6 +5151,7 @@ fn populate_small_state_event( ) } + /// Returns the display name of the sender of the given `event_tl_item`, if available. fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option { if let TimelineDetails::Ready(profile) = event_tl_item.sender_profile() { @@ -5601,6 +5161,7 @@ fn get_profile_display_name(event_tl_item: &EventTimelineItem) -> Option } } + /// Actions related to invites within a room. /// /// These are NOT widget actions, just regular actions. @@ -5635,6 +5196,7 @@ pub enum InviteResultAction { }, } + /// Actions related to a specific message within a room timeline. #[derive(Clone, Default, Debug)] pub enum MessageAction { @@ -5679,6 +5241,7 @@ pub enum MessageAction { // /// The user clicked the "report" button on a message. // Report(MessageDetails), + /// The message at the given item index in the timeline should be highlighted. HighlightMessage(usize), /// The user requested that we show a context menu with actions @@ -5732,8 +5295,7 @@ impl ActionDefaultRef for AppServicePanelAction { #[derive(Script, ScriptHook, Widget)] pub struct AppServicePanel { - #[deref] - view: View, + #[deref] view: View, } impl Widget for AppServicePanel { @@ -5822,15 +5384,11 @@ impl Widget for AppServicePanel { /// A widget representing a single message of any kind within a room timeline. #[derive(Script, ScriptHook, Widget, Animator)] pub struct Message { - #[source] - source: ScriptObjectRef, - #[deref] - view: View, - #[apply_default] - animator: Animator, - - #[rust] - details: Option, + #[source] source: ScriptObjectRef, + #[deref] view: View, + #[apply_default] animator: Animator, + + #[rust] details: Option, } impl Widget for Message { @@ -5845,9 +5403,7 @@ impl Widget for Message { self.animator_play(cx, ids!(highlight.off)); } - let Some(details) = self.details.clone() else { - return; - }; + let Some(details) = self.details.clone() else { return }; // We first handle a click on the replied-to message preview, if present, // because we don't want any widgets within the replied-to message to be @@ -5856,31 +5412,31 @@ impl Widget for Message { Hit::FingerDown(fe) => { if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - }, + } ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - }, + } ); } // If the hit occurred on the replied-to message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::JumpToRelated(details.clone()), ); } - _ => {} + _ => { } } // Handle clicks on the thread summary shown beneath a thread-root message. @@ -5897,11 +5453,11 @@ impl Widget for Message { apply_hover(cx, COLOR_THREAD_SUMMARY_BG_HOVER); if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - }, + } ); } } @@ -5913,23 +5469,23 @@ impl Widget for Message { } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - }, + } ); } Hit::FingerUp(fe) => { apply_hover(cx, COLOR_THREAD_SUMMARY_BG); if fe.is_over && fe.is_primary_hit() && fe.was_tap() { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenThread(thread_root_event_id.clone()), ); } } - _ => {} + _ => { } } } @@ -5948,21 +5504,21 @@ impl Widget for Message { // A right click means we should display the context menu. if fe.device.mouse_button().is_some_and(|b| b.is_secondary()) { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: fe.abs, - }, + } ); } } Hit::FingerLongPress(lp) => { cx.widget_action( - details.room_screen_widget_uid, + details.room_screen_widget_uid, MessageAction::OpenMessageContextMenu { details: details.clone(), abs_pos: lp.abs, - }, + } ); } Hit::FingerHoverIn(..) => { @@ -5973,16 +5529,12 @@ impl Widget for Message { self.animator_play(cx, ids!(hover.off)); // TODO: here, hide the "action bar" buttons upon hover-out } - _ => {} + _ => { } } if let Event::Actions(actions) = event { for action in actions { - match action - .as_widget_action() - .widget_uid_eq(details.room_screen_widget_uid) - .cast_ref() - { + match action.as_widget_action().widget_uid_eq(details.room_screen_widget_uid).cast_ref() { MessageAction::HighlightMessage(id) if id == &details.item_id => { self.animator_play(cx, ids!(highlight.on)); self.redraw(cx); @@ -5994,11 +5546,7 @@ impl Widget for Message { } fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - if self - .details - .as_ref() - .is_some_and(|d| d.should_be_highlighted) - { + if self.details.as_ref().is_some_and(|d| d.should_be_highlighted) { script_apply_eval!(cx, self, { draw_bg +: { color: #ffffd1, @@ -6019,9 +5567,7 @@ impl Message { impl MessageRef { fn set_data(&self, details: MessageDetails) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; inner.set_data(details); } } @@ -6030,7 +5576,7 @@ impl MessageRef { /// /// This function requires passing in a reference to `Cx`, /// which isn't used, but acts as a guarantee that this function -/// must only be called by the main UI thread. +/// must only be called by the main UI thread. pub fn clear_timeline_states(_cx: &mut Cx) { // Clear timeline states cache TIMELINE_STATES.with_borrow_mut(|states| { diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 0b1ae8c77..73ce9375e 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -16,50 +16,30 @@ //! so you can use it from other widgets or functions on the main UI thread //! that need to query basic info about a particular room or space. -use std::{ - cell::RefCell, - collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, - rc::Rc, - sync::Arc, -}; +use std::{cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, rc::Rc, sync::Arc}; use crossbeam_queue::SegQueue; use makepad_widgets::*; use matrix_sdk_ui::spaces::room_list::SpaceRoomListPaginationState; use ruma::events::tag::TagName; use tokio::sync::mpsc::UnboundedSender; -use matrix_sdk::{ - RoomState, - ruma::{ - events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, - }, -}; +use matrix_sdk::{RoomState, ruma::{events::tag::Tags, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId, OwnedRoomId, OwnedUserId}}; use crate::{ app::{AppState, SelectedRoom}, home::{ - navigation_tab_bar::{NavigationBarAction, SelectedTab}, - room_context_menu::RoomContextMenuDetails, - rooms_list_entry::RoomsListEntryAction, - space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt}, + navigation_tab_bar::{NavigationBarAction, SelectedTab}, room_context_menu::RoomContextMenuDetails, rooms_list_entry::RoomsListEntryAction, space_lobby::{SpaceLobbyAction, SpaceLobbyEntryWidgetExt} }, room::{ FetchedRoomAvatar, - room_display_filter::{ - RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn, - }, + room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria, SortFn}, }, shared::{ - collapsible_header::{ - CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory, - }, + collapsible_header::{CollapsibleHeaderAction, CollapsibleHeaderWidgetRefExt, HeaderCategory}, jump_to_bottom_button::UnreadMessageCount, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction, }, - sliding_sync::{ - MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request, - }, - space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, - utils::{RoomNameId, VecDiff}, + sliding_sync::{MatrixLinkAction, MatrixRequest, PaginationDirection, TimelineKind, submit_async_request}, + space_service_sync::{ParentChain, SpaceRequest, SpaceRoomListAction}, utils::{RoomNameId, VecDiff}, }; /// Whether to pre-paginate visible rooms at least once in order to @@ -91,10 +71,11 @@ pub fn get_invited_rooms(_cx: &mut Cx) -> Rc }, + LoadedRooms{ max_rooms: Option }, /// Add a new room to the list of rooms the user has been invited to. /// This will be maintained and displayed separately from joined rooms. AddInvitedRoom(InvitedRoomInfo), @@ -189,7 +171,9 @@ pub enum RoomsListUpdate { unread_mentions: u64, }, /// Update the displayable name for the given room. - UpdateRoomName { new_room_name: RoomNameId }, + UpdateRoomName { + new_room_name: RoomNameId, + }, /// Update the avatar (image) for the given room. UpdateRoomAvatar { room_id: OwnedRoomId, @@ -212,15 +196,21 @@ pub enum RoomsListUpdate { new_tags: Tags, }, /// Update the status label at the bottom of the list of all rooms. - Status { status: String }, + Status { + status: String, + }, /// Mark the given room as tombstoned. - TombstonedRoom { room_id: OwnedRoomId }, + TombstonedRoom { + room_id: OwnedRoomId + }, /// Hide the given room from being displayed. /// /// This is useful for temporarily preventing a room from being shown, /// e.g., after a room has been left but before the homeserver has registered /// that we left it and removed it via the RoomListService. - HideRoom { room_id: OwnedRoomId }, + HideRoom { + room_id: OwnedRoomId, + }, /// Scroll to the given room. ScrollToRoom(OwnedRoomId), /// The background space service is now listening for requests, @@ -247,7 +237,9 @@ pub enum RoomsListAction { /// A new room was joined from an accepted invite, /// meaning that the existing `InviteScreen` should be converted /// to a `RoomScreen` to display the now-joined room. - InviteAccepted { room_name_id: RoomNameId }, + InviteAccepted { + room_name_id: RoomNameId, + }, /// Instructs the top-level app to show the context menu for the given room. /// /// Emitted by the RoomsList when the user right-clicks or long-presses @@ -267,6 +259,7 @@ impl ActionDefaultRef for RoomsListAction { } } + /// UI-related info about a joined room. /// /// This includes info needed display a preview of that room in the RoomsList @@ -305,6 +298,7 @@ pub struct JoinedRoomInfo { pub is_direct: bool, /// Whether this room is tombstoned (shut down and replaced with a successor room). pub is_tombstoned: bool, + // TODO: we could store the parent chain(s) of this room, i.e., which spaces // they are children of. One room can be in multiple spaces. } @@ -396,34 +390,28 @@ struct SpaceMapValue { #[derive(Script, Widget)] pub struct RoomsList { - #[deref] - view: View, + #[deref] view: View, /// The list of all rooms that the user has been invited to. /// /// This is a shared reference to the thread-local [`ALL_INVITED_ROOMS`] variable. - #[rust] - invited_rooms: Rc>>, + #[rust] invited_rooms: Rc>>, /// The set of all joined rooms and their cached info. /// This includes both direct rooms and regular rooms, but not invited rooms. - #[rust] - all_joined_rooms: HashMap, + #[rust] all_joined_rooms: HashMap, /// The list of all room IDs in display order, matching the order from the room list service. - #[rust] - all_known_rooms_order: VecDeque, + #[rust] all_known_rooms_order: VecDeque, /// The space that is currently selected as a display filter for the rooms list, if any. /// * If `None` (default), no space is selected, and all rooms can be shown. /// * If `Some`, the rooms list is in "space" mode. A special "Space Lobby" entry /// is shown at the top, and only child rooms within this space will be displayed. - #[rust] - selected_space: Option, + #[rust] selected_space: Option, /// The sender used to send Space-related requests to the background service. - #[rust] - space_request_sender: Option>, + #[rust] space_request_sender: Option>, /// A flattened map of all spaces known to the client. /// @@ -431,66 +419,50 @@ pub struct RoomsList { /// and nested subspaces *directly* within that space. /// /// This can include both joined and non-joined spaces. - #[rust] - space_map: HashMap, + #[rust] space_map: HashMap, /// Rooms that are explicitly hidden and should never be shown in the rooms list. - #[rust] - hidden_rooms: HashSet, + #[rust] hidden_rooms: HashSet, /// The currently-active filter function for the list of rooms. /// /// ## Important Notes /// 1. Do not use this directly. Instead, use the `should_display_room!()` macro. /// 2. This does *not* get auto-applied when it changes, for performance reasons. - #[rust] - display_filter: RoomDisplayFilter, + #[rust] display_filter: RoomDisplayFilter, /// The currently-active sort function for the list of rooms. - #[rust] - sort_fn: Option>, + #[rust] sort_fn: Option>, /// The list of invited rooms currently displayed in the UI. - #[rust] - displayed_invited_rooms: Vec, - #[rust(false)] - is_invited_rooms_header_expanded: bool, - #[rust] - invited_rooms_indexes: RoomCategoryIndexes, + #[rust] displayed_invited_rooms: Vec, + #[rust(false)] is_invited_rooms_header_expanded: bool, + #[rust] invited_rooms_indexes: RoomCategoryIndexes, /// The list of direct rooms currently displayed in the UI. - #[rust] - displayed_direct_rooms: Vec, - #[rust(false)] - is_direct_rooms_header_expanded: bool, - #[rust] - direct_rooms_indexes: RoomCategoryIndexes, + #[rust] displayed_direct_rooms: Vec, + #[rust(false)] is_direct_rooms_header_expanded: bool, + #[rust] direct_rooms_indexes: RoomCategoryIndexes, /// The list of regular (non-direct) joined rooms currently displayed in the UI. /// /// **Direct rooms are excluded** from this; they are in `displayed_direct_rooms`. - #[rust] - displayed_regular_rooms: Vec, - #[rust(true)] - is_regular_rooms_header_expanded: bool, - #[rust] - regular_rooms_indexes: RoomCategoryIndexes, + #[rust] displayed_regular_rooms: Vec, + #[rust(true)] is_regular_rooms_header_expanded: bool, + #[rust] regular_rooms_indexes: RoomCategoryIndexes, /// The latest status message that should be displayed in the bottom status label. - #[rust] - status: String, + #[rust] status: String, /// The currently-selected room. - #[rust] - current_active_room: Option, + #[rust] current_active_room: Option, /// The maximum number of rooms that will ever be loaded. /// /// This should not be used to determine whether all requested rooms have been loaded, /// because we will likely never receive this many rooms due to the room list service /// excluding rooms that we have filtered out (e.g., left or tombstoned rooms, spaces, etc). - #[rust] - max_known_rooms: Option, + #[rust] max_known_rooms: Option, // /// Whether the room list service has loaded all requested rooms from the homeserver. // #[rust] all_rooms_loaded: bool, } @@ -513,16 +485,15 @@ macro_rules! should_display_room { ($self:expr, $room_id:expr, $room:expr) => { !$self.hidden_rooms.contains($room_id) && ($self.display_filter)($room) - && $self - .selected_space - .as_ref() + && $self.selected_space.as_ref() .is_none_or(|space| $self.is_room_indirectly_in_space(space.room_id(), $room_id)) }; } + impl RoomsList { /// Returns whether the homeserver has finished syncing all of the rooms - /// that should be synced to our client based on the currently-specified room list filter. + /// that should be synced to our client based on the currently-specified room list filter. pub fn all_rooms_loaded(&self) -> bool { // TODO: fix this: figure out a way to determine if // all requested rooms have been received from the homeserver. @@ -551,10 +522,7 @@ impl RoomsList { RoomsListUpdate::AddInvitedRoom(invited_room) => { let room_id = invited_room.room_name_id.room_id().clone(); let should_display = should_display_room!(self, &room_id, &invited_room); - let _replaced = self - .invited_rooms - .borrow_mut() - .insert(room_id.clone(), invited_room); + let _replaced = self.invited_rooms.borrow_mut().insert(room_id.clone(), invited_room); if should_display { self.displayed_invited_rooms.push(room_id); } @@ -580,29 +548,24 @@ impl RoomsList { // 3. Emit an action to inform other widgets that the InviteScreen // displaying the invite to this room should be converted to a // RoomScreen displaying the now-joined room. - if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) - { + if let Some(_accepted_invite) = self.invited_rooms.borrow_mut().remove(&room_id) { log!("Removed room {room_id} from the list of invited rooms"); - self.displayed_invited_rooms - .iter() + self.displayed_invited_rooms.iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); if let Some(room) = self.all_joined_rooms.get(&room_id) { cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::InviteAccepted { room_name_id: room.room_name_id.clone(), - }, + } ); } } self.update_status(); SignalToUI::set_ui_signal(); // signal the RoomScreen to update itself } - RoomsListUpdate::UpdateRoomAvatar { - room_id, - room_avatar, - } => { + RoomsListUpdate::UpdateRoomAvatar { room_id, room_avatar } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.room_avatar = room_avatar; } else if let Some(room) = self.invited_rooms.borrow_mut().get_mut(&room_id) { @@ -611,23 +574,14 @@ impl RoomsList { error!("Error: couldn't find room {room_id} to update avatar"); } } - RoomsListUpdate::UpdateLatestEvent { - room_id, - timestamp, - latest_message_text, - } => { + RoomsListUpdate::UpdateLatestEvent { room_id, timestamp, latest_message_text } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.latest = Some((timestamp, latest_message_text)); } else { error!("Error: couldn't find room {room_id} to update latest event"); } } - RoomsListUpdate::UpdateNumUnreadMessages { - room_id, - is_marked_unread, - unread_messages, - unread_mentions, - } => { + RoomsListUpdate::UpdateNumUnreadMessages { room_id, is_marked_unread, unread_messages, unread_mentions } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.num_unread_messages = match unread_messages { UnreadMessageCount::Unknown => 0, @@ -636,13 +590,11 @@ impl RoomsList { room.num_unread_mentions = unread_mentions; room.is_marked_unread = is_marked_unread; } else { - warning!( - "Warning: couldn't find room {} to update unread messages count", - room_id - ); + warning!("Warning: couldn't find room {} to update unread messages count", room_id); } } RoomsListUpdate::UpdateRoomName { new_room_name } => { + // TODO: broadcast a new AppState action to ensure that this room's or space's new name // gets updated in all of the `SelectedRoom` instances throughout Robrix, // e.g., the name of the room in the Dock Tab or the StackNav header. @@ -655,16 +607,12 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms - .iter() - .position(|r| r == &room_id), + self.displayed_direct_rooms.iter().position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms - .iter() - .position(|r| r == &room_id), + self.displayed_regular_rooms.iter().position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -682,9 +630,7 @@ impl RoomsList { if let Some(invited_room) = invited_rooms.get_mut(&room_id) { invited_room.room_name_id = new_room_name; let should_display = should_display_room!(self, &room_id, invited_room); - let pos_in_list = self - .displayed_invited_rooms - .iter() + let pos_in_list = self.displayed_invited_rooms.iter() .position(|r| r == &room_id); if should_display { if pos_in_list.is_none() { @@ -694,9 +640,7 @@ impl RoomsList { pos_in_list.map(|i| self.displayed_invited_rooms.remove(i)); } } else { - warning!( - "Warning: couldn't find room {new_room_name} to update its name." - ); + warning!("Warning: couldn't find room {new_room_name} to update its name."); } } } @@ -707,8 +651,7 @@ impl RoomsList { continue; } enqueue_popup_notification( - format!( - "{} was changed from {} to {}.", + format!("{} was changed from {} to {}.", room.room_name_id, if room.is_direct { "direct" } else { "regular" }, if is_direct { "direct" } else { "regular" } @@ -723,8 +666,7 @@ impl RoomsList { } else { &mut self.displayed_regular_rooms }; - list_to_remove_from - .iter() + list_to_remove_from.iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); @@ -748,23 +690,19 @@ impl RoomsList { // and then options/buttons for the user to re-join it if desired. if let Some(removed) = self.all_joined_rooms.remove(&room_id) { - log!( - "Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}" - ); + log!("Removed room {room_id} from the list of all joined rooms, now has state {new_state:?}"); let list_to_remove_from = if removed.is_direct { &mut self.displayed_direct_rooms } else { &mut self.displayed_regular_rooms }; - list_to_remove_from - .iter() + list_to_remove_from.iter() .position(|r| r == &room_id) .map(|index| list_to_remove_from.remove(index)); - } else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) - { + } + else if let Some(_removed) = self.invited_rooms.borrow_mut().remove(&room_id) { log!("Removed room {room_id} from the list of all invited rooms"); - self.displayed_invited_rooms - .iter() + self.displayed_invited_rooms.iter() .position(|r| r == &room_id) .map(|index| self.displayed_invited_rooms.remove(index)); } @@ -785,7 +723,7 @@ impl RoomsList { } RoomsListUpdate::LoadedRooms { max_rooms } => { self.max_known_rooms = max_rooms; - } + }, RoomsListUpdate::Tags { room_id, new_tags } => { if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.tags = new_tags; @@ -805,16 +743,12 @@ impl RoomsList { let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { ( - self.displayed_direct_rooms - .iter() - .position(|r| r == &room_id), + self.displayed_direct_rooms.iter().position(|r| r == &room_id), &mut self.displayed_direct_rooms, ) } else { ( - self.displayed_regular_rooms - .iter() - .position(|r| r == &room_id), + self.displayed_regular_rooms.iter().position(|r| r == &room_id), &mut self.displayed_regular_rooms, ) }; @@ -826,32 +760,20 @@ impl RoomsList { pos_in_list.map(|i| displayed_list.remove(i)); } } else { - warning!( - "Warning: couldn't find room {room_id} to update the tombstone status" - ); + warning!("Warning: couldn't find room {room_id} to update the tombstone status"); } } RoomsListUpdate::HideRoom { room_id } => { self.hidden_rooms.insert(room_id.clone()); // Hiding a regular room is the most common case (e.g., after its successor is joined), // so we check that list first. - if let Some(i) = self - .displayed_regular_rooms - .iter() - .position(|r| r == &room_id) - { + if let Some(i) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { self.displayed_regular_rooms.remove(i); - } else if let Some(i) = self - .displayed_direct_rooms - .iter() - .position(|r| r == &room_id) - { + } + else if let Some(i) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { self.displayed_direct_rooms.remove(i); - } else if let Some(i) = self - .displayed_invited_rooms - .iter() - .position(|r| r == &room_id) - { + } + else if let Some(i) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { self.displayed_invited_rooms.remove(i); } } @@ -860,89 +782,75 @@ impl RoomsList { self.recalculate_indexes(); let portal_list = self.view.portal_list(cx, ids!(list)); let speed = 50.0; - let portal_list_index = if let Some(regular_index) = self - .displayed_regular_rooms - .iter() - .position(|r| r == &room_id) - { + let portal_list_index = if let Some(regular_index) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { self.regular_rooms_indexes.first_room_index + regular_index - } else if let Some(direct_index) = self - .displayed_direct_rooms - .iter() - .position(|r| r == &room_id) - { + } + else if let Some(direct_index) = self.displayed_direct_rooms.iter().position(|r| r == &room_id) { self.direct_rooms_indexes.first_room_index + direct_index - } else if let Some(invited_index) = self - .displayed_invited_rooms - .iter() - .position(|r| r == &room_id) - { + } + else if let Some(invited_index) = self.displayed_invited_rooms.iter().position(|r| r == &room_id) { self.invited_rooms_indexes.first_room_index + invited_index - } else { - continue; - }; + } + else { continue }; // Scroll to just above the room to make it more obviously visible. - portal_list.smooth_scroll_to( - cx, - portal_list_index.saturating_sub(1), - speed, - Some(15), - ); + portal_list.smooth_scroll_to(cx, portal_list_index.saturating_sub(1), speed, Some(15)); } RoomsListUpdate::SpaceRequestSender(sender) => { self.space_request_sender = Some(sender); - num_updates -= 1; // this does not require a redraw. + num_updates -= 1; // this does not require a redraw. } - RoomsListUpdate::RoomOrderUpdate(diff) => match diff { - VecDiff::Append { values } => { - self.all_known_rooms_order.extend(values); - needs_sort = true; - } - VecDiff::Clear => { - self.all_known_rooms_order.clear(); - needs_sort = true; - } - VecDiff::PushFront { value } => { - self.all_known_rooms_order.push_front(value); - needs_sort = true; - } - VecDiff::PushBack { value } => { - self.all_known_rooms_order.push_back(value); - needs_sort = true; - } - VecDiff::PopFront => { - self.all_known_rooms_order.pop_front(); - needs_sort = true; - } - VecDiff::PopBack => { - self.all_known_rooms_order.pop_back(); - needs_sort = true; - } - VecDiff::Insert { index, value } => { - if index <= self.all_known_rooms_order.len() { - self.all_known_rooms_order.insert(index, value); + RoomsListUpdate::RoomOrderUpdate(diff) => { + match diff { + VecDiff::Append { values } => { + self.all_known_rooms_order.extend(values); needs_sort = true; } - } - VecDiff::Set { index, value } => { - if let Some(existing) = self.all_known_rooms_order.get_mut(index) { - if *existing != value { - *existing = value; + VecDiff::Clear => { + self.all_known_rooms_order.clear(); + needs_sort = true; + } + VecDiff::PushFront { value } => { + self.all_known_rooms_order.push_front(value); + needs_sort = true; + } + VecDiff::PushBack { value } => { + self.all_known_rooms_order.push_back(value); + needs_sort = true; + } + VecDiff::PopFront => { + self.all_known_rooms_order.pop_front(); + needs_sort = true; + } + VecDiff::PopBack => { + self.all_known_rooms_order.pop_back(); + needs_sort = true; + } + VecDiff::Insert { index, value } => { + if index <= self.all_known_rooms_order.len() { + self.all_known_rooms_order.insert(index, value); needs_sort = true; } } - } - VecDiff::Remove { index } => { - if index < self.all_known_rooms_order.len() { - self.all_known_rooms_order.remove(index); + VecDiff::Set { index, value } => { + if let Some(existing) = self.all_known_rooms_order.get_mut(index) { + if *existing != value { + *existing = value; + needs_sort = true; + } + } + } + VecDiff::Remove { index } => { + if index < self.all_known_rooms_order.len() { + self.all_known_rooms_order.remove(index); + needs_sort = true; + } + } + VecDiff::Truncate { length } => { + self.all_known_rooms_order.truncate(length); needs_sort = true; } } - VecDiff::Truncate { length } => { - self.all_known_rooms_order.truncate(length); - needs_sort = true; - } - }, + } } } if needs_sort { @@ -967,9 +875,9 @@ impl RoomsList { + self.displayed_regular_rooms.len(); let mut text = match (self.display_filter.is_none(), num_rooms) { - (true, 0) => "No joined or invited rooms found".to_string(), - (true, 1) => "Loaded 1 room".to_string(), - (true, n) => format!("Loaded {n} rooms"), + (true, 0) => "No joined or invited rooms found".to_string(), + (true, 1) => "Loaded 1 room".to_string(), + (true, n) => format!("Loaded {n} rooms"), (false, 0) => "No matching rooms found".to_string(), (false, 1) => "Found 1 matching room".to_string(), (false, n) => format!("Found {n} matching rooms"), @@ -1018,6 +926,7 @@ impl RoomsList { self.redraw(cx); } + /// Generates a tuple of three kinds of displayed rooms (accounting for the current `display_filter`): /// 1. displayed_invited_rooms /// 2. displayed_regular_rooms @@ -1025,7 +934,7 @@ impl RoomsList { /// /// If `self.sort_fn` is `Some`, the rooms are ordered based on that function. /// Otherwise, the rooms are ordered based on `self.all_known_rooms_order` (the default). - fn generate_displayed_rooms(&self) -> (Vec, Vec, Vec) { + fn generate_displayed_rooms(&self) -> (Vec,Vec, Vec) { let mut new_displayed_invited_rooms = Vec::new(); let mut new_displayed_regular_rooms = Vec::new(); let mut new_displayed_direct_rooms = Vec::new(); @@ -1043,9 +952,7 @@ impl RoomsList { // If a sort function was provided, use it. if let Some(sort_fn) = self.sort_fn.as_deref() { - let mut filtered_joined_rooms = self - .all_joined_rooms - .iter() + let mut filtered_joined_rooms = self.all_joined_rooms.iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_joined_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -1053,8 +960,7 @@ impl RoomsList { push_joined_room(room_id, jr) } - let mut filtered_invited_rooms = invited_rooms_ref - .iter() + let mut filtered_invited_rooms = invited_rooms_ref.iter() .filter(|&(room_id, room)| should_display_room!(self, room_id, room)) .collect::>(); filtered_invited_rooms.sort_by(|(_, room_a), (_, room_b)| sort_fn(*room_a, *room_b)); @@ -1077,11 +983,7 @@ impl RoomsList { } } - ( - new_displayed_invited_rooms, - new_displayed_regular_rooms, - new_displayed_direct_rooms, - ) + (new_displayed_invited_rooms, new_displayed_regular_rooms, new_displayed_direct_rooms) } /// Calculates the indexes in the PortalList where the headers and rooms should be drawn. @@ -1094,35 +996,35 @@ impl RoomsList { // Based on the various displayed room lists and is_expanded state of each room header, // calculate the indexes in the PortalList where the headers and rooms should be drawn. let should_show_invited_rooms_header = !self.displayed_invited_rooms.is_empty(); - let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); + let should_show_direct_rooms_header = !self.displayed_direct_rooms.is_empty(); let should_show_regular_rooms_header = !self.displayed_regular_rooms.is_empty(); let index_of_invited_rooms_header = should_show_invited_rooms_header.then_some(0); let index_of_first_invited_room = should_show_invited_rooms_header as usize; - let index_after_invited_rooms = index_of_first_invited_room - + if self.is_invited_rooms_header_expanded { + let index_after_invited_rooms = index_of_first_invited_room + + if self.is_invited_rooms_header_expanded { self.displayed_invited_rooms.len() } else { 0 }; - let index_of_direct_rooms_header = - should_show_direct_rooms_header.then_some(index_after_invited_rooms); - let index_of_first_direct_room = - index_after_invited_rooms + should_show_direct_rooms_header as usize; - let index_after_direct_rooms = index_of_first_direct_room - + if self.is_direct_rooms_header_expanded { + let index_of_direct_rooms_header = should_show_direct_rooms_header + .then_some(index_after_invited_rooms); + let index_of_first_direct_room = index_after_invited_rooms + + should_show_direct_rooms_header as usize; + let index_after_direct_rooms = index_of_first_direct_room + + if self.is_direct_rooms_header_expanded { self.displayed_direct_rooms.len() } else { 0 }; - let index_of_regular_rooms_header = - should_show_regular_rooms_header.then_some(index_after_direct_rooms); - let index_of_first_regular_room = - index_after_direct_rooms + should_show_regular_rooms_header as usize; - let index_after_regular_rooms = index_of_first_regular_room - + if self.is_regular_rooms_header_expanded { + let index_of_regular_rooms_header = should_show_regular_rooms_header + .then_some(index_after_direct_rooms); + let index_of_first_regular_room = index_after_direct_rooms + + should_show_regular_rooms_header as usize; + let index_after_regular_rooms = index_of_first_regular_room + + if self.is_regular_rooms_header_expanded { self.displayed_regular_rooms.len() } else { 0 @@ -1148,43 +1050,32 @@ impl RoomsList { /// Handle any incoming updates to spaces' room lists and pagination state. fn handle_space_room_list_action(&mut self, cx: &mut Cx, action: &SpaceRoomListAction) { match action { - SpaceRoomListAction::UpdatedChildren { - space_id, - parent_chain, - direct_child_rooms, - direct_subspaces, - } => { + SpaceRoomListAction::UpdatedChildren { space_id, parent_chain, direct_child_rooms, direct_subspaces } => { match self.space_map.entry(space_id.clone()) { Entry::Occupied(mut occ) => { let occ_mut = occ.get_mut(); occ_mut.parent_chain = parent_chain.clone(); occ_mut.direct_child_rooms = Arc::clone(direct_child_rooms); - occ_mut.direct_subspaces = Arc::clone(direct_subspaces); + occ_mut.direct_subspaces = Arc::clone(direct_subspaces); } Entry::Vacant(vac) => { vac.insert_entry(SpaceMapValue { is_fully_paginated: false, parent_chain: parent_chain.clone(), direct_child_rooms: Arc::clone(direct_child_rooms), - direct_subspaces: Arc::clone(direct_subspaces), + direct_subspaces: Arc::clone(direct_subspaces), }); } } - if self.selected_space.as_ref().is_some_and(|sel_space| { - sel_space.room_id() == space_id || parent_chain.contains(sel_space.room_id()) - }) { + if self.selected_space.as_ref().is_some_and(|sel_space| + sel_space.room_id() == space_id + || parent_chain.contains(sel_space.room_id()) + ) { self.update_displayed_rooms(cx, false); } } - SpaceRoomListAction::PaginationState { - space_id, - parent_chain, - state, - } => { - let is_fully_paginated = matches!( - state, - SpaceRoomListPaginationState::Idle { end_reached: true } - ); + SpaceRoomListAction::PaginationState { space_id, parent_chain, state } => { + let is_fully_paginated = matches!(state, SpaceRoomListPaginationState::Idle { end_reached: true }); // Only re-fetch the list of rooms in this space if it was not already fully paginated. let should_fetch_rooms: bool; match self.space_map.entry(space_id.clone()) { @@ -1203,22 +1094,15 @@ impl RoomsList { } } let Some(sender) = self.space_request_sender.as_ref() else { - error!( - "BUG: RoomsList: no space request sender was available after pagination state update." - ); + error!("BUG: RoomsList: no space request sender was available after pagination state update."); return; }; if should_fetch_rooms { - if sender - .send(SpaceRequest::GetChildren { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }) - .is_err() - { - error!( - "BUG: RoomsList: failed to send GetRooms request for space {space_id}." - ); + if sender.send(SpaceRequest::GetChildren { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }).is_err() { + error!("BUG: RoomsList: failed to send GetRooms request for space {space_id}."); } } @@ -1228,16 +1112,11 @@ impl RoomsList { // all of its children, such that we can see if any of them are subspaces, // and then we'll paginate those as well. if !is_fully_paginated { - if sender - .send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_id.clone(), - parent_chain: parent_chain.clone(), - }) - .is_err() - { - error!( - "BUG: RoomsList: failed to send pagination request for space {space_id}." - ); + if sender.send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_id.clone(), + parent_chain: parent_chain.clone(), + }).is_err() { + error!("BUG: RoomsList: failed to send pagination request for space {space_id}."); } } } @@ -1249,10 +1128,7 @@ impl RoomsList { None, ); } - SpaceRoomListAction::LeaveSpaceResult { - space_name_id, - result, - } => match result { + SpaceRoomListAction::LeaveSpaceResult { space_name_id, result } => match result { Ok(()) => { enqueue_popup_notification( format!("Successfully left space \"{}\".", space_name_id), @@ -1260,11 +1136,7 @@ impl RoomsList { Some(4.0), ); // If the space we left was the currently-selected one, go back to the main Home view. - if self - .selected_space - .as_ref() - .is_some_and(|s| s.room_id() == space_name_id.room_id()) - { + if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { cx.action(NavigationBarAction::GoToHome); } } @@ -1279,18 +1151,14 @@ impl RoomsList { }, // Details-related space actions are handled by SpaceLobbyScreen, not RoomsList. SpaceRoomListAction::DetailedChildren { .. } - | SpaceRoomListAction::TopLevelSpaceDetails(_) => {} + | SpaceRoomListAction::TopLevelSpaceDetails(_) => { } } } /// Returns whether the given target room or space is indirectly within the given parent space. /// /// This will recursively search all nested spaces within the given `parent_space`. - fn is_room_indirectly_in_space( - &self, - parent_space: &OwnedRoomId, - target: &OwnedRoomId, - ) -> bool { + fn is_room_indirectly_in_space(&self, parent_space: &OwnedRoomId, target: &OwnedRoomId) -> bool { if let Some(smv) = self.space_map.get(parent_space) { if smv.direct_child_rooms.contains(target) { return true; @@ -1318,14 +1186,12 @@ impl Widget for RoomsList { let props = RoomsListScopeProps { was_scrolling: self.view.portal_list(cx, ids!(list)).was_scrolling(), }; - let rooms_list_actions = cx.capture_actions(|cx| { - self.view - .handle_event(cx, event, &mut Scope::with_props(&props)) - }); + let rooms_list_actions = cx.capture_actions( + |cx| self.view.handle_event(cx, event, &mut Scope::with_props(&props)) + ); for action in rooms_list_actions { // Handle a regular room (joined or invited) being clicked. - if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() - { + if let RoomsListEntryAction::PrimaryClicked(room_id) = action.as_widget_action().cast() { let new_selected_room = if let Some(jr) = self.all_joined_rooms.get(&room_id) { SelectedRoom::JoinedRoom { room_name_id: jr.room_name_id.clone(), @@ -1341,15 +1207,13 @@ impl Widget for RoomsList { self.current_active_room = Some(new_selected_room.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_room), ); self.redraw(cx); } // Handle a room being right-clicked or long-pressed by opening the room context menu. - else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = - action.as_widget_action().cast() - { + else if let RoomsListEntryAction::SecondaryClicked(room_id, pos) = action.as_widget_action().cast() { // Determine details for the context menu let Some(jr) = self.all_joined_rooms.get(&room_id) else { error!("BUG: couldn't find right-clicked room details for room {room_id}"); @@ -1365,35 +1229,29 @@ impl Widget for RoomsList { is_bot_bound: app_state.bot_settings.is_room_bound(&room_id), }; cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::OpenRoomContextMenu { details, pos }, ); } // Handle the space lobby being clicked. else if let Some(SpaceLobbyAction::SpaceLobbyEntryClicked) = action.downcast_ref() { - let Some(space_name_id) = self.selected_space.clone() else { - continue; - }; + let Some(space_name_id) = self.selected_space.clone() else { continue }; let new_selected_space = SelectedRoom::Space { space_name_id }; self.current_active_room = Some(new_selected_space.clone()); cx.widget_action( - self.widget_uid(), + self.widget_uid(), RoomsListAction::Selected(new_selected_space), ); self.redraw(cx); } // Handle a collapsible header being clicked. - else if let CollapsibleHeaderAction::Toggled { category } = - action.as_widget_action().cast() - { + else if let CollapsibleHeaderAction::Toggled { category } = action.as_widget_action().cast() { match category { HeaderCategory::Invites => { - self.is_invited_rooms_header_expanded = - !self.is_invited_rooms_header_expanded; + self.is_invited_rooms_header_expanded = !self.is_invited_rooms_header_expanded; } HeaderCategory::RegularRooms => { - self.is_regular_rooms_header_expanded = - !self.is_regular_rooms_header_expanded; + self.is_regular_rooms_header_expanded = !self.is_regular_rooms_header_expanded; } HeaderCategory::DirectRooms => { self.is_direct_rooms_header_expanded = @@ -1418,73 +1276,47 @@ impl Widget for RoomsList { if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { match tab { SelectedTab::Space { space_name_id } => { - if self - .selected_space - .as_ref() - .is_some_and(|s| s.room_id() == space_name_id.room_id()) - { + if self.selected_space.as_ref().is_some_and(|s| s.room_id() == space_name_id.room_id()) { continue; } self.selected_space = Some(space_name_id.clone()); - self.view - .space_lobby_entry(cx, ids!(space_lobby_entry)) - .set_visible(cx, true); + self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, true); // If we don't have the full list of children in this newly-selected space, then fetch it. - let (is_fully_paginated, parent_chain) = self - .space_map + let (is_fully_paginated, parent_chain) = self.space_map .get(space_name_id.room_id()) .map(|smv| (smv.is_fully_paginated, smv.parent_chain.clone())) .unwrap_or_default(); if !is_fully_paginated { let Some(sender) = self.space_request_sender.as_ref() else { - error!( - "BUG: RoomsList: no space request sender was available." - ); + error!("BUG: RoomsList: no space request sender was available."); continue; }; - if sender - .send(SpaceRequest::SubscribeToSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }) - .is_err() - { - error!( - "BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}." - ); + if sender.send(SpaceRequest::SubscribeToSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }).is_err() { + error!("BUG: RoomsList: failed to send SubscribeToSpaceRoomList request for space {space_name_id}."); } - if sender - .send(SpaceRequest::PaginateSpaceRoomList { - space_id: space_name_id.room_id().clone(), - parent_chain: parent_chain.clone(), - }) - .is_err() - { - error!( - "BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}." - ); + if sender.send(SpaceRequest::PaginateSpaceRoomList { + space_id: space_name_id.room_id().clone(), + parent_chain: parent_chain.clone(), + }).is_err() { + error!("BUG: RoomsList: failed to send PaginateSpaceRoomList request for space {space_name_id}."); } - if sender - .send(SpaceRequest::GetChildren { - space_id: space_name_id.room_id().clone(), - parent_chain, - }) - .is_err() - { - error!( - "BUG: RoomsList: failed to send GetRooms request for space {space_name_id}." - ); + if sender.send(SpaceRequest::GetChildren { + space_id: space_name_id.room_id().clone(), + parent_chain, + }).is_err() { + error!("BUG: RoomsList: failed to send GetRooms request for space {space_name_id}."); } } } _ => { self.selected_space = None; - self.view - .space_lobby_entry(cx, ids!(space_lobby_entry)) - .set_visible(cx, false); + self.view.space_lobby_entry(cx, ids!(space_lobby_entry)).set_visible(cx, false); } } @@ -1543,31 +1375,25 @@ impl Widget for RoomsList { let total_count = status_label_id + 1; let get_invited_room_id = |portal_list_index: usize| { - portal_list_index - .checked_sub(self.invited_rooms_indexes.first_room_index) - .and_then(|index| { - self.is_invited_rooms_header_expanded - .then(|| self.displayed_invited_rooms.get(index)) - }) + portal_list_index.checked_sub(self.invited_rooms_indexes.first_room_index) + .and_then(|index| self.is_invited_rooms_header_expanded + .then(|| self.displayed_invited_rooms.get(index)) + ) .flatten() }; let get_direct_room_id = |portal_list_index: usize| { - portal_list_index - .checked_sub(self.direct_rooms_indexes.first_room_index) - .and_then(|index| { - self.is_direct_rooms_header_expanded - .then(|| self.displayed_direct_rooms.get(index)) - }) + portal_list_index.checked_sub(self.direct_rooms_indexes.first_room_index) + .and_then(|index| self.is_direct_rooms_header_expanded + .then(|| self.displayed_direct_rooms.get(index)) + ) .flatten() }; let get_regular_room_id = |portal_list_index: usize| { - portal_list_index - .checked_sub(self.regular_rooms_indexes.first_room_index) - .and_then(|index| { - self.is_regular_rooms_header_expanded - .then(|| self.displayed_regular_rooms.get(index)) - }) + portal_list_index.checked_sub(self.regular_rooms_indexes.first_room_index) + .and_then(|index| self.is_regular_rooms_header_expanded + .then(|| self.displayed_regular_rooms.get(index)) + ) .flatten() }; @@ -1579,9 +1405,7 @@ impl Widget for RoomsList { portal_list_ref.set_first_id_and_scroll(status_label_id, 0.0); } // We only care about drawing the portal list. - let Some(mut list) = portal_list_ref.borrow_mut() else { - continue; - }; + let Some(mut list) = portal_list_ref.borrow_mut() else { continue }; list.set_item_range(cx, 0, total_count); @@ -1597,13 +1421,12 @@ impl Widget for RoomsList { self.displayed_invited_rooms.len() as u64, ); item.draw_all(cx, &mut scope); - } else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { + } + else if let Some(invited_room_id) = get_invited_room_id(portal_list_index) { let mut invited_rooms_mut = self.invited_rooms.borrow_mut(); if let Some(invited_room) = invited_rooms_mut.get_mut(invited_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - invited_room.is_selected = self - .current_active_room - .as_ref() + invited_room.is_selected = self.current_active_room.as_ref() .is_some_and(|sel_room| sel_room.room_id() == invited_room_id); // Pass the room info down to the RoomsListEntry widget via Scope. scope = Scope::with_props(&*invited_room); @@ -1612,7 +1435,8 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { + } + else if self.direct_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1623,12 +1447,11 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { + } + else if let Some(direct_room_id) = get_direct_room_id(portal_list_index) { if let Some(direct_room) = self.all_joined_rooms.get_mut(direct_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - direct_room.is_selected = self - .current_active_room - .as_ref() + direct_room.is_selected = self.current_active_room.as_ref() .is_some_and(|sel_room| sel_room.room_id() == direct_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1649,7 +1472,8 @@ impl Widget for RoomsList { list.item(cx, portal_list_index, id!(empty)) .draw_all(cx, &mut scope); } - } else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { + } + else if self.regular_rooms_indexes.header_index == Some(portal_list_index) { let item = list.item(cx, portal_list_index, id!(collapsible_header)); item.as_collapsible_header().set_details( cx, @@ -1660,12 +1484,11 @@ impl Widget for RoomsList { // NOTE: this might be really slow, so we should maintain a running total of mentions in this struct ); item.draw_all(cx, &mut scope); - } else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { + } + else if let Some(regular_room_id) = get_regular_room_id(portal_list_index) { if let Some(regular_room) = self.all_joined_rooms.get_mut(regular_room_id) { let item = list.item(cx, portal_list_index, id!(rooms_list_entry)); - regular_room.is_selected = self - .current_active_room - .as_ref() + regular_room.is_selected = self.current_active_room.as_ref() .is_some_and(|sel_room| sel_room.room_id() == regular_room_id); // Paginate the room if it hasn't been paginated yet. @@ -1683,8 +1506,7 @@ impl Widget for RoomsList { scope = Scope::with_props(&*regular_room); item.draw_all(cx, &mut scope); } else { - list.item(cx, portal_list_index, id!(empty)) - .draw_all(cx, &mut scope); + list.item(cx, portal_list_index, id!(empty)).draw_all(cx, &mut scope); } } // Draw the status label as the bottom entry. @@ -1708,9 +1530,7 @@ impl Widget for RoomsList { impl RoomsListRef { /// See [`RoomsList::all_rooms_loaded()`]. pub fn all_rooms_loaded(&self) -> bool { - let Some(inner) = self.borrow() else { - return false; - }; + let Some(inner) = self.borrow() else { return false; }; inner.all_rooms_loaded() } @@ -1727,17 +1547,14 @@ impl RoomsListRef { /// Returns the name of the given room, if it is known and loaded. pub fn get_room_name(&self, room_id: &OwnedRoomId) -> Option { let inner = self.borrow()?; - inner - .all_joined_rooms + inner.all_joined_rooms .get(room_id) .map(|jr| jr.room_name_id.clone()) - .or_else(|| { - inner - .invited_rooms - .borrow() + .or_else(|| + inner.invited_rooms.borrow() .get(room_id) .map(|ir| ir.room_name_id.clone()) - }) + ) } /// Returns the currently-selected space (the one selected in the SpacesBar). @@ -1747,10 +1564,7 @@ impl RoomsListRef { /// Same as [`Self::get_selected_space()`], but only returns the space ID. pub fn get_selected_space_id(&self) -> Option { - self.borrow()? - .selected_space - .as_ref() - .map(|ss| ss.room_id().clone()) + self.borrow()?.selected_space.as_ref().map(|ss| ss.room_id().clone()) } /// Returns a clone of the space request sender channel, if available. diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index 292ebc23b..bf4563d65 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -15,33 +15,12 @@ //! * A "cannot-send-message" notice, which is shown if the user cannot send messages to the room. //! + 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 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}; script_mod! { use mod.prelude.widgets.* @@ -182,18 +161,14 @@ script_mod! { /// Main component for message input with @mention support #[derive(Script, ScriptHook, Widget)] pub struct RoomInputBar { - #[source] - source: ScriptObjectRef, - #[deref] - view: View, + #[source] source: ScriptObjectRef, + #[deref] view: View, /// Whether the `ReplyingPreview` was visible when the `EditingPane` was shown. /// If true, when the `EditingPane` gets hidden, we need to re-show the `ReplyingPreview`. - #[rust] - was_replying_preview_visible: bool, + #[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)>, + #[rust] replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, } impl Widget for RoomInputBar { @@ -203,21 +178,14 @@ impl Widget for RoomInputBar { .get::() .expect("BUG: RoomScreenProps should be available in Scope::props for RoomInputBar"); - match event.hits( - cx, - self.view - .view(cx, ids!(replying_preview.reply_preview_content)) - .area(), - ) { + match event.hits(cx, self.view.view(cx, ids!(replying_preview.reply_preview_content)).area()) { // If the hit occurred on the replying message preview, jump to it. Hit::FingerUp(fe) if fe.is_over && fe.is_primary_hit() && fe.was_tap() => { - if let Some(event_id) = self - .replying_to - .as_ref() + if let Some(event_id) = self.replying_to.as_ref() .and_then(|(event_tl_item, _)| event_tl_item.event_id().map(ToOwned::to_owned)) { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::JumpToEvent(event_id), ); } else { @@ -273,56 +241,40 @@ impl RoomInputBar { None, ); } - self.view - .location_preview(cx, ids!(location_preview)) - .show(); + self.view.location_preview(cx, ids!(location_preview)).show(); self.redraw(cx); } // Handle the send location button being clicked. - if self - .button(cx, ids!(location_preview.send_location_button)) - .clicked(actions) - { + if self.button(cx, ids!(location_preview.send_location_button)).clicked(actions) { let location_preview = self.location_preview(cx, ids!(location_preview)); if let Some((coords, _system_time_opt)) = location_preview.get_current_data() { - let geo_uri = format!( - "{}{},{}", - utils::GEO_URI_SCHEME, - coords.latitude, - coords.longitude + let geo_uri = format!("{}{},{}", utils::GEO_URI_SCHEME, coords.latitude, coords.longitude); + let message = RoomMessageEventContent::new( + MessageType::Location( + LocationMessageEventContent::new(geo_uri.clone(), geo_uri) + ) ); - let message = RoomMessageEventContent::new(MessageType::Location( - LocationMessageEventContent::new(geo_uri.clone(), geo_uri), - )); - let replied_to = self - .replying_to - .take() - .and_then(|(event_tl_item, _emb)| { - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props - .timeline_kind - .thread_root_event_id() - .is_some() - { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } - }) + let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } }) - .or_else(|| { - room_screen_props.timeline_kind.thread_root_event_id().map( - |thread_root_event_id| Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - }, - ) - }); + ).or_else(|| + room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| + Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + } + ) + ); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -339,9 +291,7 @@ impl RoomInputBar { // Handle the send message button being clicked or Cmd/Ctrl + Return being pressed. if self.button(cx, ids!(send_message_button)).clicked(actions) - || text_input - .returned(actions) - .is_some_and(|(_, m)| m.is_primary()) + || text_input.returned(actions).is_some_and(|(_, m)| m.is_primary()) { let entered_text = mentionable_text_input.text().trim().to_string(); if !entered_text.is_empty() { @@ -356,36 +306,27 @@ impl RoomInputBar { self.redraw(cx); return; } - let message = mentionable_text_input.create_message_with_mentions(&entered_text); - let replied_to = self - .replying_to - .take() - .and_then(|(event_tl_item, _emb)| { - event_tl_item.event_id().map(|event_id| { - let enforce_thread = if room_screen_props - .timeline_kind - .thread_root_event_id() - .is_some() - { - EnforceThread::Threaded(ReplyWithinThread::Yes) - } else { - EnforceThread::MaybeThreaded - }; - Reply { - event_id: event_id.to_owned(), - enforce_thread, - } - }) + let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| + event_tl_item.event_id().map(|event_id| { + let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { + EnforceThread::Threaded(ReplyWithinThread::Yes) + } else { + EnforceThread::MaybeThreaded + }; + Reply { + event_id: event_id.to_owned(), + enforce_thread, + } }) - .or_else(|| { - room_screen_props.timeline_kind.thread_root_event_id().map( - |thread_root_event_id| Reply { - event_id: thread_root_event_id.clone(), - enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), - }, - ) - }); + ).or_else(|| + room_screen_props.timeline_kind.thread_root_event_id().map(|thread_root_event_id| + Reply { + event_id: thread_root_event_id.clone(), + enforce_thread: EnforceThread::Threaded(ReplyWithinThread::No), + } + ) + ); submit_async_request(MatrixRequest::SendMessage { timeline_kind: room_screen_props.timeline_kind.clone(), message, @@ -419,29 +360,18 @@ impl RoomInputBar { if is_text_input_empty { if let Some(KeyEvent { key_code: KeyCode::ArrowUp, - modifiers: - KeyModifiers { - shift: false, - control: false, - alt: false, - logo: false, - }, + modifiers: KeyModifiers { shift: false, control: false, alt: false, logo: false }, .. - }) = text_input.key_down_unhandled(actions) - { + }) = text_input.key_down_unhandled(actions) { cx.widget_action( - room_screen_props.room_screen_widget_uid, + room_screen_props.room_screen_widget_uid, MessageAction::EditLatest, ); } } // If the EditingPane has been hidden, handle that. - if self - .view - .editing_pane(cx, ids!(editing_pane)) - .was_hidden(actions) - { + if self.view.editing_pane(cx, ids!(editing_pane)).was_hidden(actions) { self.on_editing_pane_hidden(cx); } } @@ -489,15 +419,13 @@ impl RoomInputBar { // 2. Hide other views that are irrelevant to a reply, e.g., // the `EditingPane` would improperly cover up the ReplyPreview. - self.editing_pane(cx, ids!(editing_pane)) - .force_reset_hide(cx); + self.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); self.on_editing_pane_hidden(cx); // 3. Automatically focus the keyboard on the message input box // so that the user can immediately start typing their reply // without having to manually click on the message input box. if grab_key_focus { - self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) - .set_key_focus(cx); + self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)).set_key_focus(cx); } self.button(cx, ids!(cancel_reply_button)).reset_hover(cx); self.redraw(cx); @@ -527,9 +455,7 @@ impl RoomInputBar { let replying_preview = self.view.view(cx, ids!(replying_preview)); self.was_replying_preview_visible = replying_preview.visible(); replying_preview.set_visible(cx, false); - self.view - .location_preview(cx, ids!(location_preview)) - .clear(); + self.view.location_preview(cx, ids!(location_preview)).clear(); let editing_pane = self.view.editing_pane(cx, ids!(editing_pane)); match behavior { @@ -551,14 +477,12 @@ impl RoomInputBar { // Same goes for the replying_preview, if it was previously shown. self.view.view(cx, ids!(input_bar)).set_visible(cx, true); if self.was_replying_preview_visible && self.replying_to.is_some() { - self.view - .view(cx, ids!(replying_preview)) - .set_visible(cx, true); + self.view.view(cx, ids!(replying_preview)).set_visible(cx, true); } self.redraw(cx); // We don't need to do anything with the editing pane itself here, // because it has already been hidden by the time this function gets called. - } + } /// Updates (populates and shows or hides) this room's tombstone footer /// based on the given successor room details. @@ -576,10 +500,7 @@ impl RoomInputBar { input_bar.set_visible(cx, false); } else { tombstone_footer.hide(cx); - if !self - .editing_pane(cx, ids!(editing_pane)) - .is_currently_shown(cx) - { + if !self.editing_pane(cx, ids!(editing_pane)).is_currently_shown(cx) { input_bar.set_visible(cx, true); } } @@ -602,8 +523,6 @@ impl RoomInputBar { }); } - /// Intercepts `/bot` commands and opens the room-level app service actions UI instead - /// of sending the raw command text into the room. fn try_handle_bot_shortcut( &mut self, cx: &mut Cx, @@ -614,11 +533,7 @@ impl RoomInputBar { return false; } - let popup_message = if room_screen_props - .timeline_kind - .thread_root_event_id() - .is_some() - { + let popup_message = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { Some(( "Bot commands are only supported in the main room timeline.", PopupKind::Warning, @@ -657,14 +572,14 @@ impl RoomInputBar { /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - fn update_user_power_levels(&mut self, cx: &mut Cx, user_power_levels: UserPowerLevels) { + fn update_user_power_levels( + &mut self, + cx: &mut Cx, + user_power_levels: UserPowerLevels, + ) { let can_send = user_power_levels.can_send_message(); - self.view - .view(cx, ids!(input_bar)) - .set_visible(cx, can_send); - self.view - .view(cx, ids!(can_not_send_message_notice)) - .set_visible(cx, !can_send); + self.view.view(cx, ids!(input_bar)).set_visible(cx, can_send); + self.view.view(cx, ids!(can_not_send_message_notice)).set_visible(cx, !can_send); } /// Returns true if the TSP signing checkbox is checked, false otherwise. @@ -685,9 +600,7 @@ impl RoomInputBarRef { replying_to: (EventTimelineItem, EmbeddedEvent), timeline_kind: &TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; inner.show_replying_to(cx, replying_to, timeline_kind, true); } @@ -698,9 +611,7 @@ impl RoomInputBarRef { event_tl_item: EventTimelineItem, timeline_kind: TimelineKind, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; inner.show_editing_pane( cx, ShowEditingPaneBehavior::ShowNew { event_tl_item }, @@ -711,10 +622,12 @@ impl RoomInputBarRef { /// Updates the visibility of select views based on the user's new power levels. /// /// This will show/hide the `input_bar` and the `can_not_send_message_notice` views. - pub fn update_user_power_levels(&self, cx: &mut Cx, user_power_levels: UserPowerLevels) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + pub fn update_user_power_levels( + &self, + cx: &mut Cx, + user_power_levels: UserPowerLevels, + ) { + let Some(mut inner) = self.borrow_mut() else { return }; inner.update_user_power_levels(cx, user_power_levels); } @@ -725,9 +638,7 @@ impl RoomInputBarRef { tombstoned_room_id: &OwnedRoomId, successor_room_details: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; inner.update_tombstone_footer(cx, tombstoned_room_id, successor_room_details); } @@ -739,36 +650,22 @@ impl RoomInputBarRef { timeline_event_item_id: TimelineEventItemId, edit_result: Result<(), matrix_sdk_ui::timeline::Error>, ) { - let Some(inner) = self.borrow_mut() else { - return; - }; - inner - .editing_pane(cx, ids!(editing_pane)) + let Some(inner) = self.borrow_mut() else { return }; + inner.editing_pane(cx, ids!(editing_pane)) .handle_edit_result(cx, timeline_event_item_id, edit_result); } /// Save a snapshot of the UI state of this `RoomInputBar`. pub fn save_state(&self) -> RoomInputBarState { - let Some(inner) = self.borrow() else { - return Default::default(); - }; + let Some(inner) = self.borrow() else { return Default::default() }; // Clear the location preview. We don't save this state because the // current location might change by the next time the user opens this same room. - inner - .child_by_path(ids!(location_preview)) - .as_location_preview() - .clear(); + inner.child_by_path(ids!(location_preview)).as_location_preview().clear(); RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), - editing_pane_state: inner - .child_by_path(ids!(editing_pane)) - .as_editing_pane() - .save_state(), - text_input_state: inner - .child_by_path(ids!(input_bar.mentionable_text_input.text_input)) - .as_text_input() - .save_state(), + editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), + text_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input.text_input)).as_text_input().save_state(), } } @@ -781,9 +678,7 @@ impl RoomInputBarRef { user_power_levels: UserPowerLevels, tombstone_info: Option<&SuccessorRoomDetails>, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return }; let RoomInputBarState { was_replying_preview_visible, text_input_state, @@ -799,8 +694,7 @@ impl RoomInputBarRef { inner.update_user_power_levels(cx, user_power_levels); // 1. Restore the state of the TextInput within the MentionableTextInput. - inner - .text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + inner.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) .restore_state(cx, text_input_state); // 2. Restore the state of the replying-to preview. @@ -819,9 +713,7 @@ impl RoomInputBarRef { timeline_kind.clone(), ); } else { - inner - .editing_pane(cx, ids!(editing_pane)) - .force_reset_hide(cx); + inner.editing_pane(cx, ids!(editing_pane)).force_reset_hide(cx); inner.on_editing_pane_hidden(cx); } @@ -847,7 +739,9 @@ pub struct RoomInputBarState { /// Defines what to do when showing the `EditingPane` from the `RoomInputBar`. enum ShowEditingPaneBehavior { /// Show a new edit session, e.g., when first clicking "edit" on a message. - ShowNew { event_tl_item: EventTimelineItem }, + ShowNew { + event_tl_item: EventTimelineItem, + }, /// Restore the state of an `EditingPane` that already existed, e.g., when /// reopening a room that had an `EditingPane` open when it was closed. RestoreExisting { diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 38246560c..79c690997 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,3 +1,4 @@ + use makepad_widgets::*; use crate::{ @@ -92,11 +93,11 @@ script_mod! { } } + /// The top-level widget showing all app and user settings/preferences. #[derive(Script, ScriptHook, Widget)] pub struct SettingsScreen { - #[deref] - view: View, + #[deref] view: View, } impl Widget for SettingsScreen { @@ -113,15 +114,16 @@ impl Widget for SettingsScreen { matches!( event, Event::Actions(actions) if self.button(cx, ids!(close_button)).clicked(actions) - ) || event.back_pressed() - || match event.hits(cx, area) { - Hit::KeyUp(key) => key.key_code == KeyCode::Escape, - Hit::FingerDown(_fde) => { - cx.set_key_focus(area); - false - } - _ => false, + ) + || event.back_pressed() + || match event.hits(cx, area) { + Hit::KeyUp(key) => key.key_code == KeyCode::Escape, + Hit::FingerDown(_fde) => { + cx.set_key_focus(area); + false } + _ => false, + } }; if close_pane { cx.action(NavigationBarAction::CloseSettings); @@ -139,30 +141,26 @@ impl Widget for SettingsScreen { match action.downcast_ref() { Some(CreateWalletModalAction::Open) => { use crate::tsp::create_wallet_modal::CreateWalletModalWidgetExt; - self.view - .create_wallet_modal(cx, ids!(create_wallet_modal_inner)) - .show(cx); + self.view.create_wallet_modal(cx, ids!(create_wallet_modal_inner)).show(cx); self.view.modal(cx, ids!(create_wallet_modal)).open(cx); } Some(CreateWalletModalAction::Close) => { self.view.modal(cx, ids!(create_wallet_modal)).close(cx); } - None => {} + None => { } } // Handle the create DID modal being opened or closed. match action.downcast_ref() { Some(CreateDidModalAction::Open) => { use crate::tsp::create_did_modal::CreateDidModalWidgetExt; - self.view - .create_did_modal(cx, ids!(create_did_modal_inner)) - .show(cx); + self.view.create_did_modal(cx, ids!(create_did_modal_inner)).show(cx); self.view.modal(cx, ids!(create_did_modal)).open(cx); } Some(CreateDidModalAction::Close) => { self.view.modal(cx, ids!(create_did_modal)).close(cx); } - None => {} + None => { } } } } @@ -185,12 +183,8 @@ impl SettingsScreen { error!("Failed to get own profile for settings screen."); return; }; - self.view - .account_settings(cx, ids!(account_settings)) - .populate(cx, profile); - self.view - .bot_settings(cx, ids!(bot_settings)) - .populate(cx, bot_settings); + self.view.account_settings(cx, ids!(account_settings)).populate(cx, profile); + self.view.bot_settings(cx, ids!(bot_settings)).populate(cx, bot_settings); self.view.button(cx, ids!(close_button)).reset_hover(cx); cx.set_key_focus(self.view.area()); self.redraw(cx); @@ -205,9 +199,7 @@ impl SettingsScreenRef { own_profile: Option, bot_settings: &BotSettingsState, ) { - let Some(mut inner) = self.borrow_mut() else { - return; - }; + let Some(mut inner) = self.borrow_mut() else { return; }; inner.populate(cx, own_profile, bot_settings); } } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 1ffdf7de6..255332260 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,110 +8,43 @@ 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::{ - account::register::v3::Request as RegistrationRequest, - error::ErrorKind, - profile::{AvatarUrl, DisplayName}, - receipt::create_receipt::v3::ReceiptType, - uiaa::{AuthData, AuthType, Dummy}, - }, - }, - events::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ + api::{Direction, client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }}, 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, + 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, - }, + 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, + 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::{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::{ + 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, + }, 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)] @@ -159,8 +92,7 @@ impl From for Cli { Self { user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login - .homeserver + homeserver: login.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -175,8 +107,7 @@ impl From for Cli { Self { user_id: registration.user_id.trim().to_owned(), password: registration.password, - homeserver: registration - .homeserver + homeserver: registration.homeserver .map(|homeserver| homeserver.trim().to_owned()) .filter(|homeserver| !homeserver.is_empty()), proxy: None, @@ -197,8 +128,7 @@ async fn finalize_authenticated_client( fallback_user_id: &str, ) -> Result<(Client, Option)> { if client.matrix_auth().logged_in() { - let logged_in_user_id = client - .user_id() + let logged_in_user_id = client.user_id() .map(ToString::to_string) .unwrap_or_else(|| fallback_user_id.to_owned()); log!("Logged in successfully."); @@ -215,9 +145,7 @@ async fn finalize_authenticated_client( "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.clone(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } } @@ -233,8 +161,7 @@ fn registration_localpart(user_id: &str) -> Result { } let localpart = trimmed.trim_start_matches('@'); - if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) - { + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { bail!("Please enter a valid username or full Matrix user ID."); } @@ -341,14 +268,9 @@ async fn reset_runtime_state_for_relogin() { 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(), - }); + Cx::post_action(LogoutAction::ClearAppState { on_clear_appstate: on_clear_appstate.clone() }); - if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()) - .await - .is_err() - { + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); } } @@ -360,6 +282,7 @@ fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { ) } + /// Build a new client. async fn build_client( cli: &Cli, @@ -382,13 +305,11 @@ async fn build_client( }; let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); - let homeserver_url = cli - .homeserver - .as_deref() + let homeserver_url = cli.homeserver.as_deref() .filter(|homeserver| !homeserver.trim().is_empty()) .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); - // .unwrap_or("https://matrix.org/"); + // .unwrap_or("https://matrix.org/"); let mut builder = Client::builder() .server_name_or_homeserver_url(homeserver_url) @@ -416,11 +337,13 @@ async fn build_client( // 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))); + 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(); + let homeserver_url = client.homeserver().to_string(); Ok(( client, ClientSessionPersisted { @@ -436,7 +359,10 @@ async fn build_client( /// 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)> { +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 { @@ -459,9 +385,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option if !client.matrix_auth().logged_in() { 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(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } finalize_authenticated_client(client, client_session, &cli.user_id).await @@ -517,9 +441,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option register_result.user_id, ); enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.clone(), - }); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); bail!(err_msg); } @@ -539,6 +461,7 @@ async fn login(cli: &Cli, login_request: LoginRequest) -> Result<(Client, Option } } + /// Which direction to paginate in. /// /// * `Forwards` will retrieve later events (towards the end of the timeline), @@ -607,6 +530,7 @@ pub type OnLinkPreviewFetchedFn = fn( Option>, ); + /// Actions emitted in response to a [`MatrixRequest::GenerateMatrixLink`]. #[derive(Clone, Debug)] pub enum MatrixLinkAction { @@ -637,7 +561,9 @@ pub enum DirectMessageRoomAction { 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 }, + DidNotExist { + user_profile: UserProfile, + }, /// A direct message room didn't exist, but we successfully created a new one. NewlyCreated { user_profile: UserProfile, @@ -672,10 +598,7 @@ impl TimelineKind { 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), + TimelineKind::Thread { thread_root_event_id, .. } => Some(thread_root_event_id), } } } @@ -683,10 +606,7 @@ 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, - } => { + TimelineKind::Thread { room_id, thread_root_event_id } => { write!(f, "Thread({}, {})", room_id, thread_root_event_id) } } @@ -699,7 +619,9 @@ pub enum MatrixRequest { /// Request from the login screen to log in with the given credentials. Login(LoginRequest), /// Request to logout. - Logout { is_desktop: bool }, + Logout { + is_desktop: bool, + }, /// Request to paginate the older (or newer) events of a room or thread timeline. PaginateTimeline { timeline_kind: TimelineKind, @@ -731,7 +653,9 @@ pub enum MatrixRequest { /// /// 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 }, + 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, @@ -756,9 +680,13 @@ pub enum MatrixRequest { bot_user_id: OwnedUserId, }, /// Request to join the given room. - JoinRoom { room_id: OwnedRoomId }, + JoinRoom { + room_id: OwnedRoomId, + }, /// Request to leave the given room. - LeaveRoom { room_id: OwnedRoomId }, + 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. @@ -781,7 +709,9 @@ pub enum MatrixRequest { via: Vec, }, /// Request to fetch the full details (the room preview) of a tombstoned room. - GetSuccessorRoomDetails { tombstoned_room_id: OwnedRoomId }, + 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 @@ -806,7 +736,9 @@ pub enum MatrixRequest { local_only: bool, }, /// Request to fetch the number of unread messages in the given room. - GetNumberUnreadMessages { timeline_kind: TimelineKind }, + GetNumberUnreadMessages { + timeline_kind: TimelineKind, + }, /// Request to set the unread flag for the given room. SetUnreadFlag { room_id: OwnedRoomId, @@ -891,12 +823,15 @@ pub enum MatrixRequest { /// 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 }, + 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 { + SpawnSSOServer{ brand: String, homeserver_url: String, identity_provider_id: String, @@ -941,7 +876,9 @@ pub enum MatrixRequest { /// /// 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 }, + GetRoomPowerLevels { + timeline_kind: TimelineKind, + }, /// Toggles the given reaction to the given event in the given room. ToggleReaction { timeline_kind: TimelineKind, @@ -967,7 +904,7 @@ pub enum MatrixRequest { /// The MatrixLinkPillInfo::Loaded variant is sent back to the main UI thread via. GetMatrixRoomLinkPillInfo { matrix_id: MatrixId, - via: Vec, + via: Vec }, /// Request to fetch URL preview from the Matrix homeserver. GetUrlPreview { @@ -981,19 +918,19 @@ pub enum MatrixRequest { /// 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) + 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 { +pub enum LoginRequest{ LoginByPassword(LoginByPassword), Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), + } /// Information needed to log in to a Matrix homeserver. pub struct LoginByPassword { @@ -1010,6 +947,7 @@ pub struct RegisterAccount { 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 @@ -1020,8 +958,7 @@ async fn matrix_worker_task( ) -> 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(); + 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(); @@ -1031,7 +968,7 @@ async fn matrix_worker_task( 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.", + "BUG: failed to send login request to login worker task." ))); } } @@ -1044,7 +981,7 @@ async fn matrix_worker_task( match logout_with_state_machine(is_desktop).await { Ok(()) => { log!("Logout completed successfully via state machine"); - } + }, Err(e) => { error!("Logout failed: {e:?}"); } @@ -1052,11 +989,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::PaginateTimeline { - timeline_kind, - num_events, - direction, - } => { + 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; @@ -1098,11 +1031,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::EditMessage { - timeline_kind, - timeline_event_item_id, - edited_content, - } => { + 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; @@ -1124,10 +1053,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchDetailsForEvent { - timeline_kind, - event_id, - } => { + 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; @@ -1144,10 +1070,7 @@ async fn matrix_worker_task( // error!("Error fetching details for event {event_id} in {timeline_kind}: {_e:?}"); } } - if sender - .send(TimelineUpdate::EventDetailsFetched { event_id, result }) - .is_err() - { + 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(); @@ -1201,27 +1124,17 @@ async fn matrix_worker_task( }); } - MatrixRequest::CreateThreadTimeline { - room_id, - thread_root_event_id, - } => { + 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}" - ); + 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) - { + 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()); + let newly_pending = room_info.pending_thread_timelines.insert(thread_root_event_id.clone()); if !newly_pending { continue; } @@ -1293,18 +1206,11 @@ async fn matrix_worker_task( }); } - MatrixRequest::Knock { - room_or_alias_id, - reason, - server_names, - } => { + 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 - { + 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 { @@ -1328,26 +1234,90 @@ async fn matrix_worker_task( 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 }), + Ok(_) => Cx::post_action(InviteResultAction::Sent { + room_id, + user_id, + }), Err(error) => Cx::post_action(InviteResultAction::Failed { room_id, user_id, error, }), } - } else { + } + 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(), - ), + error: matrix_sdk::Error::UnknownError("Room/Space not found in client's known list.".into()), }) } }); } + MatrixRequest::SetRoomBotBinding { + room_id, + bound, + bot_user_id, + } => { + let Some(client) = get_client() else { continue }; + let _bot_binding_task = Handle::current().spawn(async move { + let Some(room) = client.get_room(&room_id) else { + let error_message = + format!("Room {room_id} was not found for the bot binding request."); + error!("{error_message}"); + enqueue_popup_notification(error_message, PopupKind::Error, None); + return; + }; + + let membership_result = if bound { + room.invite_user_by_id(&bot_user_id).await + } else { + room.kick_user(&bot_user_id, Some("Robrix app service unbind")).await + }; + + match membership_result { + Ok(()) => { + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: None, + }); + } + Err(error) => { + let membership_exists = + room.get_member_no_sync(&bot_user_id).await.ok().flatten().is_some(); + let should_mark_bound = if bound { membership_exists } else { false }; + + if should_mark_bound != bound { + error!( + "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", + if bound { "invite" } else { "remove" } + ); + enqueue_popup_notification( + format!( + "Failed to {} BotFather {bot_user_id}: {error}", + if bound { "invite" } else { "remove" } + ), + PopupKind::Error, + None, + ); + return; + } + + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: Some(error.to_string()), + }); + } + } + }); + } + MatrixRequest::JoinRoom { room_id } => { let Some(client) = get_client() else { continue }; let _join_room_task = Handle::current().spawn(async move { @@ -1363,7 +1333,8 @@ async fn matrix_worker_task( JoinRoomResultAction::Failed { room_id, error: e } } } - } else { + } + else { match client.join_room_by_id(&room_id).await { Ok(_room) => { log!("Successfully joined new unknown room {room_id}."); @@ -1398,20 +1369,14 @@ async fn matrix_worker_task( 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(), - ), + 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, - } => { + 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; @@ -1420,9 +1385,7 @@ async fn matrix_worker_task( 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(); + sender.send(TimelineUpdate::RoomMembersListFetched { members }).unwrap(); SignalToUI::set_ui_signal(); }; @@ -1439,10 +1402,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetRoomPreview { - room_or_alias_id, - via, - } => { + 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; @@ -1450,80 +1410,12 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetRoomBotBinding { - room_id, - bound, - bot_user_id, - } => { - let Some(client) = get_client() else { continue }; - let _bot_binding_task = Handle::current().spawn(async move { - let Some(room) = client.get_room(&room_id) else { - let error_message = - format!("Room {room_id} was not found for the bot binding request."); - error!("{error_message}"); - enqueue_popup_notification(error_message, PopupKind::Error, None); - return; - }; - - let membership_result = if bound { - room.invite_user_by_id(&bot_user_id).await - } else { - room.kick_user(&bot_user_id, Some("Robrix app service unbind")).await - }; - - match membership_result { - Ok(()) => { - Cx::post_action(AppStateAction::BotRoomBindingUpdated { - room_id, - bound, - bot_user_id: Some(bot_user_id), - warning: None, - }); - } - Err(error) => { - let membership_exists = room - .get_member_no_sync(&bot_user_id) - .await - .ok() - .flatten() - .is_some(); - let should_mark_bound = if bound { membership_exists } else { false }; - - if should_mark_bound != bound { - error!( - "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", - if bound { "invite" } else { "remove" } - ); - enqueue_popup_notification( - format!( - "Failed to {} BotFather {bot_user_id}: {error}", - if bound { "invite" } else { "remove" } - ), - PopupKind::Error, - None, - ); - return; - } - - Cx::post_action(AppStateAction::BotRoomBindingUpdated { - room_id, - bound, - bot_user_id: Some(bot_user_id), - warning: Some(error.to_string()), - }); - } - } - }); - } - 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" - ); + error!("BUG: tombstoned room {tombstoned_room_id} info not found for get successor room details request"); continue; }; ( @@ -1539,10 +1431,7 @@ async fn matrix_worker_task( ); } - MatrixRequest::OpenOrCreateDirectMessage { - user_profile, - allow_create, - } => { + 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) { @@ -1565,7 +1454,7 @@ async fn matrix_worker_task( 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 { @@ -1577,11 +1466,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUserProfile { - user_id, - room_id, - local_only, - } => { + 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}, \ @@ -1675,10 +1560,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetUnreadFlag { - room_id, - mark_as_unread, - } => { + 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; @@ -1687,64 +1569,35 @@ async fn matrix_worker_task( 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 - ), + Err(e) => error!("Failed to set unread flag to {} for room {}: {:?}", mark_as_unread, room_id, e), } }); } - MatrixRequest::SetIsFavorite { - room_id, - is_favorite, - } => { + 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}" - ); + 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; + 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 - ), + Err(e) => error!("Failed to set favorite to {} for room {}: {:?}", is_favorite, room_id, e), } }); } - MatrixRequest::SetIsLowPriority { - room_id, - is_low_priority, - } => { + 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}" - ); + 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; + 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 - ), + 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), } }); } @@ -1753,24 +1606,15 @@ async fn matrix_worker_task( 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" } - ); + 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" } - ); + 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" } - ); + let err_msg = format!("Failed to {} avatar: {e}", if is_removing { "remove" } else { "set" }); Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); } } @@ -1781,87 +1625,57 @@ async fn matrix_worker_task( 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{}...", + 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() + 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; + 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, - )); + 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" } - ); + 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, - } => { + 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 + room.matrix_event_permalink(event_id).await .map(MatrixLinkAction::MatrixUri) } else { - room.matrix_permalink(join_on_click) - .await + 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 + room.matrix_to_event_permalink(event_id).await .map(MatrixLinkAction::MatrixToUri) } else { - room.matrix_to_permalink() - .await + 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" - ))); + Cx::post_action(MatrixLinkAction::Error(format!("Room {room_id} not found"))); } }); } - MatrixRequest::IgnoreUser { - ignore, - room_member, - room_id, - } => { + 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(); @@ -1916,9 +1730,7 @@ async fn matrix_worker_task( 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}" - ); + log!("BUG: skipping send typing notice request for not-yet-known room {room_id}"); continue; }; let _typing_task = Handle::current().spawn(async move { @@ -1932,21 +1744,16 @@ async fn matrix_worker_task( 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}" - ); + 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." - ); + 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(); + let (drop_guard, receiver) = main_timeline.room().subscribe_to_typing_notifications(); jrd.typing_notice_subscriber = Some(drop_guard); (main_timeline, receiver) } @@ -1955,11 +1762,7 @@ async fn matrix_worker_task( 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, - ) + (main_timeline, jrd.main_timeline.timeline_update_sender.clone(), receiver) }; let _typing_notices_task = Handle::current().spawn(async move { @@ -1986,22 +1789,15 @@ async fn matrix_worker_task( }); } - MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { - timeline_kind, - subscribe, - } => { + MatrixRequest::SubscribeToOwnUserReadReceiptsChanged { timeline_kind, subscribe } => { if !subscribe { - if let Some(task_handler) = - subscribers_own_user_read_receipts.remove(&timeline_kind) - { + 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}" - ); + log!("BUG: skipping subscribe to own user read receipts changed request for {timeline_kind}"); continue; }; @@ -2043,8 +1839,7 @@ async fn matrix_worker_task( } } }); - subscribers_own_user_read_receipts - .insert(timeline_kind_clone, subscribe_own_read_receipt_task); + subscribers_own_user_read_receipts.insert(timeline_kind_clone, subscribe_own_read_receipt_task); } MatrixRequest::SubscribeToPinnedEvents { room_id, subscribe } => { @@ -2054,13 +1849,9 @@ async fn matrix_worker_task( } continue; } - let kind = TimelineKind::MainRoom { - room_id: room_id.clone(), - }; + 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}" - ); + log!("BUG: skipping subscribe to pinned events request for unknown room {room_id}"); continue; }; let subscribe_pinned_events_task = Handle::current().spawn(async move { @@ -2082,18 +1873,8 @@ async fn matrix_worker_task( 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::SpawnSSOServer { brand, homeserver_url, identity_provider_id} => { + spawn_sso_server(brand, homeserver_url, identity_provider_id, login_sender.clone()).await; } MatrixRequest::ResolveRoomAlias(room_alias) => { @@ -2106,10 +1887,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::FetchAvatar { - mxc_uri, - on_fetched, - } => { + 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:?}..."); @@ -2119,21 +1897,13 @@ async fn matrix_worker_task( }; 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()), - }); + on_fetched(AvatarUpdate { mxc_uri, avatar_data: res.map(|v| v.into()) }); }); } - MatrixRequest::FetchMedia { - media_request, - on_fetched, - destination, - update_sender, - } => { + 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; @@ -2238,11 +2008,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::ReadReceipt { - timeline_kind, - event_id, - receipt_type, - } => { + 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; @@ -2263,7 +2029,7 @@ async fn matrix_worker_task( }); } }); - } + }, MatrixRequest::GetRoomPowerLevels { timeline_kind } => { let Some((timeline, sender)) = get_timeline_and_sender(&timeline_kind) else { @@ -2271,21 +2037,15 @@ async fn matrix_worker_task( continue; }; - let Some(user_id) = current_user_id() else { - 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() - { + 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(); @@ -2295,13 +2055,9 @@ async fn matrix_worker_task( } } }); - } + }, - MatrixRequest::ToggleReaction { - timeline_kind, - timeline_event_id, - reaction, - } => { + 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; @@ -2309,26 +2065,17 @@ async fn matrix_worker_task( 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 - { + 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:?}" - ), + }, + Err(_e) => error!("Failed to send toggle reaction to {timeline_kind}; error: {_e:?}"), } }); - } + }, - MatrixRequest::RedactMessage { - timeline_kind, - timeline_event_id, - reason, - } => { + 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; @@ -2347,13 +2094,9 @@ async fn matrix_worker_task( } } }); - } + }, - MatrixRequest::PinEvent { - timeline_kind, - event_id, - pin, - } => { + 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; @@ -2365,11 +2108,7 @@ async fn matrix_worker_task( } else { timeline.unpin_event(&event_id).await }; - match sender.send(TimelineUpdate::PinResult { - event_id, - pin, - result, - }) { + match sender.send(TimelineUpdate::PinResult { event_id, pin, result }) { Ok(_) => SignalToUI::set_ui_signal(), Err(_) => log!("Failed to send UI update for pin event."), } @@ -2403,12 +2142,7 @@ async fn matrix_worker_task( }); } - MatrixRequest::GetUrlPreview { - url, - on_fetched, - destination, - update_sender, - } => { + 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 { @@ -2418,19 +2152,17 @@ async fn matrix_worker_task( // 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") + 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()) @@ -2443,20 +2175,20 @@ async fn matrix_worker_task( // 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]); @@ -2465,25 +2197,22 @@ async fn matrix_worker_task( // } // 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) - }); + 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 { + 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) => { @@ -2503,12 +2232,11 @@ async fn matrix_worker_task( // error!("Response body that failed to parse: {}", text); UrlPreviewError::Json(e) }) - } - .await; + }.await; // match &result { // Ok(preview_data) => { - // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", + // log!("Successfully fetched URL preview for {}: title: {:?}, site_name: {:?}", // url, preview_data.title, preview_data.site_name); // } // Err(e) => { @@ -2527,6 +2255,7 @@ async fn matrix_worker_task( 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); @@ -2539,8 +2268,7 @@ static REQUEST_SENDER: Mutex>> = Mutex::ne 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())); +static DEFAULT_SSO_CLIENT_NOTIFIER: LazyLock> = LazyLock::new(|| Arc::new(Notify::new())); /// Blocks the current thread until the given future completes. /// @@ -2551,45 +2279,36 @@ 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(); + 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 }) + 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(); + 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() + DEFAULT_SSO_CLIENT.lock().unwrap() .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2606,6 +2325,7 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2672,13 +2392,13 @@ impl Drop for JoinedRoomDetails { } } + /// 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())); +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>( @@ -2688,10 +2408,7 @@ fn get_per_timeline_details<'a>( 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), + TimelineKind::Thread { thread_root_event_id, .. } => room_info.thread_timelines.get_mut(thread_root_event_id), } } @@ -2702,22 +2419,14 @@ fn get_timeline(kind: &TimelineKind) -> Option> { } /// 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(), - ) - }) +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() + ALL_JOINED_ROOMS.lock().unwrap() .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2731,16 +2440,15 @@ pub fn get_client() -> Option { /// 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())) + 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() @@ -2749,8 +2457,7 @@ pub fn get_sync_service() -> Option> { /// 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())); +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 { @@ -2762,6 +2469,7 @@ 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. @@ -2775,10 +2483,7 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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)?, + 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 { @@ -2791,18 +2496,25 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option 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() - }) +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 @@ -2830,14 +2542,18 @@ struct RoomListServiceRoomInfo { 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 { + 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(), @@ -2879,26 +2595,26 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() + 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? {}", + 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")); + 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: Option<(Client, Option, bool)> = 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: {:?}", + 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.clone()).await { @@ -2915,10 +2631,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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 - ); + 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(), @@ -2927,9 +2640,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok((client, sync_token)) => Some((client, sync_token, false)), 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}" - ))); + 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:?}"), }); @@ -2954,30 +2667,34 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let (client, sync_service, logged_in_user_id) = 'login_loop: loop { let (client, _sync_token, validate_session) = 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, false), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); + 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, false), + 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: format!("Login failed: {e}"), + status: err, }); + return; } - }, - 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; } } - }, + } }; if validate_session { @@ -2985,8 +2702,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { Ok(_) => {} Err(e) if is_invalid_token_http_error(&e) => { clear_persisted_session(client.user_id()).await; - let err_msg = - "Your login token is no longer valid.\n\nPlease log in again."; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.to_string(), @@ -2994,9 +2710,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { continue 'login_loop; } Err(e) => { - warning!( - "Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}" - ); + warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); } } } @@ -3006,8 +2720,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let _ = client_opt.take(); } - let logged_in_user_id: OwnedUserId = client - .user_id() + 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); @@ -3015,9 +2728,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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." - ); + error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); } // Listen for changes to our verification status and incoming verification requests. @@ -3041,9 +2752,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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}." - ) + format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") }; if is_invalid_token_error(&e) { clear_persisted_session(client.user_id()).await; @@ -3081,9 +2790,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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." - ); + 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)); @@ -3200,6 +2907,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } + /// 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?; @@ -3213,13 +2921,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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()), - ]))); + 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(); @@ -3235,13 +2943,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu // 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() - ); - } + 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); @@ -3252,35 +2954,20 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 - ); - } + 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 - ); + 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; + }) + ).await; // Send room order update with the new room IDs let (room_id_refs, room_ids) = { @@ -3294,57 +2981,43 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu }; if !room_ids.is_empty() { enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Append { values: room_ids }, + 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"); - } + 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; + 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 }, + 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; + 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 }, + 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 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, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopFront)); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3352,18 +3025,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).await?; } } remove_diff @ VectorDiff::PopBack => { - if LOG_ROOM_LIST_DIFFS { - log!("room_list: diff 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, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::PopBack)); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3371,61 +3039,38 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).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; + 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, - })); + 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; + 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(), - })); + 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 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 }, - )); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Remove { index })); optimize_remove_then_add_into_update( remove_diff, &room, @@ -3433,19 +3078,13 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu &mut all_known_rooms, &room_list_service, ¤t_user_id, - ) - .await?; + ).await?; } else { - error!( - "BUG: room_list: diff Remove index {index} out of bounds, len {}", - all_known_rooms.len() - ); + 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}"); - } + 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() { @@ -3454,7 +3093,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } all_known_rooms.truncate(length); // sanity check enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate( - VecDiff::Truncate { length }, + VecDiff::Truncate { length } )); } } @@ -3464,6 +3103,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu 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 @@ -3483,58 +3123,48 @@ async fn optimize_remove_then_add_into_update( ) -> 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() => { + 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 - ); + 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; + 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(), - })); + 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() => { + 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 - ); + 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; + 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(), - })); + 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() => { + 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 - ); + 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; + 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(), - })); + 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; } @@ -3548,6 +3178,7 @@ async fn optimize_remove_then_add_into_update( Ok(()) } + /// Invoked when the room list service has received an update that changes an existing room. async fn update_room( old_room: &RoomListServiceRoomInfo, @@ -3558,29 +3189,18 @@ async fn update_room( 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 - ); + 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 - ); + 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 - ); + 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 @@ -3590,17 +3210,11 @@ async fn update_room( return Ok(()); } RoomState::Joined => { - log!( - "update_room(): adding new Joined room: {:?} ({new_room_id})", - new_room.display_name - ); + 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 - ); + 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 => { @@ -3618,12 +3232,7 @@ async fn update_room( 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 - ); + 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(), @@ -3633,15 +3242,12 @@ async fn update_room( // 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) { + 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(), - ) { + 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, @@ -3650,13 +3256,9 @@ async fn update_room( 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 - ); + 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(), @@ -3667,15 +3269,11 @@ async fn update_room( || 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 {} --> {}", + 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, + 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(), @@ -3686,8 +3284,7 @@ async fn update_room( } if old_room.is_direct != new_room.is_direct { - log!( - "Updating room {} is_direct from {} to {}", + log!("Updating room {} is_direct from {} to {}", new_room_id, old_room.is_direct, new_room.is_direct, @@ -3702,8 +3299,7 @@ async fn update_room( 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 = Some(jrd.main_timeline.timeline_update_sender.clone()); } } __timeline_update_sender_opt.clone() @@ -3712,9 +3308,7 @@ async fn update_room( 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(), - }); + 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(), @@ -3723,9 +3317,7 @@ async fn update_room( timeline_update_sender, ); } else { - error!( - "BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}" - ); + error!("BUG: could not find JoinedRoomDetails for newly-tombstoned room {new_room_id}"); } } @@ -3736,38 +3328,37 @@ async fn update_room( 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}" - ), + 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." - ); + 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, + } + 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, - }); + 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, @@ -3776,39 +3367,26 @@ async fn add_new_room( ) -> Result<()> { match new_room.state { RoomState::Knocked => { - log!( - "Got new Knocked room: {:?} ({})", - new_room.display_name, - new_room.room_id - ); + 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 - ); + 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 - ); + 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())); + 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) { @@ -3825,20 +3403,18 @@ async fn add_new_room( } 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, - }, - )); + 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, @@ -3846,21 +3422,17 @@ async fn add_new_room( spawn_fetch_room_avatar(new_room); return Ok(()); } - RoomState::Joined => {} // Fall through to adding the joined room below. + 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; + room_list_service.subscribe_to_rooms(&[&new_room.room_id]).await; } let timeline = Arc::new( - new_room - .room - .timeline_builder() + new_room.room.timeline_builder() .with_focus(TimelineFocus::Live { // we show threads as separate timelines in their own RoomScreen hide_threaded_events: true, @@ -3868,12 +3440,7 @@ async fn add_new_room( .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 - ) - })?, + .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(); @@ -3889,11 +3456,7 @@ async fn add_new_room( // 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 - ); + 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 { @@ -3914,8 +3477,7 @@ async fn add_new_room( let latest = get_latest_event_details( &new_room.room.latest_event().await, room_list_service.client(), - ) - .await; + ).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()); @@ -3946,8 +3508,7 @@ async fn add_new_room( #[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() + let ignored_users = client.account() .account_data::() .await .ok()?? @@ -4011,9 +3572,7 @@ fn handle_load_app_state(user_id: OwnedUserId) { && !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, - )); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(app_state)); } } Err(_e) => { @@ -4032,12 +3591,12 @@ fn handle_load_app_state(user_id: OwnedUserId) { 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 - } + 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!( @@ -4119,12 +3678,14 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { 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); - + 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); + 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 { @@ -4137,10 +3698,7 @@ fn handle_sync_indicator_subscriber(sync_service: &SyncService) { } fn handle_room_list_service_loading_state(mut loading_state: Subscriber) { - log!( - "Initial room list loading state is {:?}", - loading_state.get() - ); + 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:?}"); @@ -4148,12 +3706,8 @@ fn handle_room_list_service_loading_state(mut loading_state: Subscriber { enqueue_rooms_list_update(RoomsListUpdate::NotLoaded); } - RoomListLoadingState::Loaded { - maximum_number_of_rooms, - } => { - enqueue_rooms_list_update(RoomsListUpdate::LoadedRooms { - max_rooms: maximum_number_of_rooms, - }); + 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; @@ -4176,12 +3730,12 @@ fn spawn_fetch_successor_room_preview( 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, - }, + 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 }) @@ -4215,18 +3769,12 @@ async fn fetch_room_preview_with_avatar( }; 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 - ); + 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 + 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()) } @@ -4246,10 +3794,7 @@ async fn fetch_room_preview_with_avatar( async fn fetch_thread_summary_details( room: &Room, thread_root_event_id: &EventId, -) -> ( - u32, - Option, -) { +) -> (u32, Option) { let mut num_replies = 0; let mut latest_reply_event = None; @@ -4307,7 +3852,10 @@ async fn fetch_latest_thread_reply_event( } /// 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 { +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; @@ -4320,10 +3868,7 @@ async fn count_thread_replies(room: &Room, thread_root_event_id: &EventId) -> Op ..Default::default() }; - let relations = room - .relations(thread_root_event_id.to_owned(), options) - .await - .ok()?; + let relations = room.relations(thread_root_event_id.to_owned(), options).await.ok()?; if relations.chunk.is_empty() { break; } @@ -4349,8 +3894,7 @@ async fn text_preview_of_latest_thread_reply( Ok(Some(rm)) => Some(rm), _ => room.get_member(&sender_id).await.ok().flatten(), }; - let sender_name = sender_room_member - .as_ref() + 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(|| { @@ -4367,6 +3911,7 @@ async fn text_preview_of_latest_thread_reply( } } + /// 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 @@ -4390,37 +3935,29 @@ async fn get_latest_event_details( match latest_event_value { LatestEventValue::None => None, - LatestEventValue::Remote { - timestamp, - sender, - is_own, - profile, - content, - } => { + 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); + 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: _, - } => { + 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); + 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. @@ -4428,9 +3965,10 @@ async fn get_latest_event_details( /// 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 - { + 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, @@ -4467,6 +4005,7 @@ async fn timeline_subscriber_handler( 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, @@ -4475,13 +4014,14 @@ async fn timeline_subscriber_handler( 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 + 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)) @@ -4490,13 +4030,11 @@ async fn timeline_subscriber_handler( } } + 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() - ); + 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(), @@ -4509,266 +4047,262 @@ async fn timeline_subscriber_handler( // 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)) - ); + 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()); + 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, + // 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, } - ).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, - }); - } + } 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(); + // 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)); } - 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; + 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. } - VectorDiff::PopFront => { - if LOG_TIMELINE_DIFFS { log!("timeline_subscriber: room {room_id}, thread {thread_root_event_id:?} diff PopFront"); } + // 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; - 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()); + } else { + index_of_first_change = min(index_of_first_change, index); 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 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)); + 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; } - - 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}"); } + } else { + found_target_event_id = find_target_event(&mut target_event_id, std::iter::once(&value)) + .map(|(i, ev)| (i + index, ev)); } - 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}"); } + + 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; } - 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; + 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.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; + 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; - } + 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; + 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(); + 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:?}!") + ); } - } - else => { - break; + // Send a Makepad-level signal to update this room's timeline UI view. + SignalToUI::set_ui_signal(); } } - } - error!( - "Error: unexpectedly ended timeline subscriber for room {room_id}, thread {thread_root_event_id:?}." - ); + 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. @@ -4793,13 +4327,8 @@ async fn room_avatar(room: &Room, room_name_id: &RoomNameId) -> FetchedRoomAvata _ => { 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 - { + 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()); } } @@ -4828,8 +4357,7 @@ async fn spawn_sso_server( // 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(), + status: "Please wait while Matrix builds and configures the client object for login.".into(), }); // Wait for the notification that the client has been built @@ -4850,21 +4378,19 @@ async fn spawn_sso_server( // 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() + 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/")) - { + && 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 - { + ).await { Ok(success) => client_and_session = Some(success), Err(e) => build_client_error = Some(e), } @@ -4873,12 +4399,10 @@ async fn spawn_sso_server( 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}" - ) + 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. @@ -4890,8 +4414,7 @@ async fn spawn_sso_server( 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(), + status: "Please finish logging in using your browser, and then come back to Robrix.".into(), }); match client .matrix_auth() @@ -4901,15 +4424,12 @@ async fn spawn_sso_server( if key == "redirectUrl" { let redirect_url = Url::parse(&value)?; Cx::post_action(LoginAction::SsoSetRedirectUrl(redirect_url)); - break; + break } } - Uri::new(&sso_url).open().map_err(|err| { - Error::Io(io::Error::other(format!( - "Unable to open SSO login url. Error: {:?}", - err - ))) - }) + 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}")) @@ -4924,13 +4444,10 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - if let Err(e) = login_sender - .send(LoginRequest::LoginBySSOSuccess(client, client_session)) - .await - { + 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.", + "BUG: failed to send login request to matrix worker thread." ))); } enqueue_rooms_list_update(RoomsListUpdate::Status { @@ -4956,6 +4473,7 @@ async fn spawn_sso_server( }); } + bitflags! { /// The powers that a user has in a given room. #[derive(Copy, Clone, PartialEq, Eq)] @@ -5033,38 +4551,14 @@ impl UserPowerLevels { 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.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 } @@ -5110,7 +4604,8 @@ impl UserPowerLevels { } pub fn can_send_message(self) -> bool { - self.contains(UserPowerLevels::RoomMessage) || self.contains(UserPowerLevels::Message) + self.contains(UserPowerLevels::RoomMessage) + || self.contains(UserPowerLevels::Message) } pub fn can_send_reaction(self) -> bool { @@ -5127,6 +4622,7 @@ impl UserPowerLevels { } } + /// 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() { @@ -5144,16 +4640,9 @@ pub async fn clear_app_state(config: &LogoutConfig) -> Result<()> { 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 - { + 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(()) From 9f1f5fd1e7185a1f127f1f12d60c0f7e91f37306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 09:30:43 +0800 Subject: [PATCH 029/283] chore: remove diff noise from bot management changes --- src/home/home_screen.rs | 1 + src/home/room_context_menu.rs | 8 +------- src/settings/settings_screen.rs | 21 +++------------------ 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 418c5214c..c4d34d2aa 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -512,3 +512,4 @@ impl HomeScreen { ) } } + diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index b2aaf90aa..9a73e08b8 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -3,13 +3,7 @@ use makepad_widgets::*; use matrix_sdk::ruma::OwnedRoomId; -use crate::{ - app::AppState, - home::invite_modal::InviteModalAction, - shared::popup_list::{PopupKind, enqueue_popup_notification}, - sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, - utils::RoomNameId, -}; +use crate::{app::AppState, home::invite_modal::InviteModalAction, shared::popup_list::{PopupKind, enqueue_popup_notification}, sliding_sync::{MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId}; const BUTTON_HEIGHT: f64 = 35.0; const MENU_WIDTH: f64 = 215.0; diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index 79c690997..5d24945bc 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -1,12 +1,7 @@ use makepad_widgets::*; -use crate::{ - app::BotSettingsState, - home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, - profile::user_profile::UserProfile, - settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}, -}; +use crate::{app::BotSettingsState, home::navigation_tab_bar::{NavigationBarAction, get_own_profile}, profile::user_profile::UserProfile, settings::{account_settings::AccountSettingsWidgetExt, bot_settings::BotSettingsWidgetExt}}; script_mod! { use mod.prelude.widgets.* @@ -173,12 +168,7 @@ impl Widget for SettingsScreen { impl SettingsScreen { /// Fetches the current user's profile and uses it to populate the settings screen. - pub fn populate( - &mut self, - cx: &mut Cx, - own_profile: Option, - bot_settings: &BotSettingsState, - ) { + pub fn populate(&mut self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState) { let Some(profile) = own_profile.or_else(|| get_own_profile(cx)) else { error!("Failed to get own profile for settings screen."); return; @@ -193,12 +183,7 @@ impl SettingsScreen { impl SettingsScreenRef { /// See [`SettingsScreen::populate()`]. - pub fn populate( - &self, - cx: &mut Cx, - own_profile: Option, - bot_settings: &BotSettingsState, - ) { + pub fn populate(&self, cx: &mut Cx, own_profile: Option, bot_settings: &BotSettingsState) { let Some(mut inner) = self.borrow_mut() else { return; }; inner.populate(cx, own_profile, bot_settings); } From 71b28853db84deae5a8b230a40d5218c079395a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 09:39:22 +0800 Subject: [PATCH 030/283] refactor: drop login and persistence changes from bot management --- src/app.rs | 2 +- src/login/login_screen.rs | 211 ++++------ src/persistence/matrix_state.rs | 74 +--- src/sliding_sync.rs | 667 +++++++++----------------------- 4 files changed, 246 insertions(+), 708 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2897cffc8..d20772c5d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -993,7 +993,7 @@ pub struct AppState { /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, /// Local configuration and UI state for bot-assisted room binding. - #[serde(default)] + #[serde(skip)] pub bot_settings: BotSettingsState, } diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index bf4ec59c2..3b3c322a1 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; +use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -69,13 +69,19 @@ script_mod! { } } - View { + RoundedView { margin: Inset{top: 40, bottom: 40} width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, + show_bg: true, + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 6.0 + } + View { width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit @@ -117,19 +123,6 @@ script_mod! { is_password: true, } - confirm_password_wrapper := View { - width: 275, height: Fit, - visible: false, - - confirm_password_input := RobrixTextInput { - width: 275, height: Fit - flow: Right, // do not wrap - padding: 10, - empty_text: "Confirm password" - is_password: true, - } - } - View { width: 275, height: Fit, flow: Down, @@ -178,61 +171,54 @@ script_mod! { text: "Login" } - login_only_view := View { - width: Fit, height: Fit, - flow: Down, - align: Align{x: 0.5, y: 0.5} - spacing: 15.0 + LineH { + width: 275 + margin: Inset{bottom: -5} + draw_bg.color: #C8C8C8 + } - LineH { - width: 275 - margin: Inset{bottom: -5} - draw_bg.color: #C8C8C8 + Label { + width: Fit, height: Fit + padding: 0, + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 11.0} } + text: "Or, login with an SSO provider:" + } - Label { - width: Fit, height: Fit - padding: 0, - draw_text +: { - color: (COLOR_TEXT) - text_style: TITLE_TEXT {font_size: 11.0} + sso_view := View { + width: 275, height: Fit, + margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide + flow: Flow.Right{wrap: true}, + apple_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/apple.png") } - text: "Or, login with an SSO provider:" } - - sso_view := View { - width: 275, height: Fit, - margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide - flow: Flow.Right{wrap: true}, - apple_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/apple.png") - } - } - facebook_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/facebook.png") - } + facebook_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/facebook.png") } - github_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/github.png") - } + } + github_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/github.png") } - gitlab_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/gitlab.png") - } + } + gitlab_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/gitlab.png") } - google_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/google.png") - } + } + google_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/google.png") } - twitter_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/x.png") - } + } + twitter_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/x.png") } } } @@ -247,7 +233,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - account_prompt_label := Label { + Label { width: Fit, height: Fit padding: Inset{left: 1, right: 1, top: 0, bottom: 0} draw_text +: { @@ -260,7 +246,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - mode_toggle_button := RobrixIconButton { + signup_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{bottom: 5} @@ -284,44 +270,16 @@ script_mod! { } } +static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; + #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { #[source] source: ScriptObjectRef, #[deref] view: View, - /// Whether the screen is showing the in-app sign-up flow. - #[rust] signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight #[rust] sso_pending: bool, /// The URL to redirect to after logging in with SSO. #[rust] sso_redirect_url: Option, - /// The most recent login failure message shown to the user. - #[rust] last_failure_message_shown: Option, -} - -impl LoginScreen { - fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { - self.signup_mode = signup_mode; - self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); - self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); - self.view.label(cx, ids!(title)).set_text(cx, - if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } - ); - self.view.button(cx, ids!(login_button)).set_text(cx, - if signup_mode { "Create account" } else { "Login" } - ); - self.view.label(cx, ids!(account_prompt_label)).set_text(cx, - if signup_mode { "Already have an account?" } else { "Don't have an account?" } - ); - self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, - if signup_mode { "Back to login" } else { "Sign up here" } - ); - - if !signup_mode { - self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); - } - - self.redraw(cx); - } } @@ -339,29 +297,27 @@ impl Widget for LoginScreen { impl MatchEvent for LoginScreen { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let login_button = self.view.button(cx, ids!(login_button)); - let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); + let signup_button = self.view.button(cx, ids!(signup_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); let password_input = self.view.text_input(cx, ids!(password_input)); - let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); - if mode_toggle_button.clicked(actions) { - self.set_signup_mode(cx, !self.signup_mode); + if signup_button.clicked(actions) { + log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); + let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); } if login_button.clicked(actions) || user_id_input.returned(actions).is_some() || password_input.returned(actions).is_some() - || (self.signup_mode && confirm_password_input.returned(actions).is_some()) || homeserver_input.returned(actions).is_some() { - let user_id = user_id_input.text().trim().to_owned(); + let user_id = user_id_input.text(); let password = password_input.text(); - let confirm_password = confirm_password_input.text(); - let homeserver = homeserver_input.text().trim().to_owned(); + let homeserver = homeserver_input.text(); if user_id.is_empty() { login_status_modal_inner.set_title(cx, "Missing User ID"); login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); @@ -370,39 +326,15 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, "Missing Password"); login_status_modal_inner.set_status(cx, "Please enter a valid password."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); - } else if self.signup_mode && password != confirm_password { - login_status_modal_inner.set_title(cx, "Passwords do not match"); - login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { - self.last_failure_message_shown = None; - login_status_modal_inner.set_title(cx, if self.signup_mode { - "Creating account..." - } else { - "Logging in..." - }); - login_status_modal_inner.set_status( - cx, - if self.signup_mode { - "Waiting for the homeserver to create your account..." - } else { - "Waiting for a login response..." - }, - ); + login_status_modal_inner.set_title(cx, "Logging in..."); + login_status_modal_inner.set_status(cx, "Waiting for a login response..."); login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); - submit_async_request(MatrixRequest::Login(if self.signup_mode { - LoginRequest::Register(RegisterAccount { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }) - } else { - LoginRequest::LoginByPassword(LoginByPassword { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }) - })); + submit_async_request(MatrixRequest::Login(LoginRequest::LoginByPassword(LoginByPassword { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }))); } login_status_modal.open(cx); self.redraw(cx); @@ -425,7 +357,6 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { - self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); @@ -440,7 +371,6 @@ impl MatchEvent for LoginScreen { login_status_modal.open(cx); } Some(LoginAction::Status { title, status }) => { - self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); @@ -452,25 +382,14 @@ impl MatchEvent for LoginScreen { Some(LoginAction::LoginSuccess) => { // The main `App` component handles showing the main screen // and hiding the login screen & login status modal. - self.last_failure_message_shown = None; - self.set_signup_mode(cx, false); user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); - confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); login_status_modal.close(cx); self.redraw(cx); } Some(LoginAction::LoginFailure(error)) => { - if self.last_failure_message_shown.as_deref() == Some(error.as_str()) { - continue; - } - self.last_failure_message_shown = Some(error.clone()); - login_status_modal_inner.set_title(cx, if self.signup_mode { - "Account Creation Failed." - } else { - "Login Failed." - }); + login_status_modal_inner.set_title(cx, "Login Failed."); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index f7d09bdf8..d99855b7c 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -1,8 +1,8 @@ //! Handles app persistence by saving and restoring client session data to/from the filesystem. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use anyhow::{anyhow, bail}; -use makepad_widgets::{log, warning, Cx}; +use makepad_widgets::{log, Cx}; use matrix_sdk::{ authentication::matrix::MatrixSession, ruma::{OwnedUserId, UserId}, @@ -254,73 +254,3 @@ pub async fn delete_latest_user_id() -> anyhow::Result { Ok(false) } } - -async fn delete_path_if_exists(path: &Path) -> anyhow::Result { - let metadata = match tokio::fs::metadata(path).await { - Ok(metadata) => metadata, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), - Err(e) => return Err(anyhow!("Failed to inspect path {}: {e}", path.display())), - }; - - if metadata.is_dir() { - tokio::fs::remove_dir_all(path) - .await - .map_err(|e| anyhow!("Failed to remove directory {}: {e}", path.display()))?; - } else { - tokio::fs::remove_file(path) - .await - .map_err(|e| anyhow!("Failed to remove file {}: {e}", path.display()))?; - } - - Ok(true) -} - -/// Remove the persisted Matrix session file for the given user if it exists. -/// -/// Returns: -/// - Ok(true) if the session file was found and deleted -/// - Ok(false) if the session file didn't exist -/// - Err if deletion failed -pub async fn delete_session(user_id: &UserId) -> anyhow::Result { - let session_file = session_file_path(user_id); - - if session_file.exists() { - let persisted_db_path = match tokio::fs::read_to_string(&session_file).await { - Ok(serialized_session) => { - match serde_json::from_str::(&serialized_session) { - Ok(session) => Some(session.client_session.db_path), - Err(e) => { - warning!( - "Failed to parse session file {} before cleanup: {e}", - session_file.display() - ); - None - } - } - } - Err(e) => { - warning!( - "Failed to read session file {} before cleanup: {e}", - session_file.display() - ); - None - } - }; - - if let Some(db_path) = persisted_db_path { - if let Err(e) = delete_path_if_exists(&db_path).await { - warning!( - "Failed to remove persisted Matrix store {} for {user_id}: {e}", - db_path.display() - ); - } - } - - tokio::fs::remove_file(&session_file) - .await - .map_err(|e| anyhow::anyhow!("Failed to remove session file {session_file:?}: {e}")) - .map(|_| true) - } else { - Ok(false) - } -} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 255332260..489efba51 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -9,13 +9,7 @@ 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::{ - account::register::v3::Request as RegistrationRequest, - error::ErrorKind, - profile::{AvatarUrl, DisplayName}, - receipt::create_receipt::v3::ReceiptType, - uiaa::{AuthData, AuthType, Dummy}, - }}, events::{ + api::{Direction, client::{profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType}}, events::{ relation::RelationType, room::{ message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource @@ -90,11 +84,9 @@ impl std::fmt::Debug for Cli { impl From for Cli { fn from(login: LoginByPassword) -> Self { Self { - user_id: login.user_id.trim().to_owned(), + user_id: login.user_id, password: login.password, - homeserver: login.homeserver - .map(|homeserver| homeserver.trim().to_owned()) - .filter(|homeserver| !homeserver.is_empty()), + homeserver: login.homeserver, proxy: None, login_screen: false, verbose: false, @@ -102,186 +94,6 @@ impl From for Cli { } } -impl From for Cli { - fn from(registration: RegisterAccount) -> Self { - Self { - user_id: registration.user_id.trim().to_owned(), - password: registration.password, - homeserver: registration.homeserver - .map(|homeserver| homeserver.trim().to_owned()) - .filter(|homeserver| !homeserver.is_empty()), - proxy: None, - login_screen: false, - verbose: false, - } - } -} - -fn infer_homeserver_from_user_id(user_id: &str) -> Option { - let user_id: OwnedUserId = user_id.trim().try_into().ok()?; - Some(user_id.server_name().to_string()) -} - -async fn finalize_authenticated_client( - client: Client, - client_session: ClientSessionPersisted, - fallback_user_id: &str, -) -> Result<(Client, Option)> { - if client.matrix_auth().logged_in() { - let logged_in_user_id = client.user_id() - .map(ToString::to_string) - .unwrap_or_else(|| fallback_user_id.to_owned()); - log!("Logged in successfully."); - let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); - 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!( - "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." - ); - enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); - bail!(err_msg); - } -} - -fn registration_localpart(user_id: &str) -> Result { - let trimmed = user_id.trim(); - if trimmed.is_empty() { - bail!("Please enter a valid username or Matrix user ID."); - } - - if let Ok(full_user_id) = >::try_from(trimmed) { - return Ok(full_user_id.localpart().to_owned()); - } - - let localpart = trimmed.trim_start_matches('@'); - if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { - bail!("Please enter a valid username or full Matrix user ID."); - } - - Ok(localpart.to_owned()) -} - -fn registration_request( - username: &str, - password: &str, - session: Option, -) -> RegistrationRequest { - let mut request = RegistrationRequest::new(); - request.username = Some(username.to_owned()); - request.password = Some(password.to_owned()); - request.initial_device_display_name = Some("robrix-un-pw".to_owned()); - request.refresh_token = true; - if let Some(session) = session { - let mut dummy = Dummy::new(); - dummy.session = Some(session); - request.auth = Some(AuthData::Dummy(dummy)); - } - request -} - -fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { - if let matrix_sdk::Error::Http(http_error) = error { - match http_error.client_api_error_kind() { - Some(ErrorKind::UserInUse) => { - return "That user ID is already taken. Please choose another one.".to_owned(); - } - Some(ErrorKind::InvalidUsername) => { - return "That user ID is invalid. Use a username like `alice` or a full Matrix ID like `@alice:matrix.org`.".to_owned(); - } - Some(ErrorKind::WeakPassword) => { - return "That password is too weak. Please choose a stronger password.".to_owned(); - } - Some(ErrorKind::Forbidden { .. }) => { - return "This homeserver does not allow open registration.".to_owned(); - } - Some(ErrorKind::LimitExceeded { .. }) => { - return "The homeserver is rate limiting account creation right now. Please try again shortly.".to_owned(); - } - _ => {} - } - } - - format!("Could not create account: {error}") -} - -fn unsupported_registration_flow_message( - flows: &[matrix_sdk::ruma::api::client::uiaa::AuthFlow], -) -> String { - let supports_registration_token = flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::RegistrationToken)) - }); - if supports_registration_token { - return "This homeserver requires a registration token. Robrix does not support token-based registration yet.".to_owned(); - } - - let supports_terms = flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::Terms)) - }); - if supports_terms { - return "This homeserver requires an interactive terms-of-service step. Robrix does not support that registration flow yet.".to_owned(); - } - - "This homeserver requires an unsupported registration flow. Please try another homeserver or register with a different client.".to_owned() -} - -async fn clear_persisted_session(user_id: Option<&UserId>) { - let Some(user_id) = user_id else { - return; - }; - - if let Err(e) = persistence::delete_session(user_id).await { - warning!("Failed to delete persisted session for {user_id}: {e}"); - } - - let latest_user_id = persistence::most_recent_user_id().await; - if latest_user_id.as_deref() == Some(user_id) { - if let Err(e) = persistence::delete_latest_user_id().await { - warning!("Failed to delete latest user id for {user_id}: {e}"); - } - } -} - -enum SessionResetAction { - Reauthenticate { message: String }, -} - -async fn reset_runtime_state_for_relogin() { - let sync_service = { SYNC_SERVICE.lock().unwrap().take() }; - if let Some(sync_service) = sync_service { - sync_service.stop().await; - } - - CLIENT.lock().unwrap().take(); - DEFAULT_SSO_CLIENT.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() }); - - if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { - warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); - } -} - -fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { - matches!( - error.client_api_error_kind(), - Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) - ) -} - /// Build a new client. async fn build_client( @@ -304,10 +116,7 @@ async fn build_client( .collect() }; - let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); let homeserver_url = cli.homeserver.as_deref() - .filter(|homeserver| !homeserver.trim().is_empty()) - .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); // .unwrap_or("https://matrix.org/"); @@ -382,71 +191,23 @@ async fn login( .initial_device_display_name("robrix-un-pw") .send() .await?; - if !client.matrix_auth().logged_in() { - 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); - } - finalize_authenticated_client(client, client_session, &cli.user_id).await - } - - LoginRequest::Register(registration) => { - let cli = Cli::from(RegisterAccount { - user_id: registration.user_id.clone(), - password: registration.password.clone(), - homeserver: registration.homeserver.clone(), - }); - let localpart = registration_localpart(®istration.user_id)?; - let (client, client_session) = build_client(&cli, app_data_dir()).await?; - Cx::post_action(LoginAction::Status { - title: "Creating account".into(), - status: format!("Creating account {localpart}..."), - }); - - let auth = client.matrix_auth(); - let initial_request = registration_request(&localpart, ®istration.password, None); - let register_result = match auth.register(initial_request).await { - Ok(response) => Ok(response), - Err(error) => { - if let Some(uiaa_info) = error.as_uiaa_response() { - let supports_dummy = uiaa_info.flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::Dummy)) - }); - if supports_dummy { - Cx::post_action(LoginAction::Status { - title: "Completing sign up".into(), - status: "Confirming registration with the homeserver...".into(), - }); - auth.register(registration_request( - &localpart, - ®istration.password, - uiaa_info.session.clone(), - )) - .await - } else { - bail!(unsupported_registration_flow_message(&uiaa_info.flows)); - } - } else { - bail!(registration_uiaa_error_message(&error)); - } + 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); } - }?; - - if !client.matrix_auth().logged_in() { - let err_msg = format!( - "Account {} was created, but the homeserver did not return a login session. Please log in manually.", - register_result.user_id, - ); + 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); } - - finalize_authenticated_client(client, client_session, register_result.user_id.as_str()) - .await } LoginRequest::LoginBySSOSuccess(client, client_session) => { @@ -926,7 +687,6 @@ pub fn submit_async_request(req: MatrixRequest) { /// Details of a login request that get submitted within [`MatrixRequest::Login`]. pub enum LoginRequest{ LoginByPassword(LoginByPassword), - Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), @@ -939,14 +699,6 @@ pub struct LoginByPassword { pub homeserver: Option, } -/// Information needed to register a new account on a Matrix homeserver. -#[derive(Clone)] -pub struct RegisterAccount { - pub user_id: String, - pub password: String, - pub homeserver: Option, -} - /// The entry point for the worker task that runs Matrix-related operations. /// @@ -2607,7 +2359,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { ); log!("Waiting for login? {}", wait_for_login); - let new_login_opt: Option<(Client, Option, bool)> = if !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, @@ -2617,17 +2369,11 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { log!("Trying to restore session for user: {:?}", specified_username.as_ref().or(most_recent_user_id.as_ref()) ); - match persistence::restore_session(specified_username.clone()).await { - Ok((client, sync_token)) => Some((client, sync_token, true)), + 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:?}"); - clear_persisted_session( - specified_username - .as_deref() - .or(most_recent_user_id.as_deref()), - ) - .await; Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); if let Ok(cli) = &cli_parse_result { @@ -2637,7 +2383,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { homeserver: cli.homeserver.clone(), }); match login(cli, LoginRequest::LoginByCli).await { - Ok((client, sync_token)) => Some((client, sync_token, false)), + Ok(new_login) => Some(new_login), Err(e) => { error!("CLI-based login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure( @@ -2663,247 +2409,197 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // which causes the loop to wait for the user to submit a new manual login request. let mut initial_client_opt = new_login_opt; - loop { - let (client, sync_service, logged_in_user_id) = 'login_loop: loop { - let (client, _sync_token, validate_session) = 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, false), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e}"), - }); - } + 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; } + }, + 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; } } } - }; - - if validate_session { - match client.whoami().await { - Ok(_) => {} - Err(e) if is_invalid_token_http_error(&e) => { - clear_persisted_session(client.user_id()).await; - let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; - Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.to_string(), - }); - continue 'login_loop; - } - Err(e) => { - warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); - } - } } + }; - // Deallocate the default SSO client after a successful login. - if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { - let _ = client_opt.take(); - } + // 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 }); + 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."); - } + // 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 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 updates to the ignored user list. + handle_ignore_user_list_subscriber(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}.") - }; - if is_invalid_token_error(&e) { - clear_persisted_session(client.user_id()).await; - } - 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; - } - }; + // Listen for session changes, e.g., when the access token becomes invalid. + handle_session_changes(client.clone()); - break 'login_loop (client, sync_service, logged_in_user_id); + 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; + } }; - let (session_reset_sender, mut session_reset_receiver) = - tokio::sync::mpsc::unbounded_channel::(); - let session_change_handler_task = - handle_session_changes(client.clone(), session_reset_sender); + 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); + // 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; + // 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(); + 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."); - } + 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 - // matrix/background tasks for the currently-authenticated session. - #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. - let reauth_message = loop { - tokio::select! { - session_reset = session_reset_receiver.recv() => { - match session_reset { - Some(SessionResetAction::Reauthenticate { message }) => { - break message; - } - None => { - warning!("Session reset receiver closed unexpectedly."); - continue; - } - } - } - result = &mut matrix_worker_task_handle => { - session_change_handler_task.abort(); - 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:?}"); + 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!"); } } - return; - } - result = &mut room_list_service_task => { - session_change_handler_task.abort(); - 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:?}"); + 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!("Room list service error: {e}"), + format!("Rooms list update error: {e}"), PopupKind::Error, None, ); - }, - Err(e) => { - error!("BUG: failed to join room list service loop task: {e:?}"); } + }, + Err(e) => { + error!("BUG: failed to join matrix worker task: {e:?}"); } - return; } - result = &mut space_service_task => { - session_change_handler_task.abort(); - 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; + } + 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:?}"); } - return; } + break; } - }; - - session_change_handler_task.abort(); - room_list_service_task.abort(); - space_service_task.abort(); - - reset_runtime_state_for_relogin().await; - Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: reauth_message, - }); - initial_client_opt = None; + 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; + } + } } } @@ -3610,10 +3306,7 @@ fn is_invalid_token_error(e: &sync_service::Error) -> bool { /// 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, - session_reset_sender: UnboundedSender, -) -> JoinHandle<()> { +fn handle_session_changes(client: Client) { let mut receiver = client.subscribe_to_session_changes(); Handle::current().spawn(async move { loop { @@ -3625,11 +3318,7 @@ fn handle_session_changes( "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."); - clear_persisted_session(client.user_id()).await; - let _ = session_reset_sender.send(SessionResetAction::Reauthenticate { - message: msg.to_string(), - }); - break; + Cx::post_action(LoginAction::LoginFailure(msg.to_string())); } Ok(SessionChange::TokensRefreshed) => {} Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3640,7 +3329,7 @@ fn handle_session_changes( } } } - }) + }); } fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { From d417e92d5f08d26ac115fcc3e664808c8c7ad6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 09:49:13 +0800 Subject: [PATCH 031/283] refactor: align bot management branch with robrix2 main --- src/login/login_screen.rs | 211 ++++++---- src/persistence/matrix_state.rs | 74 +++- src/sliding_sync.rs | 667 +++++++++++++++++++++++--------- 3 files changed, 707 insertions(+), 245 deletions(-) diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 3b3c322a1..bf4ec59c2 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest}; +use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -69,19 +69,13 @@ script_mod! { } } - RoundedView { + View { margin: Inset{top: 40, bottom: 40} width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, - show_bg: true, - draw_bg +: { - color: (COLOR_SECONDARY) - border_radius: 6.0 - } - View { width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit @@ -123,6 +117,19 @@ script_mod! { is_password: true, } + confirm_password_wrapper := View { + width: 275, height: Fit, + visible: false, + + confirm_password_input := RobrixTextInput { + width: 275, height: Fit + flow: Right, // do not wrap + padding: 10, + empty_text: "Confirm password" + is_password: true, + } + } + View { width: 275, height: Fit, flow: Down, @@ -171,54 +178,61 @@ script_mod! { text: "Login" } - LineH { - width: 275 - margin: Inset{bottom: -5} - draw_bg.color: #C8C8C8 - } + login_only_view := View { + width: Fit, height: Fit, + flow: Down, + align: Align{x: 0.5, y: 0.5} + spacing: 15.0 - Label { - width: Fit, height: Fit - padding: 0, - draw_text +: { - color: (COLOR_TEXT) - text_style: TITLE_TEXT {font_size: 11.0} + LineH { + width: 275 + margin: Inset{bottom: -5} + draw_bg.color: #C8C8C8 } - text: "Or, login with an SSO provider:" - } - sso_view := View { - width: 275, height: Fit, - margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide - flow: Flow.Right{wrap: true}, - apple_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/apple.png") + Label { + width: Fit, height: Fit + padding: 0, + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 11.0} } + text: "Or, login with an SSO provider:" } - facebook_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/facebook.png") + + sso_view := View { + width: 275, height: Fit, + margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide + flow: Flow.Right{wrap: true}, + apple_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/apple.png") + } } - } - github_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/github.png") + facebook_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/facebook.png") + } } - } - gitlab_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/gitlab.png") + github_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/github.png") + } } - } - google_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/google.png") + gitlab_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/gitlab.png") + } } - } - twitter_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/x.png") + google_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/google.png") + } + } + twitter_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/x.png") + } } } } @@ -233,7 +247,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - Label { + account_prompt_label := Label { width: Fit, height: Fit padding: Inset{left: 1, right: 1, top: 0, bottom: 0} draw_text +: { @@ -246,7 +260,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - signup_button := RobrixIconButton { + mode_toggle_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{bottom: 5} @@ -270,16 +284,44 @@ script_mod! { } } -static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; - #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { #[source] source: ScriptObjectRef, #[deref] view: View, + /// Whether the screen is showing the in-app sign-up flow. + #[rust] signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight #[rust] sso_pending: bool, /// The URL to redirect to after logging in with SSO. #[rust] sso_redirect_url: Option, + /// The most recent login failure message shown to the user. + #[rust] last_failure_message_shown: Option, +} + +impl LoginScreen { + fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { + self.signup_mode = signup_mode; + self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); + self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); + self.view.label(cx, ids!(title)).set_text(cx, + if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } + ); + self.view.button(cx, ids!(login_button)).set_text(cx, + if signup_mode { "Create account" } else { "Login" } + ); + self.view.label(cx, ids!(account_prompt_label)).set_text(cx, + if signup_mode { "Already have an account?" } else { "Don't have an account?" } + ); + self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, + if signup_mode { "Back to login" } else { "Sign up here" } + ); + + if !signup_mode { + self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); + } + + self.redraw(cx); + } } @@ -297,27 +339,29 @@ impl Widget for LoginScreen { impl MatchEvent for LoginScreen { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let login_button = self.view.button(cx, ids!(login_button)); - let signup_button = self.view.button(cx, ids!(signup_button)); + let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); let password_input = self.view.text_input(cx, ids!(password_input)); + let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); - if signup_button.clicked(actions) { - log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); - let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); + if mode_toggle_button.clicked(actions) { + self.set_signup_mode(cx, !self.signup_mode); } if login_button.clicked(actions) || user_id_input.returned(actions).is_some() || password_input.returned(actions).is_some() + || (self.signup_mode && confirm_password_input.returned(actions).is_some()) || homeserver_input.returned(actions).is_some() { - let user_id = user_id_input.text(); + let user_id = user_id_input.text().trim().to_owned(); let password = password_input.text(); - let homeserver = homeserver_input.text(); + let confirm_password = confirm_password_input.text(); + let homeserver = homeserver_input.text().trim().to_owned(); if user_id.is_empty() { login_status_modal_inner.set_title(cx, "Missing User ID"); login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); @@ -326,15 +370,39 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, "Missing Password"); login_status_modal_inner.set_status(cx, "Please enter a valid password."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + } else if self.signup_mode && password != confirm_password { + login_status_modal_inner.set_title(cx, "Passwords do not match"); + login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); + login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { - login_status_modal_inner.set_title(cx, "Logging in..."); - login_status_modal_inner.set_status(cx, "Waiting for a login response..."); + self.last_failure_message_shown = None; + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Creating account..." + } else { + "Logging in..." + }); + login_status_modal_inner.set_status( + cx, + if self.signup_mode { + "Waiting for the homeserver to create your account..." + } else { + "Waiting for a login response..." + }, + ); login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); - submit_async_request(MatrixRequest::Login(LoginRequest::LoginByPassword(LoginByPassword { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }))); + submit_async_request(MatrixRequest::Login(if self.signup_mode { + LoginRequest::Register(RegisterAccount { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }) + } else { + LoginRequest::LoginByPassword(LoginByPassword { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }) + })); } login_status_modal.open(cx); self.redraw(cx); @@ -357,6 +425,7 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { + self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); @@ -371,6 +440,7 @@ impl MatchEvent for LoginScreen { login_status_modal.open(cx); } Some(LoginAction::Status { title, status }) => { + self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); @@ -382,14 +452,25 @@ impl MatchEvent for LoginScreen { Some(LoginAction::LoginSuccess) => { // The main `App` component handles showing the main screen // and hiding the login screen & login status modal. + self.last_failure_message_shown = None; + self.set_signup_mode(cx, false); user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); + confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); login_status_modal.close(cx); self.redraw(cx); } Some(LoginAction::LoginFailure(error)) => { - login_status_modal_inner.set_title(cx, "Login Failed."); + if self.last_failure_message_shown.as_deref() == Some(error.as_str()) { + continue; + } + self.last_failure_message_shown = Some(error.clone()); + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Account Creation Failed." + } else { + "Login Failed." + }); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index d99855b7c..f7d09bdf8 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -1,8 +1,8 @@ //! Handles app persistence by saving and restoring client session data to/from the filesystem. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail}; -use makepad_widgets::{log, Cx}; +use makepad_widgets::{log, warning, Cx}; use matrix_sdk::{ authentication::matrix::MatrixSession, ruma::{OwnedUserId, UserId}, @@ -254,3 +254,73 @@ pub async fn delete_latest_user_id() -> anyhow::Result { Ok(false) } } + +async fn delete_path_if_exists(path: &Path) -> anyhow::Result { + let metadata = match tokio::fs::metadata(path).await { + Ok(metadata) => metadata, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false), + Err(e) => return Err(anyhow!("Failed to inspect path {}: {e}", path.display())), + }; + + if metadata.is_dir() { + tokio::fs::remove_dir_all(path) + .await + .map_err(|e| anyhow!("Failed to remove directory {}: {e}", path.display()))?; + } else { + tokio::fs::remove_file(path) + .await + .map_err(|e| anyhow!("Failed to remove file {}: {e}", path.display()))?; + } + + Ok(true) +} + +/// Remove the persisted Matrix session file for the given user if it exists. +/// +/// Returns: +/// - Ok(true) if the session file was found and deleted +/// - Ok(false) if the session file didn't exist +/// - Err if deletion failed +pub async fn delete_session(user_id: &UserId) -> anyhow::Result { + let session_file = session_file_path(user_id); + + if session_file.exists() { + let persisted_db_path = match tokio::fs::read_to_string(&session_file).await { + Ok(serialized_session) => { + match serde_json::from_str::(&serialized_session) { + Ok(session) => Some(session.client_session.db_path), + Err(e) => { + warning!( + "Failed to parse session file {} before cleanup: {e}", + session_file.display() + ); + None + } + } + } + Err(e) => { + warning!( + "Failed to read session file {} before cleanup: {e}", + session_file.display() + ); + None + } + }; + + if let Some(db_path) = persisted_db_path { + if let Err(e) = delete_path_if_exists(&db_path).await { + warning!( + "Failed to remove persisted Matrix store {} for {user_id}: {e}", + db_path.display() + ); + } + } + + tokio::fs::remove_file(&session_file) + .await + .map_err(|e| anyhow::anyhow!("Failed to remove session file {session_file:?}: {e}")) + .map(|_| true) + } else { + Ok(false) + } +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 489efba51..255332260 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -9,7 +9,13 @@ 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::{ + api::{Direction, client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }}, events::{ relation::RelationType, room::{ message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource @@ -84,9 +90,11 @@ impl std::fmt::Debug for Cli { impl From for Cli { fn from(login: LoginByPassword) -> Self { Self { - user_id: login.user_id, + user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login.homeserver, + homeserver: login.homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), proxy: None, login_screen: false, verbose: false, @@ -94,6 +102,186 @@ impl From for Cli { } } +impl From for Cli { + fn from(registration: RegisterAccount) -> Self { + Self { + user_id: registration.user_id.trim().to_owned(), + password: registration.password, + homeserver: registration.homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), + proxy: None, + login_screen: false, + verbose: false, + } + } +} + +fn infer_homeserver_from_user_id(user_id: &str) -> Option { + let user_id: OwnedUserId = user_id.trim().try_into().ok()?; + Some(user_id.server_name().to_string()) +} + +async fn finalize_authenticated_client( + client: Client, + client_session: ClientSessionPersisted, + fallback_user_id: &str, +) -> Result<(Client, Option)> { + if client.matrix_auth().logged_in() { + let logged_in_user_id = client.user_id() + .map(ToString::to_string) + .unwrap_or_else(|| fallback_user_id.to_owned()); + log!("Logged in successfully."); + let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); + 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!( + "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + bail!(err_msg); + } +} + +fn registration_localpart(user_id: &str) -> Result { + let trimmed = user_id.trim(); + if trimmed.is_empty() { + bail!("Please enter a valid username or Matrix user ID."); + } + + if let Ok(full_user_id) = >::try_from(trimmed) { + return Ok(full_user_id.localpart().to_owned()); + } + + let localpart = trimmed.trim_start_matches('@'); + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { + bail!("Please enter a valid username or full Matrix user ID."); + } + + Ok(localpart.to_owned()) +} + +fn registration_request( + username: &str, + password: &str, + session: Option, +) -> RegistrationRequest { + let mut request = RegistrationRequest::new(); + request.username = Some(username.to_owned()); + request.password = Some(password.to_owned()); + request.initial_device_display_name = Some("robrix-un-pw".to_owned()); + request.refresh_token = true; + if let Some(session) = session { + let mut dummy = Dummy::new(); + dummy.session = Some(session); + request.auth = Some(AuthData::Dummy(dummy)); + } + request +} + +fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { + if let matrix_sdk::Error::Http(http_error) = error { + match http_error.client_api_error_kind() { + Some(ErrorKind::UserInUse) => { + return "That user ID is already taken. Please choose another one.".to_owned(); + } + Some(ErrorKind::InvalidUsername) => { + return "That user ID is invalid. Use a username like `alice` or a full Matrix ID like `@alice:matrix.org`.".to_owned(); + } + Some(ErrorKind::WeakPassword) => { + return "That password is too weak. Please choose a stronger password.".to_owned(); + } + Some(ErrorKind::Forbidden { .. }) => { + return "This homeserver does not allow open registration.".to_owned(); + } + Some(ErrorKind::LimitExceeded { .. }) => { + return "The homeserver is rate limiting account creation right now. Please try again shortly.".to_owned(); + } + _ => {} + } + } + + format!("Could not create account: {error}") +} + +fn unsupported_registration_flow_message( + flows: &[matrix_sdk::ruma::api::client::uiaa::AuthFlow], +) -> String { + let supports_registration_token = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::RegistrationToken)) + }); + if supports_registration_token { + return "This homeserver requires a registration token. Robrix does not support token-based registration yet.".to_owned(); + } + + let supports_terms = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Terms)) + }); + if supports_terms { + return "This homeserver requires an interactive terms-of-service step. Robrix does not support that registration flow yet.".to_owned(); + } + + "This homeserver requires an unsupported registration flow. Please try another homeserver or register with a different client.".to_owned() +} + +async fn clear_persisted_session(user_id: Option<&UserId>) { + let Some(user_id) = user_id else { + return; + }; + + if let Err(e) = persistence::delete_session(user_id).await { + warning!("Failed to delete persisted session for {user_id}: {e}"); + } + + let latest_user_id = persistence::most_recent_user_id().await; + if latest_user_id.as_deref() == Some(user_id) { + if let Err(e) = persistence::delete_latest_user_id().await { + warning!("Failed to delete latest user id for {user_id}: {e}"); + } + } +} + +enum SessionResetAction { + Reauthenticate { message: String }, +} + +async fn reset_runtime_state_for_relogin() { + let sync_service = { SYNC_SERVICE.lock().unwrap().take() }; + if let Some(sync_service) = sync_service { + sync_service.stop().await; + } + + CLIENT.lock().unwrap().take(); + DEFAULT_SSO_CLIENT.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() }); + + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { + warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); + } +} + +fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { + matches!( + error.client_api_error_kind(), + Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) + ) +} + /// Build a new client. async fn build_client( @@ -116,7 +304,10 @@ async fn build_client( .collect() }; + let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); let homeserver_url = cli.homeserver.as_deref() + .filter(|homeserver| !homeserver.trim().is_empty()) + .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); // .unwrap_or("https://matrix.org/"); @@ -191,23 +382,71 @@ async fn login( .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 { + if !client.matrix_auth().logged_in() { 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); } + finalize_authenticated_client(client, client_session, &cli.user_id).await + } + + LoginRequest::Register(registration) => { + let cli = Cli::from(RegisterAccount { + user_id: registration.user_id.clone(), + password: registration.password.clone(), + homeserver: registration.homeserver.clone(), + }); + let localpart = registration_localpart(®istration.user_id)?; + let (client, client_session) = build_client(&cli, app_data_dir()).await?; + Cx::post_action(LoginAction::Status { + title: "Creating account".into(), + status: format!("Creating account {localpart}..."), + }); + + let auth = client.matrix_auth(); + let initial_request = registration_request(&localpart, ®istration.password, None); + let register_result = match auth.register(initial_request).await { + Ok(response) => Ok(response), + Err(error) => { + if let Some(uiaa_info) = error.as_uiaa_response() { + let supports_dummy = uiaa_info.flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Dummy)) + }); + if supports_dummy { + Cx::post_action(LoginAction::Status { + title: "Completing sign up".into(), + status: "Confirming registration with the homeserver...".into(), + }); + auth.register(registration_request( + &localpart, + ®istration.password, + uiaa_info.session.clone(), + )) + .await + } else { + bail!(unsupported_registration_flow_message(&uiaa_info.flows)); + } + } else { + bail!(registration_uiaa_error_message(&error)); + } + } + }?; + + if !client.matrix_auth().logged_in() { + let err_msg = format!( + "Account {} was created, but the homeserver did not return a login session. Please log in manually.", + register_result.user_id, + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + bail!(err_msg); + } + + finalize_authenticated_client(client, client_session, register_result.user_id.as_str()) + .await } LoginRequest::LoginBySSOSuccess(client, client_session) => { @@ -687,6 +926,7 @@ pub fn submit_async_request(req: MatrixRequest) { /// Details of a login request that get submitted within [`MatrixRequest::Login`]. pub enum LoginRequest{ LoginByPassword(LoginByPassword), + Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted), LoginByCli, HomeserverLoginTypesQuery(String), @@ -699,6 +939,14 @@ pub struct LoginByPassword { pub homeserver: Option, } +/// Information needed to register a new account on a Matrix homeserver. +#[derive(Clone)] +pub struct RegisterAccount { + pub user_id: String, + pub password: String, + pub homeserver: Option, +} + /// The entry point for the worker task that runs Matrix-related operations. /// @@ -2359,7 +2607,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { ); log!("Waiting for login? {}", wait_for_login); - let new_login_opt = if !wait_for_login { + let new_login_opt: Option<(Client, Option, bool)> = if !wait_for_login { let specified_username = cli_parse_result.as_ref().ok().and_then(|cli| username_to_full_user_id( &cli.user_id, @@ -2369,11 +2617,17 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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), + match persistence::restore_session(specified_username.clone()).await { + Ok((client, sync_token)) => Some((client, sync_token, true)), Err(e) => { let status_err = "Could not restore previous user session.\n\nPlease login again."; log!("{status_err} Error: {e:?}"); + clear_persisted_session( + specified_username + .as_deref() + .or(most_recent_user_id.as_deref()), + ) + .await; Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); if let Ok(cli) = &cli_parse_result { @@ -2383,7 +2637,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { homeserver: cli.homeserver.clone(), }); match login(cli, LoginRequest::LoginByCli).await { - Ok(new_login) => Some(new_login), + Ok((client, sync_token)) => Some((client, sync_token, false)), Err(e) => { error!("CLI-based login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure( @@ -2409,197 +2663,247 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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}"), - }); + loop { + let (client, sync_service, logged_in_user_id) = 'login_loop: loop { + let (client, _sync_token, validate_session) = 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, false), + 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; } - }, - 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; } } } + }; + + if validate_session { + match client.whoami().await { + Ok(_) => {} + Err(e) if is_invalid_token_http_error(&e) => { + clear_persisted_session(client.user_id()).await; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; + Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.to_string(), + }); + continue 'login_loop; + } + Err(e) => { + warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); + } + } } - }; - // Deallocate the default SSO client after a successful login. - if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { - let _ = client_opt.take(); - } + // 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 }); + 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."); - } + // 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 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 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}.") + }; + if is_invalid_token_error(&e) { + clear_persisted_session(client.user_id()).await; + } + 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; + } + }; - 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); }; - 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); + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); - // 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; + // 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); - let room_list_service = sync_service.room_list_service(); + // 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; - 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 room_list_service = sync_service.room_list_service(); - 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)); + 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."); + } - // 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!"); + 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 + // matrix/background tasks for the currently-authenticated session. + #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. + let reauth_message = loop { + tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + break message; + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; } } - 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:?}"); + } + result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); + 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:?}"); + } + } + return; + } + result = &mut room_list_service_task => { + session_change_handler_task.abort(); + 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!("Rooms list update error: {e}"), + format!("Room list service error: {e}"), PopupKind::Error, None, ); + }, + Err(e) => { + error!("BUG: failed to join room list service loop task: {e:?}"); } - }, - 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:?}"); } + return; } - 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:?}"); + result = &mut space_service_task => { + session_change_handler_task.abort(); + 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:?}"); + } } + return; } - break; } - } + }; + + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + + reset_runtime_state_for_relogin().await; + Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: reauth_message, + }); + initial_client_opt = None; } } @@ -3306,7 +3610,10 @@ fn is_invalid_token_error(e: &sync_service::Error) -> bool { /// 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) { +fn handle_session_changes( + client: Client, + session_reset_sender: UnboundedSender, +) -> JoinHandle<()> { let mut receiver = client.subscribe_to_session_changes(); Handle::current().spawn(async move { loop { @@ -3318,7 +3625,11 @@ fn handle_session_changes(client: Client) { "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())); + clear_persisted_session(client.user_id()).await; + let _ = session_reset_sender.send(SessionResetAction::Reauthenticate { + message: msg.to_string(), + }); + break; } Ok(SessionChange::TokensRefreshed) => {} Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3329,7 +3640,7 @@ fn handle_session_changes(client: Client) { } } } - }); + }) } fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { From bc9b49f6c0d2d23cf3b58c0e178990f00ccbdb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 10:16:14 +0800 Subject: [PATCH 032/283] fix: persist botfather bindings and recover room state --- src/app.rs | 81 +++++++++++++++++++++++++++++------ src/home/room_context_menu.rs | 5 ++- src/home/room_screen.rs | 33 ++++++++++++-- src/settings/bot_settings.rs | 12 ++++++ src/sliding_sync.rs | 42 +++++++++++++++++- 5 files changed, 154 insertions(+), 19 deletions(-) diff --git a/src/app.rs b/src/app.rs index d20772c5d..bf7ea02ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -419,7 +419,16 @@ impl MatchEvent for App { bot_user_id, warning, }) => { - self.app_state.bot_settings.set_room_bound(room_id.clone(), *bound); + self.app_state.bot_settings.set_room_bound( + room_id.clone(), + bot_user_id.clone(), + *bound, + ); + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(self.app_state.clone(), user_id) { + error!("Failed to persist app state after updating BotFather room binding. Error: {e}"); + } + } let kind = if warning.is_some() { PopupKind::Warning } else { @@ -993,7 +1002,6 @@ pub struct AppState { /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, /// Local configuration and UI state for bot-assisted room binding. - #[serde(skip)] pub bot_settings: BotSettingsState, } @@ -1005,8 +1013,16 @@ pub struct BotSettingsState { pub enabled: bool, /// The configured botfather user, either as a full MXID or localpart. pub botfather_user_id: String, - /// Rooms that Robrix currently considers bound to BotFather. - pub bound_rooms: Vec, + /// Rooms that Robrix currently considers bound to BotFather, + /// paired with the exact BotFather MXID used for that room. + pub room_bindings: Vec, +} + +/// A persisted room-level BotFather binding. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct RoomBotBindingState { + pub room_id: OwnedRoomId, + pub bot_user_id: OwnedUserId, } impl Default for BotSettingsState { @@ -1014,7 +1030,7 @@ impl Default for BotSettingsState { Self { enabled: false, botfather_user_id: Self::DEFAULT_BOTFATHER_LOCALPART.to_string(), - bound_rooms: Vec::new(), + room_bindings: Vec::new(), } } } @@ -1024,21 +1040,44 @@ impl BotSettingsState { /// Returns `true` if the given room is currently marked as bound locally. pub fn is_room_bound(&self, room_id: &RoomId) -> bool { - self.bound_rooms + self.room_bindings + .iter() + .any(|binding| binding.room_id == room_id) + } + + /// Returns the persisted BotFather MXID for the given room, if any. + pub fn bound_bot_user_id(&self, room_id: &RoomId) -> Option<&UserId> { + self.room_bindings .iter() - .any(|bound_room_id| bound_room_id == room_id) + .find(|binding| binding.room_id == room_id) + .map(|binding| binding.bot_user_id.as_ref()) } /// Updates the local bound/unbound state for the given room. - pub fn set_room_bound(&mut self, room_id: OwnedRoomId, bound: bool) { + pub fn set_room_bound( + &mut self, + room_id: OwnedRoomId, + bot_user_id: Option, + bound: bool, + ) { if bound { - if !self.is_room_bound(&room_id) { - self.bound_rooms.push(room_id); - self.bound_rooms.sort_by(|lhs, rhs| lhs.as_str().cmp(rhs.as_str())); + let Some(bot_user_id) = bot_user_id else { return }; + if let Some(existing_binding) = self + .room_bindings + .iter_mut() + .find(|binding| binding.room_id == room_id) + { + existing_binding.bot_user_id = bot_user_id; + } else { + self.room_bindings.push(RoomBotBindingState { + room_id, + bot_user_id, + }); + self.room_bindings.sort_by(|lhs, rhs| lhs.room_id.as_str().cmp(rhs.room_id.as_str())); } } else { - self.bound_rooms - .retain(|existing_room_id| existing_room_id != &room_id); + self.room_bindings + .retain(|existing_binding| existing_binding.room_id != room_id); } } @@ -1073,6 +1112,22 @@ impl BotSettingsState { .map(|user_id| user_id.to_owned()) .map_err(|_| format!("Invalid bot user ID: {full_user_id}")) } + + /// Returns the BotFather MXID that should be used for a room action. + /// + /// If the room already has a persisted binding, that exact MXID wins. + /// Otherwise, the current global configuration is resolved. + pub fn resolved_bot_user_id_for_room( + &self, + room_id: &RoomId, + current_user_id: Option<&UserId>, + ) -> Result { + if let Some(bot_user_id) = self.bound_bot_user_id(room_id) { + return Ok(bot_user_id.to_owned()); + } + + self.resolved_bot_user_id(current_user_id) + } } /// A snapshot of the main dock: all state needed to restore the dock tabs/layout. diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 9a73e08b8..796a43a86 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -244,7 +244,10 @@ impl WidgetMatchEvent for RoomContextMenu { else if self.button(cx, ids!(bot_binding_button)).clicked(actions) { if let Some(app_state) = scope.data.get::() { let room_id = details.room_name_id.room_id().clone(); - match app_state.bot_settings.resolved_bot_user_id(current_user_id().as_deref()) { + match app_state.bot_settings.resolved_bot_user_id_for_room( + &room_id, + current_user_id().as_deref(), + ) { Ok(bot_user_id) => { if details.is_bot_bound { submit_async_request(MatrixRequest::SetRoomBotBinding { diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index e3e9d7ab0..4df209a19 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1177,7 +1177,7 @@ impl Widget for RoomScreen { .map(|app_state| { ( app_state.bot_settings.enabled, - app_state.bot_settings.is_room_bound(&room_id), + self.is_app_service_room_bound(app_state, &room_id), ) }) .unwrap_or((false, false)); @@ -1346,7 +1346,10 @@ impl Widget for RoomScreen { } else { match app_state .bot_settings - .resolved_bot_user_id(current_user_id().as_deref()) + .resolved_bot_user_id_for_room( + room_props.room_name_id.room_id(), + current_user_id().as_deref(), + ) { Ok(bot_user_id) => { submit_async_request(MatrixRequest::SetRoomBotBinding { @@ -1776,6 +1779,28 @@ impl RoomScreen { self.close_delete_bot_modal(cx); } + fn is_app_service_room_bound(&self, app_state: &AppState, room_id: &OwnedRoomId) -> bool { + if app_state.bot_settings.is_room_bound(room_id) { + return true; + } + + let Ok(bot_user_id) = app_state + .bot_settings + .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) + else { + return false; + }; + + self.tl_state + .as_ref() + .and_then(|tl| tl.room_members.as_ref()) + .is_some_and(|room_members| { + room_members + .iter() + .any(|room_member| room_member.user_id() == bot_user_id) + }) + } + fn send_botfather_command( &mut self, cx: &mut Cx, @@ -1806,7 +1831,7 @@ impl RoomScreen { ); return; } - if !app_state.bot_settings.is_room_bound(&room_id) { + if !self.is_app_service_room_bound(app_state, &room_id) { enqueue_popup_notification( "Bind BotFather to this room before using BotFather commands.", PopupKind::Warning, @@ -1858,7 +1883,7 @@ impl RoomScreen { ); return; } - if !app_state.bot_settings.is_room_bound(&room_id) { + if !self.is_app_service_room_bound(app_state, &room_id) { enqueue_popup_notification( "Bind BotFather to this room before creating a bot.", PopupKind::Warning, diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index c1fc6a837..bc23b9c14 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -2,7 +2,9 @@ use makepad_widgets::*; use crate::{ app::{AppState, BotSettingsState}, + persistence, shared::popup_list::{PopupKind, enqueue_popup_notification}, + sliding_sync::current_user_id, }; script_mod! { @@ -129,6 +131,7 @@ impl WidgetMatchEvent for BotSettings { if toggle_button.clicked(actions) { let enabled = !app_state.bot_settings.enabled; app_state.bot_settings.enabled = enabled; + persist_bot_settings(app_state); self.sync_ui(cx, &app_state.bot_settings); bot_details.set_visible(cx, enabled); self.view.redraw(cx); @@ -136,6 +139,7 @@ impl WidgetMatchEvent for BotSettings { if save_button.clicked(actions) || bot_user_id_input.returned(actions).is_some() { app_state.bot_settings.botfather_user_id = bot_user_id_input.text().trim().to_string(); + persist_bot_settings(app_state); enqueue_popup_notification( "Saved Matrix app service settings.", PopupKind::Success, @@ -185,3 +189,11 @@ impl BotSettingsRef { inner.populate(cx, bot_settings); } } + +fn persist_bot_settings(app_state: &AppState) { + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(app_state.clone(), user_id) { + error!("Failed to persist bot settings. Error: {e}"); + } + } +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 255332260..131e6610f 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -282,6 +282,12 @@ fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { ) } +fn is_invalid_batch_token_timeline_error(error: &matrix_sdk_ui::timeline::Error) -> bool { + let error_text = error.to_string().to_ascii_lowercase(); + error_text.contains("invalid batch token") + || error_text.contains("must start with 's' or 't'") +} + /// Build a new client. async fn build_client( @@ -994,6 +1000,7 @@ async fn matrix_worker_task( log!("Skipping pagination request for unknown {timeline_kind}"); continue; }; + let client = get_client(); // Spawn a new async task that will make the actual pagination request. let _paginate_task = Handle::current().spawn(async move { @@ -1001,12 +1008,45 @@ async fn matrix_worker_task( sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap(); SignalToUI::set_ui_signal(); - let res = if direction == PaginationDirection::Forwards { + let mut res = if direction == PaginationDirection::Forwards { timeline.paginate_forwards(num_events).await } else { timeline.paginate_backwards(num_events).await }; + if direction == PaginationDirection::Backwards + && res + .as_ref() + .err() + .is_some_and(is_invalid_batch_token_timeline_error) + { + warning!( + "Detected an invalid cached batch token for {timeline_kind}; clearing the room event cache and retrying once." + ); + let room_id = timeline_kind.room_id().clone(); + if let Some(room) = client.and_then(|client| client.get_room(&room_id)) { + match room.event_cache().await { + Ok((room_event_cache, _drop_handles)) => { + match room_event_cache.clear().await { + Ok(()) => { + res = timeline.paginate_backwards(num_events).await; + } + Err(clear_error) => { + warning!( + "Failed to clear event cache for room {room_id} after invalid batch token: {clear_error}" + ); + } + } + } + Err(event_cache_error) => { + warning!( + "Failed to access room event cache for room {room_id} after invalid batch token: {event_cache_error}" + ); + } + } + } + } + match res { Ok(fully_paginated) => { log!("Completed {direction} pagination request for {timeline_kind}, hit {} of timeline? {}", From 6dbe704bd57ebc2c33d135cad77f91d3ed8ae44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 10:29:52 +0800 Subject: [PATCH 033/283] feat: add thread entry points to the message context menu --- src/home/new_message_context_menu.rs | 28 +++++++++++++++++++++++++++- src/home/room_screen.rs | 1 + 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/home/new_message_context_menu.rs b/src/home/new_message_context_menu.rs index 06c963fb3..b7552b733 100644 --- a/src/home/new_message_context_menu.rs +++ b/src/home/new_message_context_menu.rs @@ -116,6 +116,11 @@ script_mod! { text: "Reply" } + thread_button := mod.widgets.NewMessageContextMenuButton { + draw_icon +: { svg: crate_resource("self://resources/icons/double_chat.svg") } + text: "" + } + divider_after_react_reply := LineH { margin: Inset{top: 3, bottom: 3} width: Fill, @@ -272,6 +277,8 @@ pub struct MessageDetails { pub thread_root_event_id: Option, /// The widget ID of the RoomScreen that contains this message. pub room_screen_widget_uid: WidgetUid, + /// Whether this message is currently being shown in a thread-focused timeline. + pub is_thread_timeline: bool, /// Whether this message should be highlighted, i.e., /// if it mentions the room/current user or is a reply to the current user. pub should_be_highlighted: bool, @@ -382,6 +389,15 @@ impl WidgetMatchEvent for NewMessageContextMenu { ); close_menu = true; } + else if self.button(cx, ids!(thread_button)).clicked(actions) { + if let Some(thread_root_event_id) = details.thread_root_event_id.as_ref().or_else(|| details.event_id()) { + cx.widget_action( + details.room_screen_widget_uid, + MessageAction::OpenThread(thread_root_event_id.clone()), + ); + } + close_menu = true; + } else if self.button(cx, ids!(edit_message_button)).clicked(actions) { cx.widget_action( details.room_screen_widget_uid, @@ -497,6 +513,7 @@ impl NewMessageContextMenu { let react_button = self.view.button(cx, ids!(react_button)); let reply_button = self.view.button(cx, ids!(reply_button)); + let thread_button = self.view.button(cx, ids!(thread_button)); let edit_button = self.view.button(cx, ids!(edit_message_button)); let pin_button = self.view.button(cx, ids!(pin_button)); let copy_text_button = self.view.button(cx, ids!(copy_text_button)); @@ -512,7 +529,8 @@ impl NewMessageContextMenu { // `copy_text_button`, `copy_link_to_message_button`, and `view_source_button` let show_react = details.abilities.contains(MessageAbilities::CanReact); let show_reply_to = details.abilities.contains(MessageAbilities::CanReplyTo); - let show_divider_after_react_reply = show_react || show_reply_to; + let show_thread = !details.is_thread_timeline && details.event_id().is_some(); + let show_divider_after_react_reply = show_react || show_reply_to || show_thread; let show_edit = details.abilities.contains(MessageAbilities::CanEdit); let show_pin: bool; let show_copy_text = true; @@ -528,8 +546,14 @@ impl NewMessageContextMenu { self.view.view(cx, ids!(react_view)).set_visible(cx, show_react); react_button.set_visible(cx, show_react); reply_button.set_visible(cx, show_reply_to); + thread_button.set_visible(cx, show_thread); self.view.view(cx, ids!(divider_after_react_reply)).set_visible(cx, show_divider_after_react_reply); edit_button.set_visible(cx, show_edit); + if details.thread_root_event_id.is_some() { + thread_button.set_text(cx, "Open Thread"); + } else { + thread_button.set_text(cx, "Reply in Thread"); + } if details.abilities.contains(MessageAbilities::CanPin) { pin_button.set_text(cx, "Pin Message"); show_pin = true; @@ -549,6 +573,7 @@ impl NewMessageContextMenu { // Reset the hover state of each button. react_button.reset_hover(cx); reply_button.reset_hover(cx); + thread_button.reset_hover(cx); edit_button.reset_hover(cx); pin_button.reset_hover(cx); copy_text_button.reset_hover(cx); @@ -568,6 +593,7 @@ impl NewMessageContextMenu { let num_visible_buttons = show_react as u8 + show_reply_to as u8 + + show_thread as u8 + show_edit as u8 + show_pin as u8 + show_copy_text as u8 diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 61f20ced9..65ed41cbd 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -3477,6 +3477,7 @@ fn populate_message_view( item_id, related_event_id: msg_like_content.in_reply_to.as_ref().map(|r| r.event_id.clone()), room_screen_widget_uid, + is_thread_timeline: timeline_kind.thread_root_event_id().is_some(), abilities: MessageAbilities::from_user_power_and_event( user_power_levels, event_tl_item, From fd2aaca16757779454745523c8626f098ffecb2c Mon Sep 17 00:00:00 2001 From: Alvin Date: Thu, 26 Mar 2026 11:48:52 +0800 Subject: [PATCH 034/283] fix: recalculate byte offset on every target update to prevent UTF-8 panic update_target() previously only recalculated displayed_byte_offset when the new text was shorter. If the streaming backend reformatted the message body (e.g. adding markdown backticks), the stale byte offset could land inside a multi-byte character, panicking on string slice. --- src/home/streaming_animation.rs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 886f0e1ed..8456cec88 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -63,15 +63,19 @@ impl StreamingAnimState { self.target_char_count = new_text.chars().count(); self.is_live = is_live; - // Clamp display pointers if the new text is shorter than what was already displayed. + // Clamp char count if the new text is shorter than what was already displayed. if self.displayed_char_count > self.target_char_count { self.displayed_char_count = self.target_char_count; - self.displayed_byte_offset = self.target_text - .char_indices() - .nth(self.target_char_count) - .map_or(self.target_text.len(), |(i, _)| i); } + // Always recalculate byte offset: the new text may have different + // byte widths at already-displayed positions (e.g. markdown formatting + // changes between streaming updates). + self.displayed_byte_offset = self.target_text + .char_indices() + .nth(self.displayed_char_count) + .map_or(self.target_text.len(), |(i, _)| i); + let now = Instant::now(); self.chars_at_last_update = self.displayed_char_count; self.last_update_time = now; @@ -243,6 +247,25 @@ mod tests { assert!(s.display_buffer.starts_with("Hi")); } + #[test] + fn test_update_target_recalculates_byte_offset_for_different_prefix() { + // Simulate: displayed 5 ASCII chars, then text replaced with CJK characters. + // Old byte offset (5) would be inside a multi-byte char in the new text. + let mut s = make_state("hello world"); + s.advance_displayed(5); + assert_eq!(s.displayed_byte_offset, 5); + + // New text has 5+ chars but first 5 chars are 3-byte CJK. + // Without the fix, displayed_byte_offset stays 5, crashing on slice. + s.update_target("你好世界测试数据", true); + assert_eq!(s.displayed_char_count, 5); + // 5 CJK chars × 3 bytes = 15 + assert_eq!(s.displayed_byte_offset, 15); + // Must not panic: + s.fill_display_buffer(); + assert!(s.display_buffer.starts_with("你好世界测")); + } + #[test] fn test_tick_advances() { let mut s = make_state("Hello, world!"); From df0e7741140d05e249df08270dc69208a5c8af5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 12:33:25 +0800 Subject: [PATCH 035/283] perf: reduce bot binding scans in app state --- src/app.rs | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index bf7ea02ff..01083e199 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1038,19 +1038,21 @@ impl Default for BotSettingsState { impl BotSettingsState { pub const DEFAULT_BOTFATHER_LOCALPART: &'static str = "bot"; + fn room_binding_index(&self, room_id: &RoomId) -> Result { + self.room_bindings + .binary_search_by(|binding| binding.room_id.as_str().cmp(room_id.as_str())) + } + /// Returns `true` if the given room is currently marked as bound locally. pub fn is_room_bound(&self, room_id: &RoomId) -> bool { - self.room_bindings - .iter() - .any(|binding| binding.room_id == room_id) + self.room_binding_index(room_id).is_ok() } /// Returns the persisted BotFather MXID for the given room, if any. pub fn bound_bot_user_id(&self, room_id: &RoomId) -> Option<&UserId> { - self.room_bindings - .iter() - .find(|binding| binding.room_id == room_id) - .map(|binding| binding.bot_user_id.as_ref()) + self.room_binding_index(room_id) + .ok() + .map(|index| self.room_bindings[index].bot_user_id.as_ref()) } /// Updates the local bound/unbound state for the given room. @@ -1062,22 +1064,21 @@ impl BotSettingsState { ) { if bound { let Some(bot_user_id) = bot_user_id else { return }; - if let Some(existing_binding) = self - .room_bindings - .iter_mut() - .find(|binding| binding.room_id == room_id) - { - existing_binding.bot_user_id = bot_user_id; - } else { - self.room_bindings.push(RoomBotBindingState { - room_id, - bot_user_id, - }); - self.room_bindings.sort_by(|lhs, rhs| lhs.room_id.as_str().cmp(rhs.room_id.as_str())); + match self.room_binding_index(room_id.as_ref()) { + Ok(existing_index) => { + self.room_bindings[existing_index].bot_user_id = bot_user_id; + } + Err(insert_index) => { + self.room_bindings.insert(insert_index, RoomBotBindingState { + room_id, + bot_user_id, + }); + } } } else { - self.room_bindings - .retain(|existing_binding| existing_binding.room_id != room_id); + if let Ok(existing_index) = self.room_binding_index(room_id.as_ref()) { + self.room_bindings.remove(existing_index); + } } } From 8ebcf9b358c275158631b8cbddbac145dc6a8ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 26 Mar 2026 12:53:40 +0800 Subject: [PATCH 036/283] refactor: cache bot binding detection in room ui --- src/app.rs | 30 ++++++ src/home/create_bot_modal.rs | 3 - src/home/delete_bot_modal.rs | 3 - src/home/room_screen.rs | 183 +++++++++++++++++++---------------- 4 files changed, 129 insertions(+), 90 deletions(-) diff --git a/src/app.rs b/src/app.rs index 01083e199..b9a75f6ee 100644 --- a/src/app.rs +++ b/src/app.rs @@ -464,6 +464,31 @@ impl MatchEvent for App { self.ui.redraw(cx); continue; } + Some(AppStateAction::BotRoomBindingDetected { + room_id, + bot_user_id, + }) => { + if self + .app_state + .bot_settings + .bound_bot_user_id(room_id.as_ref()) + .is_some_and(|existing_bot_user_id| existing_bot_user_id.as_str() == bot_user_id.as_str()) + { + continue; + } + self.app_state.bot_settings.set_room_bound( + room_id.clone(), + Some(bot_user_id.clone()), + true, + ); + if let Some(user_id) = current_user_id() { + if let Err(e) = persistence::save_app_state(self.app_state.clone(), user_id) { + error!("Failed to persist detected BotFather room binding. Error: {e}"); + } + } + self.ui.redraw(cx); + continue; + } Some(AppStateAction::NavigateToRoom { room_to_close, destination_room }) => { self.navigate_to_room(cx, room_to_close.as_ref(), destination_room); continue; @@ -1279,6 +1304,11 @@ pub enum AppStateAction { bot_user_id: Option, warning: Option, }, + /// A room's member list indicates that the configured BotFather is already present. + BotRoomBindingDetected { + room_id: OwnedRoomId, + bot_user_id: OwnedUserId, + }, /// The given room was successfully loaded from the homeserver /// and is now known to our client. /// diff --git a/src/home/create_bot_modal.rs b/src/home/create_bot_modal.rs index bafb822e6..384510f48 100644 --- a/src/home/create_bot_modal.rs +++ b/src/home/create_bot_modal.rs @@ -184,8 +184,6 @@ pub struct CreateBotModal { #[deref] view: View, #[rust] - room_name_id: Option, - #[rust] is_showing_error: bool, } @@ -260,7 +258,6 @@ impl WidgetMatchEvent for CreateBotModal { impl CreateBotModal { pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { - self.room_name_id = Some(room_name_id.clone()); self.is_showing_error = false; self.view diff --git a/src/home/delete_bot_modal.rs b/src/home/delete_bot_modal.rs index caab2bd49..634171936 100644 --- a/src/home/delete_bot_modal.rs +++ b/src/home/delete_bot_modal.rs @@ -145,8 +145,6 @@ pub struct DeleteBotModal { #[deref] view: View, #[rust] - room_name_id: Option, - #[rust] is_showing_error: bool, } @@ -206,7 +204,6 @@ impl WidgetMatchEvent for DeleteBotModal { impl DeleteBotModal { pub fn show(&mut self, cx: &mut Cx, room_name_id: RoomNameId) { - self.room_name_id = Some(room_name_id.clone()); self.is_showing_error = false; self.view diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4df209a19..ce2dfd115 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -8,7 +8,7 @@ use hashbrown::{HashMap, HashSet}; use imbl::Vector; use makepad_widgets::{image_cache::ImageBuffer, *}; use matrix_sdk::{ - OwnedServerName, RoomDisplayName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ + OwnedServerName, media::{MediaFormat, MediaRequestParameters}, room::RoomMember, ruma::{ EventId, MatrixToUri, MatrixUri, OwnedEventId, OwnedMxcUri, OwnedRoomId, UserId, events::{ receipt::Receipt, room::{ @@ -119,6 +119,28 @@ fn resolve_delete_bot_user_id( .map_err(|_| format!("Invalid Matrix user ID: {full_user_id}")) } +fn detected_bot_binding_for_members( + app_state: &AppState, + room_id: &OwnedRoomId, + members: &[RoomMember], +) -> Option { + if app_state.bot_settings.is_room_bound(room_id) { + return None; + } + + let Ok(bot_user_id) = app_state + .bot_settings + .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) + else { + return None; + }; + + members + .iter() + .any(|room_member| room_member.user_id() == bot_user_id) + .then_some(bot_user_id) +} + script_mod! { use mod.prelude.widgets.* @@ -868,6 +890,8 @@ pub struct RoomScreen { /// The name and ID of the currently-shown room, if any. #[rust] room_name_id: Option, + /// The avatar URL of the currently-shown room, if any. + #[rust] room_avatar_url: Option, /// The timeline currently displayed by this RoomScreen, if any. #[rust] timeline_kind: Option, /// The persistent UI-relevant states for the room that this widget is currently displaying. @@ -1126,7 +1150,7 @@ impl Widget for RoomScreen { self.show_timeline(cx); } - self.process_timeline_updates(cx, &portal_list); + self.process_timeline_updates(cx, &portal_list, scope.data.get::()); // Ideally we would do this elsewhere on the main thread, because it's not room-specific, // but it doesn't hurt to do it here. @@ -1182,21 +1206,12 @@ impl Widget for RoomScreen { }) .unwrap_or((false, false)); - // Fetch room data once to avoid duplicate expensive lookups - let (room_display_name, room_avatar_url) = get_client() - .and_then(|client| client.get_room(&room_id)) - .map(|room| ( - room.cached_display_name().unwrap_or(RoomDisplayName::Empty), - room.avatar_url() - )) - .unwrap_or((RoomDisplayName::Empty, None)); - RoomScreenProps { room_screen_widget_uid, - room_name_id: RoomNameId::new(room_display_name, room_id), + room_name_id: self.room_name_id.clone().unwrap_or_else(|| RoomNameId::empty(room_id)), timeline_kind: tl.kind.clone(), room_members, - room_avatar_url, + room_avatar_url: self.room_avatar_url.clone(), app_service_enabled, app_service_room_bound, } @@ -1435,36 +1450,33 @@ impl Widget for RoomScreen { None => {} } - match action + if let MessageAction::ToggleAppServiceActions = action .as_widget_action() .widget_uid_eq(room_screen_widget_uid) .cast() { - MessageAction::ToggleAppServiceActions => { - if room_props.timeline_kind.thread_root_event_id().is_some() { - enqueue_popup_notification( - "Bot commands are only supported in the main room timeline.", - PopupKind::Warning, - Some(4.0), - ); - } else if !room_props.app_service_enabled { - enqueue_popup_notification( - "Enable App Service in Settings before using /bot.", - PopupKind::Warning, - Some(4.0), - ); - } else if !room_props.app_service_room_bound { - enqueue_popup_notification( - "Bind BotFather to this room before using /bot.", - PopupKind::Warning, - Some(4.0), - ); - } else { - self.toggle_app_service_actions(cx); - } - return false; + if room_props.timeline_kind.thread_root_event_id().is_some() { + enqueue_popup_notification( + "Bot commands are only supported in the main room timeline.", + PopupKind::Warning, + Some(4.0), + ); + } else if !room_props.app_service_enabled { + enqueue_popup_notification( + "Enable App Service in Settings before using /bot.", + PopupKind::Warning, + Some(4.0), + ); + } else if !room_props.app_service_room_bound { + enqueue_popup_notification( + "Bind BotFather to this room before using /bot.", + PopupKind::Warning, + Some(4.0), + ); + } else { + self.toggle_app_service_actions(cx); } - _ => {} + return false; } // Handle the action that requests to show the user profile sliding pane. @@ -1780,25 +1792,7 @@ impl RoomScreen { } fn is_app_service_room_bound(&self, app_state: &AppState, room_id: &OwnedRoomId) -> bool { - if app_state.bot_settings.is_room_bound(room_id) { - return true; - } - - let Ok(bot_user_id) = app_state - .bot_settings - .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) - else { - return false; - }; - - self.tl_state - .as_ref() - .and_then(|tl| tl.room_members.as_ref()) - .is_some_and(|room_members| { - room_members - .iter() - .any(|room_member| room_member.user_id() == bot_user_id) - }) + app_state.bot_settings.is_room_bound(room_id) } fn send_botfather_command( @@ -1807,9 +1801,9 @@ impl RoomScreen { app_state: &AppState, command: &str, success_message: &str, - ) { + ) -> bool { let Some(timeline_kind) = self.timeline_kind.clone() else { - return; + return false; }; if timeline_kind.thread_root_event_id().is_some() { enqueue_popup_notification( @@ -1817,11 +1811,11 @@ impl RoomScreen { PopupKind::Warning, Some(4.0), ); - return; + return false; } let Some(room_id) = self.room_id().cloned() else { - return; + return false; }; if !app_state.bot_settings.enabled { enqueue_popup_notification( @@ -1829,7 +1823,7 @@ impl RoomScreen { PopupKind::Warning, Some(4.0), ); - return; + return false; } if !self.is_app_service_room_bound(app_state, &room_id) { enqueue_popup_notification( @@ -1837,7 +1831,7 @@ impl RoomScreen { PopupKind::Warning, Some(4.0), ); - return; + return false; } submit_async_request(MatrixRequest::SendMessage { @@ -1850,6 +1844,7 @@ impl RoomScreen { enqueue_popup_notification(success_message.to_string(), PopupKind::Info, Some(4.0)); self.set_app_service_actions_visible(cx, false); + true } fn send_create_bot_command( @@ -1893,20 +1888,14 @@ impl RoomScreen { } let command = format_create_bot_command(username, display_name, system_prompt); - submit_async_request(MatrixRequest::SendMessage { - timeline_kind, - message: RoomMessageEventContent::text_plain(command), - replied_to: None, - #[cfg(feature = "tsp")] - sign_with_tsp: false, - }); - - enqueue_popup_notification( - format!("Sent `/createbot` for `{username}` to BotFather."), - PopupKind::Info, - Some(4.0), - ); - self.close_create_bot_modal(cx); + if self.send_botfather_command( + cx, + app_state, + &command, + &format!("Sent `/createbot` for `{username}` to BotFather."), + ) { + self.close_create_bot_modal(cx); + } } fn send_delete_bot_command( @@ -1925,19 +1914,25 @@ impl RoomScreen { }; let command = format_delete_bot_command(matrix_user_id.as_ref()); - self.send_botfather_command( + if self.send_botfather_command( cx, app_state, &command, &format!("Sent `/deletebot` for {matrix_user_id} to BotFather."), - ); - self.close_delete_bot_modal(cx); + ) { + self.close_delete_bot_modal(cx); + } } /// Processes all pending background updates to the currently-shown timeline. /// /// Redraws this RoomScreen view if any updates were applied. - fn process_timeline_updates(&mut self, cx: &mut Cx, portal_list: &PortalListRef) { + fn process_timeline_updates( + &mut self, + cx: &mut Cx, + portal_list: &PortalListRef, + app_state: Option<&AppState>, + ) { let top_space = self.view(cx, ids!(top_space)); let jump_to_bottom_button = self.jump_to_bottom_button(cx, ids!(jump_to_bottom_button)); let curr_first_id = portal_list.first_id(); @@ -2209,8 +2204,21 @@ impl RoomScreen { // but for now we just fall through and let the final `redraw()` call re-draw the whole timeline view. } TimelineUpdate::RoomMembersListFetched { members } => { - // Store room members directly in TimelineUiState - tl.room_members = Some(Arc::new(members)); + let members = Arc::new(members); + if let Some(app_state) = app_state { + let room_id = tl.kind.room_id().clone(); + if let Some(bot_user_id) = detected_bot_binding_for_members( + app_state, + &room_id, + members.as_ref(), + ) { + Cx::post_action(AppStateAction::BotRoomBindingDetected { + room_id, + bot_user_id, + }); + } + } + tl.room_members = Some(members); }, TimelineUpdate::MediaFetched(request) => { log!("process_timeline_updates(): media fetched for room {}", tl.kind.room_id()); @@ -3047,7 +3055,7 @@ impl RoomScreen { // Now that we have restored the TimelineUiState into this RoomScreen widget, // we can proceed to processing pending background updates. - self.process_timeline_updates(cx, &self.portal_list(cx, ids!(list))); + self.process_timeline_updates(cx, &self.portal_list(cx, ids!(list)), None); self.redraw(cx); } @@ -3078,6 +3086,7 @@ impl RoomScreen { timeline_kind, subscribe: false, }); + self.room_avatar_url = None; } /// Removes the current room's visual UI state from this widget @@ -3165,6 +3174,9 @@ impl RoomScreen { // but we do need update the `room_name_id` in case it has changed, or it has been cleared. if self.timeline_kind.as_ref().is_some_and(|kind| kind == &timeline_kind) { self.room_name_id = Some(room_name_id.clone()); + self.room_avatar_url = get_client() + .and_then(|client| client.get_room(room_name_id.room_id())) + .and_then(|room| room.avatar_url()); return; } @@ -3174,6 +3186,9 @@ impl RoomScreen { self.loading_pane(cx, ids!(loading_pane)).take_state(); self.room_name_id = Some(room_name_id.clone()); + self.room_avatar_url = get_client() + .and_then(|client| client.get_room(room_name_id.room_id())) + .and_then(|room| room.avatar_url()); self.timeline_kind = Some(timeline_kind.clone()); // We initially tell every MentionableTextInput widget that the current user From ded8ff24f779103f6f919ed386221b0f0512d160 Mon Sep 17 00:00:00 2001 From: Alvin Date: Thu, 26 Mar 2026 14:21:06 +0800 Subject: [PATCH 037/283] refactor: switch to fixed-cadence Moly-style reveal for smoother streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the dynamic speed strategy (speed = remaining, with hard jumps at gap > 200/500) with a fixed-cadence chunked reveal: 2 chars every 55ms. - Arrival burst only when display had fully caught up (not on every update) - Preserve tick clock when backlog exists to maintain smooth cadence - Finish snap when stream ends with ≤20 chars remaining - Remove chars_per_second/chars_at_last_update/update_speed() complexity --- src/home/streaming_animation.rs | 164 ++++++++++++++++++++++---------- 1 file changed, 114 insertions(+), 50 deletions(-) diff --git a/src/home/streaming_animation.rs b/src/home/streaming_animation.rs index 8456cec88..2846885d6 100644 --- a/src/home/streaming_animation.rs +++ b/src/home/streaming_animation.rs @@ -3,6 +3,15 @@ use std::time::{Duration, Instant}; const FINISHED_STREAM_TIMEOUT: Duration = Duration::from_secs(30); const LIVE_STREAM_STALL_TIMEOUT: Duration = Duration::from_secs(5 * 60); +/// Characters to reveal per amortized chunk, closer to Moly's small-block growth. +const REVEAL_CHUNK_SIZE: usize = 2; +/// Fixed cadence for releasing each chunk. +const REVEAL_INTERVAL: Duration = Duration::from_millis(55); +/// Characters to reveal immediately when new content arrives after the UI had caught up. +const ARRIVAL_BURST: usize = 1; +/// When the stream is finished and this few chars remain, snap to the end. +const FINISH_SNAP_THRESHOLD: usize = 20; + /// Animation state for a single streaming message. /// Tracks an MSC4357 live message and drives character-by-character reveal. pub struct StreamingAnimState { @@ -10,12 +19,10 @@ pub struct StreamingAnimState { pub target_char_count: usize, pub displayed_char_count: usize, pub displayed_byte_offset: usize, - pub chars_per_second: f64, - pub fractional_chars: f64, + pub fractional_chunks: f64, pub last_update_time: Instant, pub last_tick_time: Instant, pub animation_start_time: Instant, - pub chars_at_last_update: usize, pub display_buffer: String, /// Whether the message currently carries the MSC4357 `live` field. pub is_live: bool, @@ -31,12 +38,10 @@ impl StreamingAnimState { target_char_count: char_count, displayed_char_count: 0, displayed_byte_offset: 0, - chars_per_second: 1.0, - fractional_chars: 0.0, + fractional_chunks: 0.0, last_update_time: now, last_tick_time: now, animation_start_time: now, - chars_at_last_update: 0, display_buffer: String::with_capacity(initial_text.len() + 4), is_live, timeline_index: None, @@ -50,14 +55,15 @@ impl StreamingAnimState { restored.displayed_char_count = common_chars; restored.displayed_byte_offset = common_bytes; - restored.chars_at_last_update = common_chars; restored.animation_start_time = previous.animation_start_time; restored.timeline_index = previous.timeline_index; - restored.update_speed(); restored } pub fn update_target(&mut self, new_text: &str, is_live: bool) { + let prev_char_count = self.target_char_count; + let had_backlog = self.displayed_char_count < prev_char_count; + self.target_text.clear(); self.target_text.push_str(new_text); self.target_char_count = new_text.chars().count(); @@ -76,11 +82,21 @@ impl StreamingAnimState { .nth(self.displayed_char_count) .map_or(self.target_text.len(), |(i, _)| i); + // Arrival burst: only when we had fully caught up and were waiting + // for more text. If backlog already exists, stay on the amortized cadence. + let added_chars = self.target_char_count.saturating_sub(prev_char_count); + if added_chars > 0 && !had_backlog { + self.advance_displayed(added_chars.min(ARRIVAL_BURST)); + } + let now = Instant::now(); - self.chars_at_last_update = self.displayed_char_count; self.last_update_time = now; - self.last_tick_time = now; - self.update_speed(); + // If the animation had already caught up and was waiting for more text, + // restart the frame clock so idle time doesn't count as reveal time. + // If backlog already existed, keep the clock to preserve smooth cadence. + if !had_backlog { + self.last_tick_time = now; + } // Reserve only the deficit (reserve(n) guarantees capacity >= len + n). let needed = new_text.len() + 4; if self.display_buffer.capacity() < needed { @@ -88,16 +104,6 @@ impl StreamingAnimState { } } - fn update_speed(&mut self) { - let remaining = self.target_char_count.saturating_sub(self.displayed_char_count); - if remaining > 0 { - self.chars_per_second = remaining as f64; - if self.chars_per_second < 30.0 { - self.chars_per_second = 30.0; - } - } - } - pub fn advance_displayed(&mut self, chars_to_add: usize) { if chars_to_add == 0 || self.displayed_char_count >= self.target_char_count { return; } let remaining = &self.target_text[self.displayed_byte_offset..]; @@ -123,28 +129,24 @@ impl StreamingAnimState { pub fn tick_with_elapsed(&mut self, elapsed: Duration) -> bool { if self.displayed_char_count >= self.target_char_count { return false; } - let gap = self.target_char_count - self.displayed_char_count; - let mut changed = false; - - let speed = if gap > 500 { - let jump = gap - 50; - self.advance_displayed(jump); - changed = true; - self.chars_per_second - } else if gap > 200 { - self.chars_per_second * 3.0 - } else { - self.chars_per_second - }; + let remaining = self.target_char_count - self.displayed_char_count; + + // Finish snap: when the stream is done and only a few chars remain, show them all. + if !self.is_live && remaining <= FINISH_SNAP_THRESHOLD { + self.advance_displayed(remaining); + return true; + } - self.fractional_chars += speed * elapsed.as_secs_f64(); - let advance = self.fractional_chars.floor() as usize; - self.fractional_chars -= advance as f64; - if advance > 0 { - self.advance_displayed(advance); - changed = true; + // Moly-style amortization: reveal fixed-size chunks at a fixed cadence + // instead of accelerating as backlog grows. + self.fractional_chunks += elapsed.as_secs_f64() / REVEAL_INTERVAL.as_secs_f64(); + let advance_chunks = self.fractional_chunks.floor() as usize; + self.fractional_chunks -= advance_chunks as f64; + if advance_chunks > 0 { + self.advance_displayed(advance_chunks * REVEAL_CHUNK_SIZE); + return true; } - changed + false } pub fn fill_display_buffer(&mut self) { @@ -232,8 +234,26 @@ mod tests { s.advance_displayed(5); s.update_target("Hello, world!", true); assert_eq!(s.target_char_count, 13); - assert_eq!(s.displayed_char_count, 5); - assert!(s.chars_per_second > 0.0); + // Arrival burst reveals only the newly added chars, capped by ARRIVAL_BURST. + assert_eq!(s.displayed_char_count, 5 + ARRIVAL_BURST.min(8)); + } + + #[test] + fn test_update_target_uses_single_char_burst_when_waiting_for_new_text() { + let mut s = make_state("Hello"); + s.advance_displayed(5); + s.update_target("Hello, world!", true); + assert_eq!(s.displayed_char_count, 6); + } + + #[test] + fn test_update_target_does_not_burst_while_backlog_exists() { + let mut s = make_state("Hello"); + s.advance_displayed(2); + s.update_target("Hello!", true); + // When backlog already exists, keep the amortized cadence instead of + // applying a fresh burst on every incoming update. + assert_eq!(s.displayed_char_count, 2); } #[test] @@ -269,18 +289,25 @@ mod tests { #[test] fn test_tick_advances() { let mut s = make_state("Hello, world!"); - s.chars_per_second = 4.0; - let changed = s.tick_with_elapsed(Duration::from_millis(500)); + let changed = s.tick_with_elapsed(REVEAL_INTERVAL); assert!(changed); - assert_eq!(s.displayed_char_count, 2); + assert_eq!(s.displayed_char_count, REVEAL_CHUNK_SIZE); + } + + #[test] + fn test_tick_waits_for_full_chunk_interval() { + let mut s = make_state("Hello, world!"); + assert!(!s.tick_with_elapsed(REVEAL_INTERVAL / 2)); + assert_eq!(s.displayed_char_count, 0); } #[test] - fn test_tick_large_gap() { + fn test_tick_large_gap_smooth() { let mut s = make_state(&"a".repeat(1000)); - s.chars_per_second = 0.1; + // Even after a large elapsed gap, keep a steady amortized pace. assert!(s.tick_with_elapsed(Duration::from_secs(1))); - assert!(s.displayed_char_count > 900); + assert!(s.displayed_char_count >= 30); + assert!(s.displayed_char_count <= 40); } #[test] @@ -344,9 +371,46 @@ mod tests { #[test] fn test_tick_zero_elapsed() { let mut s = make_state("Hello"); - s.chars_per_second = 20.0; assert!(!s.tick_with_elapsed(Duration::ZERO)); assert_eq!(s.displayed_char_count, 0); } + #[test] + fn test_update_target_preserves_tick_clock_when_backlog_already_exists() { + let mut s = make_state("Hello, world!"); + s.advance_displayed(3); + let before = Instant::now() - Duration::from_millis(120); + s.last_tick_time = before; + + s.update_target("Hello, world!!!", true); + + assert_eq!(s.last_tick_time, before); + } + + #[test] + fn test_update_target_resets_tick_clock_when_waiting_for_new_text() { + let mut s = make_state("Hello"); + s.advance_displayed(5); + let before = Instant::now() - Duration::from_secs(5); + s.last_tick_time = before; + + s.update_target("Hello!", true); + + assert!(s.last_tick_time > before); + } + + #[test] + fn test_finish_snap() { + let mut s = make_state(&"a".repeat(30)); + s.advance_displayed(20); + // 10 remaining but is_live=true → normal tick, no snap. + s.tick_with_elapsed(Duration::from_millis(16)); + assert!(s.displayed_char_count < 30); + + // Mark as finished → remaining <= FINISH_SNAP_THRESHOLD → snaps to end. + s.is_live = false; + assert!(s.tick_with_elapsed(Duration::from_millis(1))); + assert_eq!(s.displayed_char_count, 30); + } + } From f0efba2e87f00536c6f9fbc02ae0aac6760752e9 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 27 Mar 2026 10:02:12 +0800 Subject: [PATCH 038/283] logging in release mode --- src/app.rs | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/src/app.rs b/src/app.rs index b9a75f6ee..011d9ce7a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,8 @@ //! //! See `handle_startup()` for the first code that runs on app startup. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::{fs::{File, OpenOptions}, io::Write, sync::Mutex}; use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}}; @@ -192,11 +194,221 @@ impl ScriptHook for App { } } +// ============================================================================= +// File Logging for Packaged Builds (non-mobile platforms) +// ============================================================================= + +/// Global log file handle for packaged builds. +/// Only used on desktop platforms when running as a packaged application. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +static LOG_FILE: std::sync::OnceLock>> = std::sync::OnceLock::new(); + +/// Detects if the application is running as a packaged build (not via `cargo run`). +/// +/// Detection methods per platform: +/// - macOS: Check if executable is inside a `.app/Contents/MacOS/` bundle +/// - Windows: Check if executable is in `Program Files` or similar installation directory +/// - Linux: Check if executable is in `/usr`, `/opt`, or is an AppImage +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn is_packaged_build() -> bool { + let Ok(exe_path) = std::env::current_exe() else { + return false; + }; + let exe_path_str = exe_path.to_string_lossy(); + + #[cfg(target_os = "macos")] + { + // Check if running from a .app bundle + exe_path_str.contains(".app/Contents/MacOS/") + } + + #[cfg(target_os = "windows")] + { + // Check if running from Program Files or a typical installation directory + let exe_lower = exe_path_str.to_lowercase(); + exe_lower.contains("program files") + || exe_lower.contains("programfiles") + || exe_lower.contains("appdata\\local\\programs") + } + + #[cfg(target_os = "linux")] + { + // Check if running from system directories or AppImage + exe_path_str.starts_with("/usr/") + || exe_path_str.starts_with("/opt/") + || exe_path_str.contains(".AppImage") + || std::env::var("APPIMAGE").is_ok() + } + + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + false + } +} + +/// Initializes file logging for packaged builds. +/// Creates a log file in the app data directory with timestamp. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn init_file_logging() -> Option<()> { + if !is_packaged_build() { + LOG_FILE.get_or_init(|| None); + return None; + } + + // Get platform-specific logs directory + let logs_dir = logs_dir(); + std::fs::create_dir_all(&logs_dir).ok()?; + + // Create log file with timestamp + let now = chrono::Local::now(); + let log_filename = format!("robrix_{}.log", now.format("%Y-%m-%d_%H-%M-%S")); + let log_path = logs_dir.join(&log_filename); + + // Also create/update a symlink to the latest log file for convenience + let latest_log_path = logs_dir.join("robrix_latest.log"); + + // Remove old symlink if it exists (ignore errors) + #[cfg(unix)] + { + let _ = std::fs::remove_file(&latest_log_path); + let _ = std::os::unix::fs::symlink(&log_filename, &latest_log_path); + } + + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + .ok()?; + + LOG_FILE.get_or_init(|| Some(Mutex::new(file))); + + // Print to stderr so user knows where logs are going + eprintln!("[Robrix] Logging to file: {}", log_path.display()); + + Some(()) +} + +/// Writes a log message to the log file (if file logging is enabled). +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn write_to_log_file(message: &str) { + if let Some(Some(file_mutex)) = LOG_FILE.get() { + if let Ok(mut file) = file_mutex.lock() { + let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f"); + let _ = writeln!(file, "[{}] {}", timestamp, message); + let _ = file.flush(); + } + } +} + +/// Returns the path to the logs directory using platform-standard locations. +/// +/// Platform-specific paths: +/// - macOS: `~/Library/Logs/Robrix/` +/// - Windows: `%APPDATA%/Robrix/logs/` +/// - Linux: `~/.local/share/robrix/logs/` (or `$XDG_DATA_HOME/robrix/logs/`) +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn logs_dir() -> std::path::PathBuf { + use std::path::PathBuf; + + #[cfg(target_os = "macos")] + { + // macOS standard log location: ~/Library/Logs/Robrix/ + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home) + .join("Library") + .join("Logs") + .join("Robrix"); + } + } + + #[cfg(target_os = "windows")] + { + // Windows: %APPDATA%/Robrix/logs/ + if let Ok(appdata) = std::env::var("APPDATA") { + return PathBuf::from(appdata).join("Robrix").join("logs"); + } + } + + #[cfg(target_os = "linux")] + { + // Linux: Use XDG_DATA_HOME if set, otherwise ~/.local/share/ + if let Ok(xdg_data) = std::env::var("XDG_DATA_HOME") { + return PathBuf::from(xdg_data).join("robrix").join("logs"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home) + .join(".local") + .join("share") + .join("robrix") + .join("logs"); + } + } + + // Fallback to app data directory + crate::app_data_dir().join("logs") +} + +/// Cleans up old log files, keeping only the most recent N log files. +/// This should be called periodically to prevent disk space issues. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn cleanup_old_logs(max_logs_to_keep: usize) { + let logs_dir = logs_dir(); + if !logs_dir.exists() { + return; + } + + // Collect all log files (excluding the symlink) + let mut log_files: Vec<_> = match std::fs::read_dir(&logs_dir) { + Ok(entries) => entries + .filter_map(|e| e.ok()) + .filter(|e| { + let name = e.file_name(); + let name_str = name.to_string_lossy(); + name_str.starts_with("robrix_") + && name_str.ends_with(".log") + && name_str != "robrix_latest.log" + }) + .collect(), + Err(_) => return, + }; + + // Sort by modification time (oldest first) + log_files.sort_by(|a, b| { + let a_time = a.metadata().and_then(|m| m.modified()).ok(); + let b_time = b.metadata().and_then(|m| m.modified()).ok(); + a_time.cmp(&b_time) + }); + + // Remove old log files + if log_files.len() > max_logs_to_keep { + let files_to_remove = log_files.len() - max_logs_to_keep; + for entry in log_files.into_iter().take(files_to_remove) { + let _ = std::fs::remove_file(entry.path()); + } + } +} + +/// Maximum number of log files to keep +#[cfg(not(any(target_os = "android", target_os = "ios")))] +const MAX_LOG_FILES_TO_KEEP: usize = 10; + impl MatchEvent for App { fn handle_startup(&mut self, cx: &mut Cx) { // only init logging/tracing once let _ = tracing_subscriber::fmt::try_init(); + // Initialize the project directory here from the main UI thread + // such that background threads/tasks will be able to access it. + // This must be done before initializing file logging. + let _app_data_dir = crate::app_data_dir(); + // Initialize file logging for packaged builds (non-mobile platforms). + // This must be done before setting up the log handler. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + init_file_logging(); + // Clean up old log files to prevent disk space issues + cleanup_old_logs(MAX_LOG_FILES_TO_KEEP); + } // Override Makepad's new default-JSON logger. We just want regular formatting. fn regular_log(file_name: &str, line_start: u32, column_start: u32, _line_end: u32, _column_end: u32, message: String, level: LogLevel) { let l = match level { From 4983cc25023ad5ed39ac443c805422ffee17034b Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 27 Mar 2026 10:18:02 +0800 Subject: [PATCH 039/283] Add multi-account management support (issue #374) Introduces AccountManager for handling multiple Matrix accounts: - Account struct for storing client, user_id, session, profile info - AccountManager for add/remove/switch account operations - Global singleton with thread-safe access functions Co-Authored-By: Claude Opus 4.5 --- src/account_manager.rs | 250 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 + 2 files changed, 252 insertions(+) create mode 100644 src/account_manager.rs diff --git a/src/account_manager.rs b/src/account_manager.rs new file mode 100644 index 000000000..e23732e67 --- /dev/null +++ b/src/account_manager.rs @@ -0,0 +1,250 @@ +//! Multi-account management for Robrix. +//! +//! This module provides the infrastructure for managing multiple Matrix accounts +//! simultaneously, including: +//! - Storing and switching between multiple logged-in accounts +//! - Tracking the active (currently selected) account +//! - Managing account-specific state and sync connections + +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; +use matrix_sdk::{Client, ruma::OwnedUserId}; +use crate::persistence::ClientSessionPersisted; + +/// Represents a logged-in Matrix account with its associated client and session info. +#[derive(Clone)] +pub struct Account { + /// The Matrix client for this account + pub client: Client, + /// The user ID for this account + pub user_id: OwnedUserId, + /// The persisted session data for rebuilding the client + pub session: ClientSessionPersisted, + /// Display name for the account (cached from profile) + pub display_name: Option, + /// Avatar URL for the account (cached from profile) + pub avatar_url: Option, +} + +impl std::fmt::Debug for Account { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Account") + .field("user_id", &self.user_id) + .field("display_name", &self.display_name) + .field("avatar_url", &self.avatar_url) + .finish_non_exhaustive() + } +} + +/// Manager for multiple Matrix accounts. +/// +/// This struct handles: +/// - Storing multiple logged-in accounts +/// - Tracking which account is currently active +/// - Providing access to account-specific clients +#[derive(Default, Debug)] +pub struct AccountManager { + /// Map of user_id to Account for all logged-in accounts + accounts: HashMap, + /// The currently active (selected) account's user_id + active_account_id: Option, +} + +impl AccountManager { + /// Creates a new empty AccountManager. + pub fn new() -> Self { + Self { + accounts: HashMap::new(), + active_account_id: None, + } + } + + /// Adds a new account to the manager. + /// + /// If this is the first account, it becomes the active account automatically. + /// Returns true if the account was newly added, false if it replaced an existing one. + pub fn add_account(&mut self, account: Account) -> bool { + let user_id = account.user_id.clone(); + let is_new = !self.accounts.contains_key(&user_id); + + // If this is the first account, make it active + if self.accounts.is_empty() { + self.active_account_id = Some(user_id.clone()); + } + + self.accounts.insert(user_id, account); + is_new + } + + /// Removes an account from the manager. + /// + /// If the removed account was active, switches to another available account. + /// Returns the removed account if it existed. + pub fn remove_account(&mut self, user_id: &OwnedUserId) -> Option { + let removed = self.accounts.remove(user_id); + + // If we removed the active account, switch to another one + if self.active_account_id.as_ref() == Some(user_id) { + self.active_account_id = self.accounts.keys().next().cloned(); + } + + removed + } + + /// Sets the active account by user_id. + /// + /// Returns true if the account exists and was made active, false otherwise. + pub fn set_active_account(&mut self, user_id: &OwnedUserId) -> bool { + if self.accounts.contains_key(user_id) { + self.active_account_id = Some(user_id.clone()); + true + } else { + false + } + } + + /// Gets the currently active account. + pub fn active_account(&self) -> Option<&Account> { + self.active_account_id + .as_ref() + .and_then(|id| self.accounts.get(id)) + } + + /// Gets the currently active account mutably. + pub fn active_account_mut(&mut self) -> Option<&mut Account> { + let id = self.active_account_id.clone()?; + self.accounts.get_mut(&id) + } + + /// Gets the client for the currently active account. + pub fn active_client(&self) -> Option { + self.active_account().map(|a| a.client.clone()) + } + + /// Gets the user_id of the currently active account. + pub fn active_user_id(&self) -> Option<&OwnedUserId> { + self.active_account_id.as_ref() + } + + /// Gets an account by user_id. + pub fn get_account(&self, user_id: &OwnedUserId) -> Option<&Account> { + self.accounts.get(user_id) + } + + /// Gets a client by user_id. + pub fn get_client(&self, user_id: &OwnedUserId) -> Option { + self.accounts.get(user_id).map(|a| a.client.clone()) + } + + /// Returns an iterator over all accounts. + pub fn accounts(&self) -> impl Iterator { + self.accounts.values() + } + + /// Returns the number of logged-in accounts. + pub fn account_count(&self) -> usize { + self.accounts.len() + } + + /// Returns true if there are no logged-in accounts. + pub fn is_empty(&self) -> bool { + self.accounts.is_empty() + } + + /// Returns all user IDs of logged-in accounts. + pub fn user_ids(&self) -> Vec { + self.accounts.keys().cloned().collect() + } + + /// Updates the display name for an account. + pub fn update_display_name(&mut self, user_id: &OwnedUserId, display_name: Option) { + if let Some(account) = self.accounts.get_mut(user_id) { + account.display_name = display_name; + } + } + + /// Updates the avatar URL for an account. + pub fn update_avatar_url(&mut self, user_id: &OwnedUserId, avatar_url: Option) { + if let Some(account) = self.accounts.get_mut(user_id) { + account.avatar_url = avatar_url; + } + } +} + +// ============================================================================= +// Global Account Manager Singleton +// ============================================================================= + +/// Global singleton for the account manager. +static ACCOUNT_MANAGER: OnceLock> = OnceLock::new(); + +/// Gets the global account manager. +fn account_manager() -> &'static Mutex { + ACCOUNT_MANAGER.get_or_init(|| Mutex::new(AccountManager::new())) +} + +/// Adds an account to the global account manager. +pub fn add_account(account: Account) -> bool { + account_manager().lock().unwrap().add_account(account) +} + +/// Removes an account from the global account manager. +pub fn remove_account(user_id: &OwnedUserId) -> Option { + account_manager().lock().unwrap().remove_account(user_id) +} + +/// Sets the active account in the global account manager. +pub fn set_active_account(user_id: &OwnedUserId) -> bool { + account_manager().lock().unwrap().set_active_account(user_id) +} + +/// Gets the client for the currently active account. +pub fn get_active_client() -> Option { + account_manager().lock().unwrap().active_client() +} + +/// Gets the user_id of the currently active account. +pub fn get_active_user_id() -> Option { + account_manager().lock().unwrap().active_user_id().cloned() +} + +/// Gets a client by user_id. +pub fn get_client_for_user(user_id: &OwnedUserId) -> Option { + account_manager().lock().unwrap().get_client(user_id) +} + +/// Returns the number of logged-in accounts. +pub fn account_count() -> usize { + account_manager().lock().unwrap().account_count() +} + +/// Returns all user IDs of logged-in accounts. +pub fn get_all_user_ids() -> Vec { + account_manager().lock().unwrap().user_ids() +} + +/// Executes a closure with access to the account manager. +pub fn with_account_manager(f: F) -> R +where + F: FnOnce(&AccountManager) -> R, +{ + let manager = account_manager().lock().unwrap(); + f(&manager) +} + +/// Executes a closure with mutable access to the account manager. +pub fn with_account_manager_mut(f: F) -> R +where + F: FnOnce(&mut AccountManager) -> R, +{ + let mut manager = account_manager().lock().unwrap(); + f(&mut manager) +} + +/// Clears all accounts from the global account manager. +/// This should only be used during logout of all accounts. +pub fn clear_all_accounts() { + let mut manager = account_manager().lock().unwrap(); + manager.accounts.clear(); + manager.active_account_id = None; +} diff --git a/src/lib.rs b/src/lib.rs index 346c0314b..164d00802 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,8 @@ pub mod media_cache; pub mod verification; pub mod utils; +/// Multi-account management for supporting multiple Matrix accounts simultaneously. +pub mod account_manager; pub mod temp_storage; pub mod location; From cacb3ebd4e95aebd3554979871bbe9caba18bb1a Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 27 Mar 2026 10:23:45 +0800 Subject: [PATCH 040/283] Add multi-account UI and sync support (issue #374) - app.rs: Handle account switching actions and state, add adding_account field - sliding_sync.rs: Add AccountSwitchAction, is_add_account flag for logins, support running multiple sync connections - login_screen.rs: Support add-account login flow - account_settings.rs: Account switcher UI integration - navigation_tab_bar.rs: Account indicator updates Note: File logging changes (issue #345) intentionally excluded. Co-Authored-By: Claude Opus 4.5 --- src/app.rs | 64 +- src/home/navigation_tab_bar.rs | 9 +- src/login/login_screen.rs | 276 +++-- src/settings/account_settings.rs | 282 ++++- src/sliding_sync.rs | 1812 +++++++++++++++++++----------- 5 files changed, 1644 insertions(+), 799 deletions(-) diff --git a/src/app.rs b/src/app.rs index b9a75f6ee..1faa187b5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,7 @@ use crate::{ 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 }, 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}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}}, sliding_sync::{AccountSwitchAction, DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -293,11 +293,69 @@ impl MatchEvent for App { if let Some(LoginAction::LoginSuccess) = action.downcast_ref() { log!("Received LoginAction::LoginSuccess, hiding login view."); self.app_state.logged_in = true; + self.app_state.adding_account = false; self.update_login_visibility(cx); self.ui.redraw(cx); continue; } + // Handle request to show login screen for adding another account + if let Some(LoginAction::ShowAddAccountScreen) = action.downcast_ref() { + log!("Received LoginAction::ShowAddAccountScreen, showing login view for adding account."); + self.app_state.adding_account = true; + self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, true); + self.ui.redraw(cx); + continue; + } + + // Handle successful addition of a new account + if let Some(LoginAction::AddAccountSuccess) = action.downcast_ref() { + log!("Received LoginAction::AddAccountSuccess, hiding login view."); + self.app_state.adding_account = false; + self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, false); + self.ui.redraw(cx); + continue; + } + + // Handle account switch actions + match action.downcast_ref() { + Some(AccountSwitchAction::Starting(user_id)) => { + log!("Account switch starting to: {}", user_id); + // Clear UI state during account switch + clear_all_app_state(cx); + self.app_state.selected_room = None; + // Clear saved dock state so tabs will be closed + self.app_state.saved_dock_state_home = Default::default(); + enqueue_popup_notification( + format!("Switching to account {}...", user_id), + PopupKind::Info, + Some(3.0), + ); + self.ui.redraw(cx); + continue; + } + Some(AccountSwitchAction::Switched(user_id)) => { + log!("Account switch completed to: {}", user_id); + enqueue_popup_notification( + format!("Switched to account {}", user_id), + PopupKind::Info, + Some(5.0), + ); + self.ui.redraw(cx); + continue; + } + Some(AccountSwitchAction::Failed(error)) => { + log!("Account switch failed: {}", error); + enqueue_popup_notification( + format!("Failed to switch account: {}", error), + PopupKind::Error, + None, + ); + continue; + } + _ => {} + } + // If a login failure occurs mid-session (e.g., an expired/revoked token detected // by `handle_session_changes`), navigate back to the login screen. // When not yet logged in, the login_screen widget handles displaying the failure modal. @@ -1026,6 +1084,10 @@ pub struct AppState { pub saved_dock_state_per_space: HashMap, /// Whether a user is currently logged in to Robrix or not. pub logged_in: bool, + /// Whether the app is currently showing the login screen for adding another account. + /// This is transient state and not persisted. + #[serde(skip)] + pub adding_account: bool, /// Local configuration and UI state for bot-assisted room binding. pub bot_settings: BotSettingsState, } diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index 95cec1317..1f52e9b6c 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -36,7 +36,7 @@ use crate::{ user_profile_cache::{self, UserProfileUpdate}, }, shared::{ avatar::{AvatarState, AvatarWidgetExt}, styles::*, verification_badge::VerificationBadgeWidgetExt - }, sliding_sync::{current_user_id, AccountDataAction}, utils::{self, RoomNameId} + }, sliding_sync::{current_user_id, AccountDataAction, AccountSwitchAction}, utils::{self, RoomNameId} }; script_mod! { @@ -289,6 +289,13 @@ impl Widget for ProfileIcon { continue; } + // Handle account switch - refresh profile with new account's data + if let Some(AccountSwitchAction::Switched(_new_user_id)) = action.downcast_ref() { + self.own_profile = get_own_profile(cx); + self.view.redraw(cx); + continue; + } + // Handle account data changes (e.g., avatar updated/removed) match action.downcast_ref() { Some(AccountDataAction::AvatarChanged(None)) => { diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index bf4ec59c2..5fdedfa1d 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; +use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -69,13 +69,19 @@ script_mod! { } } - View { + RoundedView { margin: Inset{top: 40, bottom: 40} width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, + show_bg: true, + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 6.0 + } + View { width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit @@ -117,19 +123,6 @@ script_mod! { is_password: true, } - confirm_password_wrapper := View { - width: 275, height: Fit, - visible: false, - - confirm_password_input := RobrixTextInput { - width: 275, height: Fit - flow: Right, // do not wrap - padding: 10, - empty_text: "Confirm password" - is_password: true, - } - } - View { width: 275, height: Fit, flow: Down, @@ -178,61 +171,54 @@ script_mod! { text: "Login" } - login_only_view := View { - width: Fit, height: Fit, - flow: Down, - align: Align{x: 0.5, y: 0.5} - spacing: 15.0 + LineH { + width: 275 + margin: Inset{bottom: -5} + draw_bg.color: #C8C8C8 + } - LineH { - width: 275 - margin: Inset{bottom: -5} - draw_bg.color: #C8C8C8 + Label { + width: Fit, height: Fit + padding: 0, + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 11.0} } + text: "Or, login with an SSO provider:" + } - Label { - width: Fit, height: Fit - padding: 0, - draw_text +: { - color: (COLOR_TEXT) - text_style: TITLE_TEXT {font_size: 11.0} + sso_view := View { + width: 275, height: Fit, + margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide + flow: Flow.Right{wrap: true}, + apple_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/apple.png") } - text: "Or, login with an SSO provider:" } - - sso_view := View { - width: 275, height: Fit, - margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide - flow: Flow.Right{wrap: true}, - apple_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/apple.png") - } - } - facebook_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/facebook.png") - } + facebook_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/facebook.png") } - github_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/github.png") - } + } + github_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/github.png") } - gitlab_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/gitlab.png") - } + } + gitlab_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/gitlab.png") } - google_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/google.png") - } + } + google_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/google.png") } - twitter_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/x.png") - } + } + twitter_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/x.png") } } } @@ -247,7 +233,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - account_prompt_label := Label { + Label { width: Fit, height: Fit padding: Inset{left: 1, right: 1, top: 0, bottom: 0} draw_text +: { @@ -260,13 +246,23 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - mode_toggle_button := RobrixIconButton { + signup_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{bottom: 5} align: Align{x: 0.5, y: 0.5} text: "Sign up here" } + + // Cancel button for add-account mode (hidden by default) + cancel_button := RobrixIconButton { + width: Fit, height: Fit + padding: Inset{left: 15, right: 15, top: 10, bottom: 10} + margin: Inset{top: 10, bottom: 5} + align: Align{x: 0.5, y: 0.5} + text: "Cancel" + visible: false + } } // The modal that pops up to display login status messages, @@ -284,44 +280,18 @@ script_mod! { } } +static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; + #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { #[source] source: ScriptObjectRef, #[deref] view: View, - /// Whether the screen is showing the in-app sign-up flow. - #[rust] signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight #[rust] sso_pending: bool, /// The URL to redirect to after logging in with SSO. #[rust] sso_redirect_url: Option, - /// The most recent login failure message shown to the user. - #[rust] last_failure_message_shown: Option, -} - -impl LoginScreen { - fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { - self.signup_mode = signup_mode; - self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); - self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); - self.view.label(cx, ids!(title)).set_text(cx, - if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } - ); - self.view.button(cx, ids!(login_button)).set_text(cx, - if signup_mode { "Create account" } else { "Login" } - ); - self.view.label(cx, ids!(account_prompt_label)).set_text(cx, - if signup_mode { "Already have an account?" } else { "Don't have an account?" } - ); - self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, - if signup_mode { "Back to login" } else { "Sign up here" } - ); - - if !signup_mode { - self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); - } - - self.redraw(cx); - } + /// Boolean to indicate if we're in "add account" mode (adding another Matrix account). + #[rust] adding_account: bool, } @@ -339,29 +309,40 @@ impl Widget for LoginScreen { impl MatchEvent for LoginScreen { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let login_button = self.view.button(cx, ids!(login_button)); - let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); + let signup_button = self.view.button(cx, ids!(signup_button)); + let cancel_button = self.view.button(cx, ids!(cancel_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); let password_input = self.view.text_input(cx, ids!(password_input)); - let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); let login_status_modal_inner = self.view.login_status_modal(cx, ids!(login_status_modal_inner)); - if mode_toggle_button.clicked(actions) { - self.set_signup_mode(cx, !self.signup_mode); + // Handle cancel button for add-account mode + if cancel_button.clicked(actions) { + self.adding_account = false; + // Reset the UI back to normal login mode + self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + cancel_button.set_visible(cx, false); + self.view.view(cx, ids!(sso_view)).set_visible(cx, true); + signup_button.set_visible(cx, true); + cx.action(LoginAction::CancelAddAccount); + self.redraw(cx); + } + + if signup_button.clicked(actions) { + log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); + let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); } if login_button.clicked(actions) || user_id_input.returned(actions).is_some() || password_input.returned(actions).is_some() - || (self.signup_mode && confirm_password_input.returned(actions).is_some()) || homeserver_input.returned(actions).is_some() { - let user_id = user_id_input.text().trim().to_owned(); + let user_id = user_id_input.text(); let password = password_input.text(); - let confirm_password = confirm_password_input.text(); - let homeserver = homeserver_input.text().trim().to_owned(); + let homeserver = homeserver_input.text(); if user_id.is_empty() { login_status_modal_inner.set_title(cx, "Missing User ID"); login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); @@ -370,39 +351,16 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, "Missing Password"); login_status_modal_inner.set_status(cx, "Please enter a valid password."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); - } else if self.signup_mode && password != confirm_password { - login_status_modal_inner.set_title(cx, "Passwords do not match"); - login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); - login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { - self.last_failure_message_shown = None; - login_status_modal_inner.set_title(cx, if self.signup_mode { - "Creating account..." - } else { - "Logging in..." - }); - login_status_modal_inner.set_status( - cx, - if self.signup_mode { - "Waiting for the homeserver to create your account..." - } else { - "Waiting for a login response..." - }, - ); + login_status_modal_inner.set_title(cx, "Logging in..."); + login_status_modal_inner.set_status(cx, "Waiting for a login response..."); login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); - submit_async_request(MatrixRequest::Login(if self.signup_mode { - LoginRequest::Register(RegisterAccount { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }) - } else { - LoginRequest::LoginByPassword(LoginByPassword { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - }) - })); + submit_async_request(MatrixRequest::Login(LoginRequest::LoginByPassword(LoginByPassword { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + is_add_account: self.adding_account, + }))); } login_status_modal.open(cx); self.redraw(cx); @@ -425,7 +383,6 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { - self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); @@ -440,7 +397,6 @@ impl MatchEvent for LoginScreen { login_status_modal.open(cx); } Some(LoginAction::Status { title, status }) => { - self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); @@ -452,25 +408,19 @@ impl MatchEvent for LoginScreen { Some(LoginAction::LoginSuccess) => { // The main `App` component handles showing the main screen // and hiding the login screen & login status modal. - self.last_failure_message_shown = None; - self.set_signup_mode(cx, false); + self.adding_account = false; user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); - confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); + // Reset title and buttons in case we were in add-account mode + self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + cancel_button.set_visible(cx, false); + signup_button.set_visible(cx, true); login_status_modal.close(cx); self.redraw(cx); } Some(LoginAction::LoginFailure(error)) => { - if self.last_failure_message_shown.as_deref() == Some(error.as_str()) { - continue; - } - self.last_failure_message_shown = Some(error.clone()); - login_status_modal_inner.set_title(cx, if self.signup_mode { - "Account Creation Failed." - } else { - "Login Failed." - }); + login_status_modal_inner.set_title(cx, "Login Failed."); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); @@ -495,6 +445,28 @@ impl MatchEvent for LoginScreen { Some(LoginAction::SsoSetRedirectUrl(url)) => { self.sso_redirect_url = Some(url.to_string()); } + Some(LoginAction::ShowAddAccountScreen) => { + self.adding_account = true; + // Update UI to "add account" mode + self.view.label(cx, ids!(title)).set_text(cx, "Add Another Account"); + cancel_button.set_visible(cx, true); + // Hide signup button in add-account mode (user already has an account) + signup_button.set_visible(cx, false); + self.redraw(cx); + } + Some(LoginAction::AddAccountSuccess) => { + // Reset the login screen state + self.adding_account = false; + user_id_input.set_text(cx, ""); + password_input.set_text(cx, ""); + homeserver_input.set_text(cx, ""); + // Reset title and buttons + self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); + cancel_button.set_visible(cx, false); + signup_button.set_visible(cx, true); + login_status_modal.close(cx); + self.redraw(cx); + } _ => { } } } @@ -529,6 +501,9 @@ impl MatchEvent for LoginScreen { pub enum LoginAction { /// A positive response from the backend Matrix task to the login screen. LoginSuccess, + /// A positive response when adding an additional account (multi-account mode). + /// The login was successful but we should add this as a new account, not replace the existing one. + AddAccountSuccess, /// A negative response from the backend Matrix task to the login screen. LoginFailure(String), /// A login-related status message to display to the user. @@ -546,15 +521,20 @@ pub enum LoginAction { /// informing it that the SSO login process is either still in flight (`true`) or has finished (`false`). /// /// Note that an inner value of `false` does *not* imply that the login request has - /// successfully finished. + /// successfully finished. /// The login screen can use this to prevent the user from submitting - /// additional SSO login requests while a previous request is in flight. + /// additional SSO login requests while a previous request is in flight. SsoPending(bool), /// Set the SSO redirect URL in the LoginScreen. /// /// When an SSO-based login is pendng, pressing the cancel button will send /// an HTTP request to this SSO server URL to gracefully shut it down. SsoSetRedirectUrl(Url), + /// Request to show the login screen in "add account" mode. + /// This is used when the user wants to add another Matrix account. + ShowAddAccountScreen, + /// Request to cancel adding an account and return to the previous screen. + CancelAddAccount, #[default] None, } diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 877f66bfc..25b2d43fc 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -2,7 +2,8 @@ use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; -use crate::{app::ConfirmDeleteAction, avatar_cache::{self}, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, utils}; +use matrix_sdk::ruma::OwnedUserId; +use crate::{account_manager, app::ConfirmDeleteAction, avatar_cache::{self}, home::navigation_tab_bar::get_own_profile, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, AccountSwitchAction, MatrixRequest, submit_async_request}, utils}; script_mod! { use mod.prelude.widgets.* @@ -174,6 +175,127 @@ script_mod! { } } + SubsectionLabel { + text: "Multiple Accounts:" + } + + View { + width: Fill, height: Fit + flow: Down, + spacing: 8, + margin: Inset{left: 5, right: 5, bottom: 10} + + // Account entries will be shown here + // Active account (current) + active_account_view := RoundedView { + width: Fill, height: Fit + flow: Right, + align: Align{y: 0.5} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 10 + show_bg: true + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + border_radius: 4.0 + } + + View { + width: Fill, height: Fit + flow: Down, + spacing: 2 + + active_account_label := Label { + width: Fill, height: Fit + draw_text +: { + color: (COLOR_TEXT), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "@user:server" + } + + Label { + width: Fit, height: Fit + draw_text +: { + color: (COLOR_FG_ACCEPT_GREEN), + text_style: MESSAGE_TEXT_STYLE { font_size: 9 }, + } + text: "Active" + } + } + } + + // Other accounts section (populated dynamically) + other_accounts_label := Label { + width: Fill, height: Fit + margin: Inset{top: 5, left: 2} + visible: false + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 10 }, + } + text: "Other accounts:" + } + + // Container for other account entries (simplified: show one other account) + other_account_entry := RoundedView { + width: Fill, height: Fit + flow: Right, + align: Align{y: 0.5} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 10 + visible: false + show_bg: true + draw_bg +: { + color: (COLOR_SECONDARY) + border_radius: 4.0 + border_size: 1.0 + border_color: #555 + } + + View { + width: Fill, height: Fit + flow: Down, + spacing: 2 + + other_account_label := Label { + width: Fill, height: Fit + draw_text +: { + color: (COLOR_TEXT), + text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, + } + text: "@other:server" + } + } + + switch_account_button := RobrixIconButton { + width: Fit, height: Fit + padding: Inset{top: 6, bottom: 6, left: 10, right: 10} + draw_icon.svg: (ICON_JUMP) + icon_walk: Walk{width: 14, height: 14} + text: "Switch" + } + } + + account_count_label := Label { + width: Fill, height: Fit + margin: Inset{top: 5, bottom: 5, left: 5} + draw_text +: { + color: (MESSAGE_TEXT_COLOR), + text_style: MESSAGE_TEXT_STYLE { font_size: 10 }, + } + text: "1 account logged in" + } + + add_account_button := RobrixIconButton { + width: Fit, + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} + margin: Inset{top: 5} + draw_icon.svg: (ICON_ADD) + icon_walk: Walk{width: 16, height: 16} + text: "Add Another Account" + } + } + SubsectionLabel { text: "Other actions:" } @@ -210,6 +332,8 @@ pub struct AccountSettings { #[deref] view: View, #[rust] own_profile: Option, + /// List of other account user IDs (not the currently active one) + #[rust] other_accounts: Vec, } impl Widget for AccountSettings { @@ -221,7 +345,7 @@ impl Widget for AccountSettings { match event.hits(cx, copy_user_id_button_area) { Hit::FingerHoverIn(_) | Hit::FingerLongPress(_) => { cx.widget_action( - copy_user_id_button.widget_uid(), + copy_user_id_button.widget_uid(), TooltipAction::HoverIn { text: "Copy User ID".to_string(), widget_rect: copy_user_id_button_area.rect(cx), @@ -234,7 +358,7 @@ impl Widget for AccountSettings { } Hit::FingerHoverOut(_) => { cx.widget_action( - copy_user_id_button.widget_uid(), + copy_user_id_button.widget_uid(), TooltipAction::HoverOut, ); } @@ -371,14 +495,70 @@ impl MatchEvent for AccountSettings { let Some(own_profile) = &self.own_profile else { return }; if upload_avatar_button.clicked(actions) { - // TODO: uncomment the below once avatar uploading is implemented - // Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); - // Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); - enqueue_popup_notification( - "Avatar uploading is not yet implemented.", - PopupKind::Warning, - Some(4.0), - ); + Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); + Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); + + // Use rfd directly on the main thread (modal dialog blocks until selection) + let file_dialog = rfd::FileDialog::new() + .add_filter("Images", &["png", "jpg", "jpeg", "gif", "webp"]) + .set_title("Select Avatar Image"); + + if let Some(path) = file_dialog.pick_file() { + // Read the file data + match std::fs::read(&path) { + Ok(data) => { + if data.is_empty() { + enqueue_popup_notification( + "Cannot upload empty file.", + PopupKind::Error, + None, + ); + Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); + Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); + } else { + let file_name = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("avatar") + .to_string(); + + // Determine MIME type from extension + let mime_type = mime_guess::from_path(&path) + .first_or(mime_guess::mime::IMAGE_PNG) + .to_string(); + + log!("Avatar file selected: {} ({}, {} bytes)", file_name, mime_type, data.len()); + + // Submit the avatar upload request + submit_async_request(MatrixRequest::UploadAvatar { + file_name, + mime_type, + data, + }); + + enqueue_popup_notification( + "Uploading avatar...", + PopupKind::Info, + Some(3.0), + ); + Cx::post_action(AccountSettingsAction::AvatarUploadStarted); + } + } + Err(e) => { + error!("Failed to read avatar file: {:?}", e); + enqueue_popup_notification( + format!("Failed to read file: {}", e), + PopupKind::Error, + None, + ); + Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); + Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); + } + } + } else { + // User cancelled - re-enable buttons + Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); + Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); + } } if delete_avatar_button.clicked(actions) { @@ -459,6 +639,47 @@ impl MatchEvent for AccountSettings { if self.view.button(cx, ids!(logout_button)).clicked(actions) { cx.action(LogoutConfirmModalAction::Open); } + + // Handle "Switch Account" button click + if self.view.button(cx, ids!(switch_account_button)).clicked(actions) { + // Switch to the first other account + if let Some(other_id) = self.other_accounts.first().cloned() { + log!("Switching to account: {}", other_id); + submit_async_request(MatrixRequest::SwitchAccount { user_id: other_id }); + } + } + + // Handle "Add Account" button click + if self.view.button(cx, ids!(add_account_button)).clicked(actions) { + // Navigate to login screen in "add account" mode + cx.action(LoginAction::ShowAddAccountScreen); + } + + // Handle account switch result and new account added + for action in actions { + if let Some(AccountSwitchAction::Switched(new_user_id)) = action.downcast_ref() { + log!("Account switched to: {}, refreshing profile and account list", new_user_id); + // Refresh the profile with new account's data + if let Some(new_profile) = get_own_profile(cx) { + self.own_profile = Some(new_profile.clone()); + // Update the UI with new profile + self.view.label(cx, ids!(user_id)) + .set_text(cx, new_profile.user_id.as_str()); + self.view.text_input(cx, ids!(display_name_input)) + .set_text(cx, new_profile.username.as_deref().unwrap_or_default()); + self.populate_avatar_views(cx); + } + // Refresh the account list to show new active account + self.populate_account_list(cx); + self.view.redraw(cx); + } + // Refresh account list when a new account is added + if let Some(LoginAction::AddAccountSuccess) = action.downcast_ref() { + log!("New account added, refreshing account list"); + self.populate_account_list(cx); + self.view.redraw(cx); + } + } } } @@ -517,6 +738,7 @@ impl AccountSettings { self.own_profile = Some(own_profile); self.populate_avatar_views(cx); + self.populate_account_list(cx); self.view.button(cx, ids!(upload_avatar_button)).reset_hover(cx); self.view.button(cx, ids!(delete_avatar_button)).reset_hover(cx); @@ -528,6 +750,44 @@ impl AccountSettings { self.view.redraw(cx); } + /// Populate the account list with logged-in accounts from the AccountManager. + fn populate_account_list(&mut self, cx: &mut Cx) { + let count = account_manager::account_count(); + let label_text = if count == 1 { + "1 account logged in".to_string() + } else { + format!("{} accounts logged in", count) + }; + self.view.label(cx, ids!(account_count_label)).set_text(cx, &label_text); + + // Get the active account + let active_user_id = account_manager::get_active_user_id(); + + // Show the active account + if let Some(ref active_id) = active_user_id { + self.view.label(cx, ids!(active_account_label)) + .set_text(cx, active_id.as_str()); + } + + // Get other accounts (excluding active) + let all_accounts = account_manager::get_all_user_ids(); + self.other_accounts = all_accounts + .into_iter() + .filter(|id| Some(id) != active_user_id.as_ref()) + .collect(); + + // Show "Other accounts" label and entry only if there are other accounts + let has_other_accounts = !self.other_accounts.is_empty(); + self.view.label(cx, ids!(other_accounts_label)).set_visible(cx, has_other_accounts); + self.view.view(cx, ids!(other_account_entry)).set_visible(cx, has_other_accounts); + + // If there's at least one other account, show it + if let Some(other_id) = self.other_accounts.first() { + self.view.label(cx, ids!(other_account_label)) + .set_text(cx, other_id.as_str()); + } + } + /// Enable or disable the delete avatar button. fn enable_delete_avatar_button( cx: &mut Cx, diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 131e6610f..4fa0fa6bc 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,14 +8,8 @@ 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::{ - account::register::v3::Request as RegistrationRequest, - error::ErrorKind, - profile::{AvatarUrl, DisplayName}, - receipt::create_receipt::v3::ReceiptType, - uiaa::{AuthData, AuthType, Dummy}, - }}, events::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::{MediaFormat, 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 @@ -37,8 +31,9 @@ use std::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefaul use std::io; use hashbrown::{HashMap, HashSet}; use crate::{ + account_manager::{self, Account}, 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 + add_room::KnockResultAction, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate, TypingUser}, 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}, @@ -47,7 +42,7 @@ use crate::{ }, 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)] +#[derive(Parser, Default, Clone)] struct Cli { /// The user ID to login with. #[clap(value_parser)] @@ -90,11 +85,9 @@ impl std::fmt::Debug for Cli { impl From for Cli { fn from(login: LoginByPassword) -> Self { Self { - user_id: login.user_id.trim().to_owned(), + user_id: login.user_id, password: login.password, - homeserver: login.homeserver - .map(|homeserver| homeserver.trim().to_owned()) - .filter(|homeserver| !homeserver.is_empty()), + homeserver: login.homeserver, proxy: None, login_screen: false, verbose: false, @@ -102,192 +95,6 @@ impl From for Cli { } } -impl From for Cli { - fn from(registration: RegisterAccount) -> Self { - Self { - user_id: registration.user_id.trim().to_owned(), - password: registration.password, - homeserver: registration.homeserver - .map(|homeserver| homeserver.trim().to_owned()) - .filter(|homeserver| !homeserver.is_empty()), - proxy: None, - login_screen: false, - verbose: false, - } - } -} - -fn infer_homeserver_from_user_id(user_id: &str) -> Option { - let user_id: OwnedUserId = user_id.trim().try_into().ok()?; - Some(user_id.server_name().to_string()) -} - -async fn finalize_authenticated_client( - client: Client, - client_session: ClientSessionPersisted, - fallback_user_id: &str, -) -> Result<(Client, Option)> { - if client.matrix_auth().logged_in() { - let logged_in_user_id = client.user_id() - .map(ToString::to_string) - .unwrap_or_else(|| fallback_user_id.to_owned()); - log!("Logged in successfully."); - let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); - 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!( - "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." - ); - enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); - enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); - bail!(err_msg); - } -} - -fn registration_localpart(user_id: &str) -> Result { - let trimmed = user_id.trim(); - if trimmed.is_empty() { - bail!("Please enter a valid username or Matrix user ID."); - } - - if let Ok(full_user_id) = >::try_from(trimmed) { - return Ok(full_user_id.localpart().to_owned()); - } - - let localpart = trimmed.trim_start_matches('@'); - if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { - bail!("Please enter a valid username or full Matrix user ID."); - } - - Ok(localpart.to_owned()) -} - -fn registration_request( - username: &str, - password: &str, - session: Option, -) -> RegistrationRequest { - let mut request = RegistrationRequest::new(); - request.username = Some(username.to_owned()); - request.password = Some(password.to_owned()); - request.initial_device_display_name = Some("robrix-un-pw".to_owned()); - request.refresh_token = true; - if let Some(session) = session { - let mut dummy = Dummy::new(); - dummy.session = Some(session); - request.auth = Some(AuthData::Dummy(dummy)); - } - request -} - -fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { - if let matrix_sdk::Error::Http(http_error) = error { - match http_error.client_api_error_kind() { - Some(ErrorKind::UserInUse) => { - return "That user ID is already taken. Please choose another one.".to_owned(); - } - Some(ErrorKind::InvalidUsername) => { - return "That user ID is invalid. Use a username like `alice` or a full Matrix ID like `@alice:matrix.org`.".to_owned(); - } - Some(ErrorKind::WeakPassword) => { - return "That password is too weak. Please choose a stronger password.".to_owned(); - } - Some(ErrorKind::Forbidden { .. }) => { - return "This homeserver does not allow open registration.".to_owned(); - } - Some(ErrorKind::LimitExceeded { .. }) => { - return "The homeserver is rate limiting account creation right now. Please try again shortly.".to_owned(); - } - _ => {} - } - } - - format!("Could not create account: {error}") -} - -fn unsupported_registration_flow_message( - flows: &[matrix_sdk::ruma::api::client::uiaa::AuthFlow], -) -> String { - let supports_registration_token = flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::RegistrationToken)) - }); - if supports_registration_token { - return "This homeserver requires a registration token. Robrix does not support token-based registration yet.".to_owned(); - } - - let supports_terms = flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::Terms)) - }); - if supports_terms { - return "This homeserver requires an interactive terms-of-service step. Robrix does not support that registration flow yet.".to_owned(); - } - - "This homeserver requires an unsupported registration flow. Please try another homeserver or register with a different client.".to_owned() -} - -async fn clear_persisted_session(user_id: Option<&UserId>) { - let Some(user_id) = user_id else { - return; - }; - - if let Err(e) = persistence::delete_session(user_id).await { - warning!("Failed to delete persisted session for {user_id}: {e}"); - } - - let latest_user_id = persistence::most_recent_user_id().await; - if latest_user_id.as_deref() == Some(user_id) { - if let Err(e) = persistence::delete_latest_user_id().await { - warning!("Failed to delete latest user id for {user_id}: {e}"); - } - } -} - -enum SessionResetAction { - Reauthenticate { message: String }, -} - -async fn reset_runtime_state_for_relogin() { - let sync_service = { SYNC_SERVICE.lock().unwrap().take() }; - if let Some(sync_service) = sync_service { - sync_service.stop().await; - } - - CLIENT.lock().unwrap().take(); - DEFAULT_SSO_CLIENT.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() }); - - if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { - warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); - } -} - -fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { - matches!( - error.client_api_error_kind(), - Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) - ) -} - -fn is_invalid_batch_token_timeline_error(error: &matrix_sdk_ui::timeline::Error) -> bool { - let error_text = error.to_string().to_ascii_lowercase(); - error_text.contains("invalid batch token") - || error_text.contains("must start with 's' or 't'") -} - /// Build a new client. async fn build_client( @@ -310,10 +117,7 @@ async fn build_client( .collect() }; - let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); let homeserver_url = cli.homeserver.as_deref() - .filter(|homeserver| !homeserver.trim().is_empty()) - .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); // .unwrap_or("https://matrix.org/"); @@ -364,19 +168,22 @@ async fn build_client( /// /// 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. +/// Upon success, this function returns the logged-in client, an optional sync token, +/// a boolean indicating if this is an add-account operation (multi-account mode), +/// and the client session for storing in the account manager. async fn login( cli: &Cli, login_request: LoginRequest, -) -> Result<(Client, Option)> { +) -> Result<(Client, Option, bool, ClientSessionPersisted)> { match login_request { LoginRequest::LoginByCli | LoginRequest::LoginByPassword(_) => { - let cli = if let LoginRequest::LoginByPassword(login_by_password) = login_request { - &Cli::from(login_by_password) + let (cli, is_add_account) = if let LoginRequest::LoginByPassword(login_by_password) = login_request { + let is_add_account = login_by_password.is_add_account; + (Cli::from(login_by_password), is_add_account) } else { - cli + ((*cli).clone(), false) }; - let (client, client_session) = build_client(cli, app_data_dir()).await?; + 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), @@ -388,78 +195,30 @@ async fn login( .initial_device_display_name("robrix-un-pw") .send() .await?; - if !client.matrix_auth().logged_in() { - 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); - } - finalize_authenticated_client(client, client_session, &cli.user_id).await - } - - LoginRequest::Register(registration) => { - let cli = Cli::from(RegisterAccount { - user_id: registration.user_id.clone(), - password: registration.password.clone(), - homeserver: registration.homeserver.clone(), - }); - let localpart = registration_localpart(®istration.user_id)?; - let (client, client_session) = build_client(&cli, app_data_dir()).await?; - Cx::post_action(LoginAction::Status { - title: "Creating account".into(), - status: format!("Creating account {localpart}..."), - }); - - let auth = client.matrix_auth(); - let initial_request = registration_request(&localpart, ®istration.password, None); - let register_result = match auth.register(initial_request).await { - Ok(response) => Ok(response), - Err(error) => { - if let Some(uiaa_info) = error.as_uiaa_response() { - let supports_dummy = uiaa_info.flows.iter().any(|flow| { - flow.stages - .iter() - .any(|stage| matches!(stage, AuthType::Dummy)) - }); - if supports_dummy { - Cx::post_action(LoginAction::Status { - title: "Completing sign up".into(), - status: "Confirming registration with the homeserver...".into(), - }); - auth.register(registration_request( - &localpart, - ®istration.password, - uiaa_info.session.clone(), - )) - .await - } else { - bail!(unsupported_registration_flow_message(&uiaa_info.flows)); - } - } else { - bail!(registration_uiaa_error_message(&error)); - } + 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.clone()).await { + let err_msg = format!("Failed to save session state to storage: {e}"); + error!("{err_msg}"); + enqueue_popup_notification(err_msg, PopupKind::Error, None); } - }?; - - if !client.matrix_auth().logged_in() { - let err_msg = format!( - "Account {} was created, but the homeserver did not return a login session. Please log in manually.", - register_result.user_id, - ); + Ok((client, None, is_add_account, client_session)) + } 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); } - - finalize_authenticated_client(client, client_session, register_result.user_id.as_str()) - .await } - LoginRequest::LoginBySSOSuccess(client, client_session) => { - if let Err(e) = persistence::save_session(&client, client_session).await { + LoginRequest::LoginBySSOSuccess(client, client_session, is_add_account) => { + if let Err(e) = persistence::save_session(&client, client_session.clone()).await { error!("Failed to save session state to storage: {e:?}"); } - Ok((client, None)) + Ok((client, None, is_add_account, client_session)) } LoginRequest::HomeserverLoginTypesQuery(_) => { bail!("LoginRequest::HomeserverLoginTypesQuery not handled earlier"); @@ -558,6 +317,17 @@ pub enum AccountDataAction { DisplayNameChangeFailed(String), } +/// Actions emitted in response to account switching. +#[derive(Debug, Clone)] +pub enum AccountSwitchAction { + /// Account switch is starting - UI should show loading state. + Starting(OwnedUserId), + /// Successfully switched to a different account. + Switched(OwnedUserId), + /// Failed to switch accounts. + Failed(String), +} + /// Actions emitted in response to a [`MatrixRequest::OpenOrCreateDirectMessage`]. #[derive(Debug)] pub enum DirectMessageRoomAction { @@ -624,6 +394,10 @@ impl std::fmt::Display for TimelineKind { pub enum MatrixRequest { /// Request from the login screen to log in with the given credentials. Login(LoginRequest), + /// Request to switch to a different logged-in account. + SwitchAccount { + user_id: OwnedUserId, + }, /// Request to logout. Logout { is_desktop: bool, @@ -679,12 +453,6 @@ pub enum MatrixRequest { room_id: OwnedRoomId, user_id: OwnedUserId, }, - /// Request to bind or unbind the configured botfather for the given room. - SetRoomBotBinding { - room_id: OwnedRoomId, - bound: bool, - bot_user_id: OwnedUserId, - }, /// Request to join the given room. JoinRoom { room_id: OwnedRoomId, @@ -791,6 +559,15 @@ pub enum MatrixRequest { /// * If `None`, the avatar will be removed. avatar_url: Option, }, + /// Request to upload and set a new avatar for the current user's account. + UploadAvatar { + /// The file name of the avatar image. + file_name: String, + /// The MIME type of the avatar image (e.g., "image/png", "image/jpeg"). + mime_type: String, + /// The raw bytes of the avatar image. + data: Vec, + }, /// 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. @@ -816,6 +593,15 @@ pub enum MatrixRequest { destination: MediaCacheEntryRef, update_sender: Option>, }, + /// Request to download a file from Matrix and save it to disk. + DownloadFile { + /// The media source of the file to download. + media_source: ruma::events::room::MediaSource, + /// The suggested filename for the downloaded file. + filename: String, + /// The destination path to save the file to. + destination_path: std::path::PathBuf, + }, /// Request to send a message to the given room. SendMessage { timeline_kind: TimelineKind, @@ -824,6 +610,16 @@ pub enum MatrixRequest { #[cfg(feature = "tsp")] sign_with_tsp: bool, }, + /// Request to send a file attachment to the given room. + SendAttachment { + room_id: OwnedRoomId, + file_name: String, + mime_type: String, + data: Vec, + /// Optional sender for progress updates. If provided, the upload will send + /// progress notifications through this channel. + timeline_update_sender: Option>, + }, /// 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 @@ -919,6 +715,60 @@ pub enum MatrixRequest { destination: Arc>, update_sender: Option>, }, + + // ==================== Call-related requests ==================== + + /// Request to start a new call in a room. + StartCall { + room_id: OwnedRoomId, + /// Whether this is a video call (vs audio-only). + is_video_call: bool, + }, + /// Request to join an existing call in a room. + JoinCall { + room_id: OwnedRoomId, + }, + /// Request to leave an ongoing call. + LeaveCall { + room_id: OwnedRoomId, + }, + /// Request to send a MatrixRTC call membership state event. + SendCallMembershipEvent { + room_id: OwnedRoomId, + /// The serialized membership event content. + membership_content: String, + }, + /// Toggle audio mute for the current call. + ToggleCallAudio { + room_id: OwnedRoomId, + }, + /// Toggle video for the current call. + ToggleCallVideo { + room_id: OwnedRoomId, + }, + /// Fetch the TURN server configuration from the homeserver. + GetTurnServers, + /// Fetch the RTC foci configuration from the homeserver's well-known endpoint. + /// This retrieves the LiveKit service URL from `org.matrix.msc4143.rtc_foci`. + FetchRtcWellKnown, + /// Fetch a LiveKit SFU JWT token for joining a call. + FetchLiveKitSfuToken { + room_id: OwnedRoomId, + /// The device ID of the local user. + device_id: String, + }, + /// Request to search room members in the background. + /// Used to avoid blocking the UI thread for large rooms. + SearchRoomMembers { + /// Unique ID to identify this search and discard stale results. + search_id: u64, + /// The search query string. + query: String, + /// The room ID this search is for. + room_id: OwnedRoomId, + /// The list of members to search through. + members: std::sync::Arc>, + }, } /// Submits a request to the worker thread to be executed asynchronously. @@ -932,8 +782,7 @@ pub fn submit_async_request(req: MatrixRequest) { /// Details of a login request that get submitted within [`MatrixRequest::Login`]. pub enum LoginRequest{ LoginByPassword(LoginByPassword), - Register(RegisterAccount), - LoginBySSOSuccess(Client, ClientSessionPersisted), + LoginBySSOSuccess(Client, ClientSessionPersisted, bool), LoginByCli, HomeserverLoginTypesQuery(String), @@ -943,14 +792,8 @@ pub struct LoginByPassword { pub user_id: String, pub password: String, pub homeserver: Option, -} - -/// Information needed to register a new account on a Matrix homeserver. -#[derive(Clone)] -pub struct RegisterAccount { - pub user_id: String, - pub password: String, - pub homeserver: Option, + /// Whether this login is for adding another account (multi-account mode). + pub is_add_account: bool, } @@ -971,11 +814,92 @@ async fn matrix_worker_task( 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." - ))); + // Check if this is an add-account login (when already logged in) + let is_add_account = match &login_request { + LoginRequest::LoginByPassword(lpw) => lpw.is_add_account, + LoginRequest::LoginBySSOSuccess(_, _, is_add) => *is_add, + _ => false, + }; + + if is_add_account { + // Handle add-account login directly in the worker task + log!("Processing add-account login directly in worker task"); + let cli = Cli::default(); + match login(&cli, login_request).await { + Ok((client, _sync_token, _is_add, session)) => { + let user_id = client.user_id() + .expect("BUG: client.user_id() returned None after login!"); + + // Add to account manager + let account = Account { + client: client.clone(), + user_id: user_id.to_owned(), + session, + display_name: None, + avatar_url: None, + }; + let is_new = account_manager::add_account(account); + log!("Add-account login successful for {}. New account: {}", user_id, is_new); + + // Post success action + Cx::post_action(LoginAction::AddAccountSuccess); + enqueue_popup_notification( + format!("Added account: {}", user_id), + PopupKind::Success, + Some(3.0), + ); + } + Err(e) => { + error!("Add-account login failed: {e:?}"); + Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); + } + } + } else { + // Forward to login_sender for initial login flow + 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::SwitchAccount { user_id } => { + log!("Received MatrixRequest::SwitchAccount for {}", user_id); + + // Check if the account exists in AccountManager + if account_manager::get_client_for_user(&user_id).is_some() { + // Set the target account for switch + set_account_switch_target(user_id.clone()); + + // Notify UI that switch is starting + Cx::post_action(AccountSwitchAction::Starting(user_id.clone())); + enqueue_popup_notification( + format!("Switching to {}...", user_id), + PopupKind::Info, + Some(2.0), + ); + + // Stop the sync service - this will cause the main loop to restart + if let Some(sync_service) = get_sync_service() { + log!("Stopping sync service for account switch"); + sync_service.stop().await; + } + + // The main loop will detect the account switch target and restart with the new account + // We return Ok(()) to signal the worker should end gracefully + return Ok(()); + } else { + error!("Account {} not found in AccountManager", user_id); + Cx::post_action(AccountSwitchAction::Failed( + format!("Account {} not found", user_id) + )); + enqueue_popup_notification( + format!("Account not found: {}", user_id), + PopupKind::Error, + Some(3.0), + ); } } @@ -1000,7 +924,6 @@ async fn matrix_worker_task( log!("Skipping pagination request for unknown {timeline_kind}"); continue; }; - let client = get_client(); // Spawn a new async task that will make the actual pagination request. let _paginate_task = Handle::current().spawn(async move { @@ -1008,45 +931,12 @@ async fn matrix_worker_task( sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap(); SignalToUI::set_ui_signal(); - let mut res = if direction == PaginationDirection::Forwards { + let res = if direction == PaginationDirection::Forwards { timeline.paginate_forwards(num_events).await } else { timeline.paginate_backwards(num_events).await }; - if direction == PaginationDirection::Backwards - && res - .as_ref() - .err() - .is_some_and(is_invalid_batch_token_timeline_error) - { - warning!( - "Detected an invalid cached batch token for {timeline_kind}; clearing the room event cache and retrying once." - ); - let room_id = timeline_kind.room_id().clone(); - if let Some(room) = client.and_then(|client| client.get_room(&room_id)) { - match room.event_cache().await { - Ok((room_event_cache, _drop_handles)) => { - match room_event_cache.clear().await { - Ok(()) => { - res = timeline.paginate_backwards(num_events).await; - } - Err(clear_error) => { - warning!( - "Failed to clear event cache for room {room_id} after invalid batch token: {clear_error}" - ); - } - } - } - Err(event_cache_error) => { - warning!( - "Failed to access room event cache for room {room_id} after invalid batch token: {event_cache_error}" - ); - } - } - } - } - match res { Ok(fully_paginated) => { log!("Completed {direction} pagination request for {timeline_kind}, hit {} of timeline? {}", @@ -1296,68 +1186,6 @@ async fn matrix_worker_task( }); } - MatrixRequest::SetRoomBotBinding { - room_id, - bound, - bot_user_id, - } => { - let Some(client) = get_client() else { continue }; - let _bot_binding_task = Handle::current().spawn(async move { - let Some(room) = client.get_room(&room_id) else { - let error_message = - format!("Room {room_id} was not found for the bot binding request."); - error!("{error_message}"); - enqueue_popup_notification(error_message, PopupKind::Error, None); - return; - }; - - let membership_result = if bound { - room.invite_user_by_id(&bot_user_id).await - } else { - room.kick_user(&bot_user_id, Some("Robrix app service unbind")).await - }; - - match membership_result { - Ok(()) => { - Cx::post_action(AppStateAction::BotRoomBindingUpdated { - room_id, - bound, - bot_user_id: Some(bot_user_id), - warning: None, - }); - } - Err(error) => { - let membership_exists = - room.get_member_no_sync(&bot_user_id).await.ok().flatten().is_some(); - let should_mark_bound = if bound { membership_exists } else { false }; - - if should_mark_bound != bound { - error!( - "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", - if bound { "invite" } else { "remove" } - ); - enqueue_popup_notification( - format!( - "Failed to {} BotFather {bot_user_id}: {error}", - if bound { "invite" } else { "remove" } - ), - PopupKind::Error, - None, - ); - return; - } - - Cx::post_action(AppStateAction::BotRoomBindingUpdated { - room_id, - bound, - bot_user_id: Some(bot_user_id), - warning: Some(error.to_string()), - }); - } - } - }); - } - MatrixRequest::JoinRoom { room_id } => { let Some(client) = get_client() else { continue }; let _join_room_task = Handle::current().spawn(async move { @@ -1431,12 +1259,14 @@ async fn matrix_worker_task( let room = timeline.room(); if local_only { - if let Ok(members) = room.members_no_sync(memberships).await { - send_update(members, "Got"); + match room.members_no_sync(memberships).await { + Ok(members) => send_update(members, "Got"), + Err(e) => error!("Failed to get room members (local_only) for {timeline_kind}: {e:?}"), } } else { - if let Ok(members) = room.members(memberships).await { - send_update(members, "Successfully fetched"); + match room.members(memberships).await { + Ok(members) => send_update(members, "Successfully fetched"), + Err(e) => error!("Failed to fetch room members for {timeline_kind}: {e:?}"), } } }); @@ -1661,6 +1491,48 @@ async fn matrix_worker_task( }); } + MatrixRequest::UploadAvatar { file_name, mime_type, data } => { + let Some(client) = get_client() else { continue }; + let _upload_avatar_task = Handle::current().spawn(async move { + log!("Uploading avatar {} ({}, {} bytes)...", file_name, mime_type, data.len()); + + // Parse the MIME type + let content_type: mime::Mime = mime_type.parse().unwrap_or(mime::IMAGE_PNG); + + // Upload the media to the server + match client.media().upload(&content_type, data, None).await { + Ok(response) => { + let mxc_uri = response.content_uri; + log!("Successfully uploaded avatar, got MXC URI: {}", mxc_uri); + + // Now set the avatar URL + match client.account().set_avatar_url(Some(&mxc_uri)).await { + Ok(_) => { + log!("Successfully set avatar to {}", mxc_uri); + Cx::post_action(AccountDataAction::AvatarChanged(Some(mxc_uri))); + enqueue_popup_notification( + "Avatar updated successfully!", + PopupKind::Info, + Some(3.0), + ); + } + Err(e) => { + let err_msg = format!("Failed to set avatar URL: {e}"); + error!("{}", err_msg); + Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); + } + } + } + Err(e) => { + let err_msg = format!("Failed to upload avatar: {e}"); + error!("{}", err_msg); + Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); + } + } + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::SetDisplayName { new_display_name } => { let Some(client) = get_client() else { continue }; let _set_display_name_task = Handle::current().spawn(async move { @@ -1810,15 +1682,21 @@ async fn matrix_worker_task( // 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 member = main_timeline.room() + .get_member_no_sync(&user_id) + .await + .ok() + .flatten(); + let display_name = member.as_ref() + .and_then(|m| m.display_name().map(|d| d.to_owned())) + .unwrap_or_else(|| user_id.to_string()); + let avatar_url = member.as_ref() + .and_then(|m| m.avatar_url().map(|u| u.to_owned())); + users.push(TypingUser { + user_id: user_id.clone(), + display_name, + avatar_url, + }); } 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:?}"); @@ -1943,7 +1821,7 @@ async fn matrix_worker_task( 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; @@ -1951,6 +1829,48 @@ async fn matrix_worker_task( }); } + MatrixRequest::DownloadFile { media_source, filename, destination_path } => { + let Some(client) = get_client() else { continue }; + + let _download_task = Handle::current().spawn(async move { + log!("Downloading file {filename} to {:?}...", destination_path); + let media_request = MediaRequestParameters { + source: media_source, + format: MediaFormat::File, + }; + match client.media().get_media_content(&media_request, true).await { + Ok(data) => { + match std::fs::write(&destination_path, &data) { + Ok(_) => { + log!("Successfully downloaded file to {:?}", destination_path); + enqueue_popup_notification( + format!("Downloaded: {filename}"), + PopupKind::Success, + None, + ); + } + Err(e) => { + error!("Failed to write file to {:?}: {e}", destination_path); + enqueue_popup_notification( + format!("Failed to save file: {e}"), + PopupKind::Error, + None, + ); + } + } + } + Err(e) => { + error!("Failed to download file {filename}: {e}"); + enqueue_popup_notification( + format!("Failed to download: {e}"), + PopupKind::Error, + None, + ); + } + } + }); + } + MatrixRequest::SendMessage { timeline_kind, message, @@ -2048,19 +1968,81 @@ async fn matrix_worker_task( }); } - 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}"); + MatrixRequest::SendAttachment { room_id, file_name, mime_type, data, timeline_update_sender } => { + let Some(client) = get_client() else { continue }; + let Some(room) = client.get_room(&room_id) else { + error!("BUG: room {room_id} not found for send attachment request"); + enqueue_popup_notification( + "Failed to send attachment: room not found.", + PopupKind::Error, + None, + ); 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:?}"), + let _send_attachment_task = Handle::current().spawn(async move { + use crate::home::room_screen::TimelineUpdate; + + let data_len = data.len() as u64; + log!("Sending attachment {} ({}, {} bytes) to room {}", file_name, mime_type, data_len, room_id); + + // Send initial progress update (0%) + if let Some(ref sender) = timeline_update_sender { + let _ = sender.send(TimelineUpdate::FileUploadProgress { current: 0, total: data_len }); + SignalToUI::set_ui_signal(); } - if let TimelineKind::MainRoom { room_id } = timeline_kind { - // Also update the number of unread messages in the room. + + // Parse the MIME type + let content_type: mime::Mime = mime_type.parse().unwrap_or(mime::APPLICATION_OCTET_STREAM); + + // Create attachment config + let config = matrix_sdk::attachment::AttachmentConfig::new(); + + // Send the attachment + match room.send_attachment(&file_name, &content_type, data, config).await { + Ok(_response) => { + log!("Successfully sent attachment {} to room {}", file_name, room_id); + // Send completion progress update (100%) + if let Some(ref sender) = timeline_update_sender { + let _ = sender.send(TimelineUpdate::FileUploadProgress { current: data_len, total: data_len }); + SignalToUI::set_ui_signal(); + } + enqueue_popup_notification( + format!("Sent: {}", file_name), + PopupKind::Info, + Some(3.0), + ); + } + Err(e) => { + error!("Failed to send attachment {} to room {}: {:?}", file_name, room_id, e); + if let Some(ref sender) = timeline_update_sender { + let _ = sender.send(TimelineUpdate::FileUploadError(format!("{}", e))); + SignalToUI::set_ui_signal(); + } + enqueue_popup_notification( + format!("Failed to send attachment: {}", 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(), @@ -2288,6 +2270,430 @@ async fn matrix_worker_task( SignalToUI::set_ui_signal(); }); } + + MatrixRequest::SearchRoomMembers { search_id, query, room_id, members } => { + // Perform the search in a background task to avoid blocking the worker. + Handle::current().spawn(async move { + let query_lower = query.to_lowercase(); + let matched_indices: Vec = members + .iter() + .enumerate() + .filter(|(_, m)| { + m.displayable_name().to_lowercase().contains(&query_lower) + || m.user_id.as_str().to_lowercase().contains(&query_lower) + }) + .map(|(i, _)| i) + .collect(); + + crate::home::members_panel::enqueue_member_search_result( + crate::home::members_panel::MemberSearchResult { + search_id, + room_id, + query, + matched_indices, + } + ); + }); + } + + // ==================== Call-related request handlers ==================== + MatrixRequest::StartCall { room_id, is_video_call } => { + log!("StartCall request received for room {} (video: {})", room_id, is_video_call); + let Some(client) = get_client() else { continue }; + let manager = crate::call::webrtc_manager::webrtc_manager(); + + let _task = Handle::current().spawn(async move { + let Some(user_id) = client.user_id().map(|u| u.to_owned()) else { + error!("StartCall: user_id not available"); + return; + }; + let Some(device_id) = client.device_id().map(|d| d.to_owned()) else { + error!("StartCall: device_id not available"); + return; + }; + let config = crate::call::webrtc_session::WebRTCSessionConfig::default(); + + match manager.start_call(room_id.clone(), user_id.clone(), device_id.clone(), is_video_call, config).await { + Ok(membership) => { + // Serialize membership directly (not wrapped in MatrixRTCMemberEvent) + match serde_json::to_value(&membership) { + Ok(content) => { + if let Some(room) = client.get_room(&room_id) { + // Use correct state key format: _@user:server_deviceId_m.call + let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( + user_id.as_str(), + device_id.as_str(), + ); + match room.send_state_event_raw( + crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, + &state_key, + content, + ).await { + Ok(_) => log!("Successfully sent call membership event for room {}", room_id), + Err(e) => error!("Failed to send call membership event: {}", e), + } + } else { + error!("StartCall: room {} not found", room_id); + } + } + Err(e) => error!("Failed to serialize membership event: {}", e), + } + } + Err(e) => error!("Failed to start call: {}", e), + } + }); + } + MatrixRequest::JoinCall { room_id } => { + log!("JoinCall request received for room {}", room_id); + let Some(client) = get_client() else { continue }; + + // Check if LiveKit service URL is available + let Some(livekit_service_url) = get_livekit_service_url() else { + error!("JoinCall: No LiveKit service URL available. Call joining requires LiveKit."); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: "LiveKit service not configured for this homeserver".to_string(), + }); + continue; + }; + + let room_id_clone = room_id.clone(); + let livekit_url_clone = livekit_service_url.clone(); + let _task = Handle::current().spawn(async move { + let Some(user_id) = client.user_id().map(|u| u.to_owned()) else { + error!("JoinCall: user_id not available"); + return; + }; + let Some(device_id) = client.device_id().map(|d| d.to_owned()) else { + error!("JoinCall: device_id not available"); + return; + }; + + // Step 1: Get OpenID token for authentication with LiveKit service + log!("JoinCall: Step 1 - Getting OpenID token"); + let openid_token = match crate::call::matrixrtc::get_openid_token(&client).await { + Ok(token) => { + log!("JoinCall: OpenID token obtained successfully"); + token + } + Err(e) => { + error!("JoinCall: Failed to get OpenID token: {}", e); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: format!("Failed to authenticate: {}", e), + }); + SignalToUI::set_ui_signal(); + return; + } + }; + + // Step 2: Send call membership state event to the room (BEFORE getting SFU token) + // This matches Element's flow where membership event is sent first + log!("JoinCall: Step 2 - Sending call membership state event"); + let start_time_ms = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + // Create membership content matching Element's format + let mut membership = crate::call::matrixrtc::MatrixRTCMembership::new(device_id.clone()) + .with_focus_active(crate::call::matrixrtc::FocusActive::livekit()) + .with_call_intent("video"); + + // Add LiveKit focus info with room_id as alias (matching Element) + let livekit_alias = room_id_clone.to_string(); + membership.add_preferred_focus( + crate::call::matrixrtc::FocusInfo::livekit(livekit_url_clone.clone(), livekit_alias.clone()) + ); + // Add second focus entry (Element sends two identical entries) + membership.add_preferred_focus( + crate::call::matrixrtc::FocusInfo::livekit(livekit_url_clone.clone(), livekit_alias) + ); + + // Serialize membership directly (not wrapped in MatrixRTCMemberEvent) + if let Ok(content) = serde_json::to_value(&membership) { + if let Some(room) = client.get_room(&room_id_clone) { + // State key format: _@user:server_deviceId_m.call + let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( + user_id.as_str(), + device_id.as_str(), + ); + log!("JoinCall: Sending membership with state_key: {}", state_key); + + match room.send_state_event_raw( + crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, + &state_key, + content, + ).await { + Ok(_) => log!("JoinCall: Membership state event sent successfully"), + Err(e) => { + error!("JoinCall: Failed to send membership event: {}", e); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: format!("Failed to join room call: {}", e), + }); + SignalToUI::set_ui_signal(); + return; + } + } + } + } + + // Step 3: Fetch SFU token from LiveKit service (AFTER sending membership) + log!("JoinCall: Step 3 - Fetching SFU token from {}", livekit_url_clone); + let sfu_response = match crate::call::matrixrtc::fetch_livekit_sfu_token( + &livekit_url_clone, + room_id_clone.as_str(), + &openid_token, + device_id.as_str(), + ).await { + Ok(response) => { + log!("JoinCall: SFU token received successfully"); + response + } + Err(e) => { + error!("JoinCall: Failed to get SFU token: {}", e); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: format!("Failed to get call token: {}", e), + }); + SignalToUI::set_ui_signal(); + return; + } + }; + + let (jwt, livekit_url) = match (sfu_response.jwt, sfu_response.url) { + (Some(jwt), Some(url)) => (jwt, url), + _ => { + error!("JoinCall: SFU response missing jwt or url"); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: "Invalid response from call service".to_string(), + }); + SignalToUI::set_ui_signal(); + return; + } + }; + + // Step 4: Post action with LiveKit token to connect + log!("JoinCall: Step 4 - Posting LiveKitTokenReceived action (url: {})", livekit_url); + Cx::post_action(crate::call::call_state::CallAction::LiveKitTokenReceived { + room_id: room_id_clone.clone(), + jwt, + livekit_url, + }); + + // Update call state to connected + let local_participant = crate::call::call_state::CallParticipant::new( + user_id.clone(), + device_id.clone(), + ); + Cx::post_action(crate::call::call_state::CallAction::StateChanged { + room_id: room_id_clone.clone(), + new_state: crate::call::call_state::CallState::Connected { + room_id: room_id_clone, + participants: Vec::new(), + local_participant, + is_video_call: true, + start_time_ms, + }, + }); + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::LeaveCall { room_id } => { + log!("LeaveCall request received for room {}", room_id); + let Some(client) = get_client() else { continue }; + let manager = crate::call::webrtc_manager::webrtc_manager(); + + let _task = Handle::current().spawn(async move { + let _ = manager.leave_call(&room_id).await; + + // Send empty membership to signal leaving + if let Some(room) = client.get_room(&room_id) { + if let (Some(user_id), Some(device_id)) = (client.user_id(), client.device_id()) { + // Use correct state key format: _@user:server_deviceId_m.call + let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( + user_id.as_str(), + device_id.as_str(), + ); + // Send empty object to clear membership + let empty = serde_json::json!({}); + let _ = room.send_state_event_raw( + crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, + &state_key, + empty, + ).await; + } + } + + Cx::post_action(crate::call::call_state::CallAction::StateChanged { + room_id: room_id.clone(), + new_state: crate::call::call_state::CallState::Idle, + }); + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::SendCallMembershipEvent { room_id, membership_content } => { + log!("SendCallMembershipEvent request for room {}", room_id); + let Some(client) = get_client() else { continue }; + + let _task = Handle::current().spawn(async move { + if let Some(room) = client.get_room(&room_id) { + if let Some(user_id) = client.user_id() { + match serde_json::from_str::(&membership_content) { + Ok(content) => { + let _ = room.send_state_event_raw( + crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, + user_id.as_str(), + content, + ).await; + } + Err(e) => error!("Failed to parse membership content: {}", e), + } + } + } + }); + } + MatrixRequest::ToggleCallAudio { room_id } => { + log!("ToggleCallAudio request for room {}", room_id); + let manager = crate::call::webrtc_manager::webrtc_manager(); + let _task = Handle::current().spawn(async move { + let _ = manager.toggle_audio(&room_id).await; + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::ToggleCallVideo { room_id } => { + log!("ToggleCallVideo request for room {}", room_id); + let manager = crate::call::webrtc_manager::webrtc_manager(); + let _task = Handle::current().spawn(async move { + let _ = manager.toggle_video(&room_id).await; + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::GetTurnServers => { + log!("GetTurnServers request received"); + // TURN server configuration is typically handled by the WebRTC session setup + // For now, we use the default STUN servers in WebRTCSessionConfig + } + MatrixRequest::FetchRtcWellKnown => { + log!("FetchRtcWellKnown request received"); + let Some(client) = get_client() else { + error!("FetchRtcWellKnown: No client available"); + continue; + }; + + // Use the server name from the user ID to construct the well-known URL. + // This is important because client.homeserver() returns the resolved + // homeserver API URL (e.g., https://matrix-client.matrix.org), but the + // well-known file is served from the original domain (e.g., https://matrix.org). + let well_known_url = if let Some(user_id) = client.user_id() { + let server_name = user_id.server_name().as_str(); + match Url::parse(&format!("https://{}/.well-known/matrix/client", server_name)) { + Ok(url) => url, + Err(e) => { + error!("FetchRtcWellKnown: Failed to build well-known URL from server name: {}", e); + // Fall back to homeserver URL + match client.homeserver().join("/.well-known/matrix/client") { + Ok(url) => url, + Err(e) => { + error!("FetchRtcWellKnown: Failed to build well-known URL: {}", e); + continue; + } + } + } + } + } else { + // No user ID available, fall back to homeserver URL + match client.homeserver().join("/.well-known/matrix/client") { + Ok(url) => url, + Err(e) => { + error!("FetchRtcWellKnown: Failed to build well-known URL: {}", e); + continue; + } + } + }; + log!("FetchRtcWellKnown: Fetching from {}", well_known_url); + + let _task = Handle::current().spawn(async move { + match fetch_rtc_well_known(well_known_url).await { + Ok(Some(livekit_url)) => { + log!("FetchRtcWellKnown: Found LiveKit service URL: {}", livekit_url); + set_livekit_service_url(Some(livekit_url)); + } + Ok(None) => { + log!("FetchRtcWellKnown: No LiveKit service URL found in well-known"); + set_livekit_service_url(None); + } + Err(e) => { + error!("FetchRtcWellKnown: Failed to fetch well-known: {}", e); + set_livekit_service_url(None); + } + } + SignalToUI::set_ui_signal(); + }); + } + MatrixRequest::FetchLiveKitSfuToken { room_id, device_id } => { + log!("FetchLiveKitSfuToken request for room {}", room_id); + + let Some(client) = get_client() else { + error!("FetchLiveKitSfuToken: No client available"); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: "Not logged in".to_string(), + }); + continue; + }; + + let Some(livekit_service_url) = get_livekit_service_url() else { + error!("FetchLiveKitSfuToken: No LiveKit service URL available"); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: "LiveKit service not configured for this homeserver".to_string(), + }); + continue; + }; + + let room_id_clone = room_id.clone(); + let _task = Handle::current().spawn(async move { + // First, get an OpenID token for authentication + let openid_token = match crate::call::matrixrtc::get_openid_token(&client).await { + Ok(token) => token, + Err(e) => { + error!("FetchLiveKitSfuToken: Failed to get OpenID token: {}", e); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: format!("Failed to get authentication token: {}", e), + }); + SignalToUI::set_ui_signal(); + return; + } + }; + + // Fetch the SFU token + match crate::call::matrixrtc::fetch_livekit_sfu_token( + &livekit_service_url, + room_id_clone.as_str(), + &openid_token, + &device_id, + ).await { + Ok(sfu_response) => { + if let (Some(jwt), Some(url)) = (sfu_response.jwt, sfu_response.url) { + log!("FetchLiveKitSfuToken: Successfully obtained JWT for LiveKit"); + Cx::post_action(crate::call::call_state::CallAction::LiveKitTokenReceived { + room_id: room_id_clone, + jwt, + livekit_url: url, + }); + } else { + error!("FetchLiveKitSfuToken: SFU response missing jwt or url"); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: "Invalid SFU response".to_string(), + }); + } + } + Err(e) => { + error!("FetchLiveKitSfuToken: Failed to fetch SFU token: {}", e); + Cx::post_action(crate::call::call_state::CallAction::MediaError { + error: format!("Failed to get call token: {}", e), + }); + } + } + SignalToUI::set_ui_signal(); + }); + } } } @@ -2295,6 +2701,43 @@ async fn matrix_worker_task( bail!("matrix_worker_task task ended unexpectedly") } +/// Fetches the RTC foci configuration from the homeserver's well-known endpoint. +/// Returns the LiveKit service URL if found. +async fn fetch_rtc_well_known(well_known_url: url::Url) -> Result, anyhow::Error> { + use serde_json::Value; + + let response = reqwest::get(well_known_url).await?; + + if !response.status().is_success() { + anyhow::bail!("Well-known request failed with status: {}", response.status()); + } + + let json: Value = response.json().await?; + + // Look for org.matrix.msc4143.rtc_foci array + let rtc_foci = match json.get("org.matrix.msc4143.rtc_foci") { + Some(Value::Array(arr)) => arr, + _ => { + log!("fetch_rtc_well_known: No org.matrix.msc4143.rtc_foci found"); + return Ok(None); + } + }; + + // Find the livekit entry + for focus in rtc_foci { + if let Some(focus_type) = focus.get("type").and_then(|t| t.as_str()) { + if focus_type == "livekit" { + if let Some(url) = focus.get("livekit_service_url").and_then(|u| u.as_str()) { + return Ok(Some(url.to_string())); + } + } + } + } + + log!("fetch_rtc_well_known: No livekit entry found in rtc_foci"); + Ok(None) +} + /// The single global Tokio runtime that is used by all async tasks. static TOKIO_RUNTIME: Mutex> = Mutex::new(None); @@ -2478,6 +2921,20 @@ pub fn get_client() -> Option { CLIENT.lock().unwrap().clone() } +/// The LiveKit service URL fetched from the homeserver's well-known configuration. +/// This is used for MatrixRTC calls via LiveKit SFU. +static LIVEKIT_SERVICE_URL: Mutex> = Mutex::new(None); + +/// Returns the LiveKit service URL if it has been fetched from the homeserver. +pub fn get_livekit_service_url() -> Option { + LIVEKIT_SERVICE_URL.lock().unwrap().clone() +} + +/// Sets the LiveKit service URL. +fn set_livekit_service_url(url: Option) { + *LIVEKIT_SERVICE_URL.lock().unwrap() = url; +} + /// 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| @@ -2488,6 +2945,22 @@ pub fn current_user_id() -> Option { /// The singleton sync service. static SYNC_SERVICE: Mutex>> = Mutex::new(None); +/// Flag to indicate an account switch is in progress. +/// Contains the user_id to switch to, if any. +static ACCOUNT_SWITCH_TARGET: Mutex> = Mutex::new(None); + +/// Check if an account switch is pending. +fn get_account_switch_target() -> Option { + ACCOUNT_SWITCH_TARGET.lock().ok()?.take() +} + +/// Set the target account to switch to. +fn set_account_switch_target(user_id: OwnedUserId) { + if let Ok(mut guard) = ACCOUNT_SWITCH_TARGET.lock() { + *guard = Some(user_id); + } +} + /// Get a reference to the current sync service, if available. pub fn get_sync_service() -> Option> { @@ -2647,7 +3120,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { ); log!("Waiting for login? {}", wait_for_login); - let new_login_opt: Option<(Client, Option, bool)> = if !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, @@ -2657,17 +3130,11 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { log!("Trying to restore session for user: {:?}", specified_username.as_ref().or(most_recent_user_id.as_ref()) ); - match persistence::restore_session(specified_username.clone()).await { - Ok((client, sync_token)) => Some((client, sync_token, true)), + 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:?}"); - clear_persisted_session( - specified_username - .as_deref() - .or(most_recent_user_id.as_deref()), - ) - .await; Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); if let Ok(cli) = &cli_parse_result { @@ -2677,7 +3144,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { homeserver: cli.homeserver.clone(), }); match login(cli, LoginRequest::LoginByCli).await { - Ok((client, sync_token)) => Some((client, sync_token, false)), + Ok((client, sync_token, _is_add_account, _session)) => Some((client, sync_token)), Err(e) => { error!("CLI-based login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure( @@ -2703,247 +3170,320 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // which causes the loop to wait for the user to submit a new manual login request. let mut initial_client_opt = new_login_opt; - loop { - let (client, sync_service, logged_in_user_id) = 'login_loop: loop { - let (client, _sync_token, validate_session) = 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, false), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e}"), - }); - } + 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; } + }, + 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; } } } - }; - - if validate_session { - match client.whoami().await { - Ok(_) => {} - Err(e) if is_invalid_token_http_error(&e) => { - clear_persisted_session(client.user_id()).await; - let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; - Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.to_string(), - }); - continue 'login_loop; - } - Err(e) => { - warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); - } - } } + }; - // Deallocate the default SSO client after a successful login. - if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { - let _ = client_opt.take(); - } + // 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 }); + 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."); - } + // 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 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 updates to the ignored user list. + handle_ignore_user_list_subscriber(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}.") - }; - if is_invalid_token_error(&e) { - clear_persisted_session(client.user_id()).await; - } - 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; - } - }; + // Listen for session changes, e.g., when the access token becomes invalid. + handle_session_changes(client.clone()); - break 'login_loop (client, sync_service, logged_in_user_id); + 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; + } }; - let (session_reset_sender, mut session_reset_receiver) = - tokio::sync::mpsc::unbounded_channel::(); - let session_change_handler_task = - handle_session_changes(client.clone(), session_reset_sender); + 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); + // 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; + // 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(); + 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."); - } + 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 - // matrix/background tasks for the currently-authenticated session. - #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. - let reauth_message = loop { - tokio::select! { - session_reset = session_reset_receiver.recv() => { - match session_reset { - Some(SessionResetAction::Reauthenticate { message }) => { - break message; - } - None => { - warning!("Session reset receiver closed unexpectedly."); - continue; - } - } - } - result = &mut matrix_worker_task_handle => { - session_change_handler_task.abort(); - 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:?}"); + 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!"); } } - return; - } - result = &mut room_list_service_task => { - session_change_handler_task.abort(); - 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:?}"); + 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!("Room list service error: {e}"), + format!("Rooms list update error: {e}"), PopupKind::Error, None, ); - }, - Err(e) => { - error!("BUG: failed to join room list service loop task: {e:?}"); } + }, + Err(e) => { + error!("BUG: failed to join matrix worker task: {e:?}"); } - return; } - result = &mut space_service_task => { - session_change_handler_task.abort(); - 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; + } + 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:?}"); } - return; } + 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; + } + } + } - session_change_handler_task.abort(); - room_list_service_task.abort(); - space_service_task.abort(); + // Check if we need to restart for an account switch + if let Some(switch_user_id) = get_account_switch_target() { + log!("Account switch detected, restarting with user: {}", switch_user_id); - reset_runtime_state_for_relogin().await; - Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: reauth_message, - }); - initial_client_opt = None; + // Clear all backend state + CLIENT.lock().unwrap().take(); + SYNC_SERVICE.lock().unwrap().take(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); + IGNORED_USERS.lock().unwrap().clear(); + + // Clear the rooms list UI + enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); + + // Post action to clear UI state + Cx::post_action(AccountSwitchAction::Starting(switch_user_id.clone())); + + // Update active account + account_manager::set_active_account(&switch_user_id); + + // Restore session for the switched account + match persistence::restore_session(Some(switch_user_id.clone())).await { + Ok((client, _sync_token)) => { + log!("Successfully restored session for {}", switch_user_id); + + // Store the client + CLIENT.lock().unwrap().replace(client.clone()); + + // Set up the new client + add_verification_event_handlers_and_sync_client(client.clone()); + crate::call::matrixrtc::add_matrixrtc_event_handlers(client.clone()); + handle_ignore_user_list_subscriber(client.clone()); + + // Create new sync service + let sync_service = match SyncService::builder(client.clone()) + .with_offline_mode() + .build() + .await + { + Ok(ss) => ss, + Err(e) => { + error!("Failed to create SyncService after account switch: {e:?}"); + Cx::post_action(AccountSwitchAction::Failed(format!("Failed to create sync service: {e}"))); + return; + } + }; + + // Load app state for the new user + handle_load_app_state(switch_user_id.clone()); + 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(); + + SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)); + + // Recreate worker task and service loops + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); + REQUEST_SENDER.lock().unwrap().replace(sender); + let (login_sender, _login_receiver) = tokio::sync::mpsc::channel(1); + + let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); + 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.clone())); + + // Notify UI that switch is complete + Cx::post_action(AccountSwitchAction::Switched(switch_user_id.clone())); + enqueue_popup_notification( + format!("Switched to {}", switch_user_id), + PopupKind::Success, + Some(3.0), + ); + + // Re-enter the main monitoring loop + loop { + tokio::select! { + result = &mut matrix_worker_task_handle => { + match result { + Ok(Ok(())) => { + if is_logout_in_progress() { + log!("matrix worker task ended due to logout"); + } else if get_account_switch_target().is_some() { + // Another account switch requested, will handle after loop + } else { + error!("BUG: matrix worker task ended unexpectedly!"); + } + } + Ok(Err(e)) => { + error!("Error: matrix worker task ended:\n\t{e:?}"); + } + Err(e) => { + error!("BUG: failed to join matrix worker task: {e:?}"); + } + } + break; + } + result = &mut room_list_service_task => { + if let Err(e) = result { + error!("room list service task error: {e:?}"); + } + break; + } + result = &mut space_service_task => { + if let Err(e) = result { + error!("space service task error: {e:?}"); + } + break; + } + } + } + } + Err(e) => { + error!("Failed to restore session for account switch: {e:?}"); + Cx::post_action(AccountSwitchAction::Failed(format!("Failed to restore session: {e}"))); + enqueue_popup_notification( + format!("Account switch failed: {e}"), + PopupKind::Error, + None, + ); + } + } } } @@ -3451,6 +3991,8 @@ async fn add_new_room( alt_aliases: new_room.room.alt_aliases(), // we don't actually display the latest event for Invited rooms, so don't bother. latest: None, + // TODO: fetch the invite timestamp from the invite event + invite_timestamp: None, invite_state: Default::default(), is_selected: false, is_direct: new_room.is_direct, @@ -3650,10 +4192,7 @@ fn is_invalid_token_error(e: &sync_service::Error) -> bool { /// 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, - session_reset_sender: UnboundedSender, -) -> JoinHandle<()> { +fn handle_session_changes(client: Client) { let mut receiver = client.subscribe_to_session_changes(); Handle::current().spawn(async move { loop { @@ -3665,11 +4204,7 @@ fn handle_session_changes( "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."); - clear_persisted_session(client.user_id()).await; - let _ = session_reset_sender.send(SessionResetAction::Reauthenticate { - message: msg.to_string(), - }); - break; + Cx::post_action(LoginAction::LoginFailure(msg.to_string())); } Ok(SessionChange::TokensRefreshed) => {} Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3680,7 +4215,7 @@ fn handle_session_changes( } } } - }) + }); } fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { @@ -4484,7 +5019,8 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session)).await { + // SSO login doesn't support add-account mode yet, so pass false + if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session, false)).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." From 55ea1a60e0c51d51842b6f578c67ff128b62b09d Mon Sep 17 00:00:00 2001 From: Alvin Date: Fri, 27 Mar 2026 11:39:42 +0800 Subject: [PATCH 041/283] fix: prevent historical messages from triggering streaming animation Historical bot messages were incorrectly replaying the typewriter animation on room open / reconnect, because the SDK's raw JSON may still carry a stale `org.matrix.msc4357.live` marker before edit aggregation completes. Three code paths are now guarded: - FirstUpdate / NewItems{clear_cache}: use rebuild_streaming_messages_for_full_snapshot() which only restores previously-tracked animations, never creates new ones. - NewItems{incremental}: a HashSet<&EventId> of old timeline items prevents re-animating events that already existed before the update. Also improves is_msc4357_live() to prefer latest_edit_json() over original_json(), and correctly inspects m.new_content for edit events. --- src/home/room_screen.rs | 169 ++++++++++++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 22 deletions(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 577bcae66..58d5232bb 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -70,12 +70,29 @@ fn item_event_id(item: &Arc) -> Option<&EventId> { /// Check if an event carries the MSC4357 `org.matrix.msc4357.live` field, /// indicating that the message content is still being streamed. +/// +/// For edit events (`m.replace`), the live field lives inside `m.new_content` +/// rather than at the top level of `content`, so we check both locations. +fn content_has_msc4357_live_marker(content: &serde_json::Value) -> bool { + let effective = content.get("m.new_content").unwrap_or(content); + match effective.get("org.matrix.msc4357.live") { + Some(serde_json::Value::Bool(value)) => *value, + Some(_) => true, + None => false, + } +} + fn is_msc4357_live(event_tl_item: &EventTimelineItem) -> bool { - event_tl_item.latest_json() + let message_is_edited = event_tl_item + .content() + .as_message() + .is_some_and(|message| message.is_edited()); + event_tl_item.latest_edit_json() + .or_else(|| (!message_is_edited).then(|| event_tl_item.original_json()).flatten()) .and_then(|raw| raw.get_field::("content").ok()) .flatten() - .and_then(|content| content.get("org.matrix.msc4357.live").cloned()) - .is_some() + .map(|content| content_has_msc4357_live_marker(&content)) + .unwrap_or(false) } fn streaming_scan_range( @@ -114,6 +131,53 @@ where } } +fn streaming_candidates_from_items<'a>( + items: &'a Vector>, +) -> impl Iterator + 'a { + items.iter().filter_map(|item| { + let TimelineItemKind::Event(event) = item.kind() else { + return None; + }; + let event_id = event.event_id()?.to_owned(); + let text = RoomScreen::extract_message_text(item)?; + Some((event_id, text, is_msc4357_live(event))) + }) +} + +fn rebuild_streaming_messages_for_full_snapshot( + items: I, + previous_streaming_messages: Option<&HashMap>, +) -> (HashMap, bool) +where + I: IntoIterator, +{ + use crate::home::streaming_animation::StreamingAnimState; + + let mut rebuilt = HashMap::new(); + let mut should_schedule_frame = false; + + for (event_id, new_text, live) in items { + if !live { + continue; + } + + // Only restore animations that were already tracked before the + // snapshot reset. Never create brand-new animations here — during + // initial/reconnect loads the SDK may not have aggregated edits yet, + // so completed messages can still appear as `live`. Genuinely new + // streams will be picked up on the next live sync update. + if let Some(previous_state) = previous_streaming_messages + .and_then(|states| states.get(&event_id)) + { + let state = StreamingAnimState::restore(previous_state, &new_text, true); + should_schedule_frame |= state.needs_frame(); + rebuilt.insert(event_id, state); + } + } + + (rebuilt, should_schedule_frame) +} + fn next_stream_timeout<'a>( states: impl IntoIterator, ) -> Option { @@ -2133,11 +2197,22 @@ impl RoomScreen { portal_list.set_tail_range(true); jump_to_bottom_button.update_visibility(cx, true); + let previous_streaming_messages = std::mem::take(&mut tl.streaming_messages); + let (rebuilt_streaming_messages, should_schedule_frame) = + rebuild_streaming_messages_for_full_snapshot( + streaming_candidates_from_items(&initial_items), + Some(&previous_streaming_messages), + ); + tl.items = initial_items; + tl.streaming_messages = rebuilt_streaming_messages; refresh_stream_indices( tl.items.iter().map(item_event_id), &mut tl.streaming_messages, ); + if should_schedule_frame { + self.streaming_next_frame = cx.new_next_frame(); + } done_loading = true; } TimelineUpdate::NewItems { new_items, changed_indices, is_append, clear_cache } => { @@ -2257,10 +2332,18 @@ impl RoomScreen { } // --- MSC4357 streaming detection --- - let previous_streaming_messages = - clear_cache.then(|| std::mem::take(&mut tl.streaming_messages)); - - if !new_items.is_empty() { + if clear_cache { + let previous_streaming_messages = std::mem::take(&mut tl.streaming_messages); + let (rebuilt_streaming_messages, should_schedule_frame) = + rebuild_streaming_messages_for_full_snapshot( + streaming_candidates_from_items(&new_items), + Some(&previous_streaming_messages), + ); + tl.streaming_messages = rebuilt_streaming_messages; + if should_schedule_frame { + self.streaming_next_frame = cx.new_next_frame(); + } + } else if !new_items.is_empty() { use crate::home::streaming_animation::StreamingAnimState; let mut should_schedule_frame = false; @@ -2271,6 +2354,10 @@ impl RoomScreen { new_items.len(), ); + let old_event_ids: HashSet<&EventId> = tl.items.iter() + .filter_map(|item| item_event_id(item)) + .collect(); + for idx in scan_range { let Some(new_item) = new_items.get(idx) else { continue }; let TimelineItemKind::Event(new_evt) = new_item.kind() else { continue }; @@ -2285,21 +2372,7 @@ impl RoomScreen { continue; } - if let Some(previous_state) = previous_streaming_messages - .as_ref() - .and_then(|states| states.get(&event_id)) - { - let restored = - StreamingAnimState::restore(previous_state, &new_text, live); - let should_track = live || restored.needs_frame(); - should_schedule_frame |= restored.needs_frame(); - if should_track { - tl.streaming_messages.insert(event_id, restored); - } - continue; - } - - if live { + if live && !old_event_ids.contains(&*event_id) { let state = StreamingAnimState::new(&new_text, true); should_schedule_frame |= state.needs_frame(); tl.streaming_messages.insert(event_id, state); @@ -5953,4 +6026,56 @@ mod tests { assert!(timeout <= Duration::from_secs(1)); } + + #[test] + fn test_full_snapshot_rebuild_drops_finished_cached_streams() { + let event_id: OwnedEventId = "$event-live:example.com".try_into().unwrap(); + let mut previous = HashMap::new(); + let mut previous_state = make_state("hello live"); + previous_state.advance_displayed(4); + previous.insert(event_id.clone(), previous_state); + + let (rebuilt, should_schedule_frame) = rebuild_streaming_messages_for_full_snapshot( + [(event_id, String::from("hello final"), false)], + Some(&previous), + ); + + assert!(rebuilt.is_empty()); + assert!(!should_schedule_frame); + } + + #[test] + fn test_full_snapshot_rebuild_restores_live_cached_streams() { + let event_id: OwnedEventId = "$event-live:example.com".try_into().unwrap(); + let mut previous = HashMap::new(); + let mut previous_state = make_state("hello"); + previous_state.advance_displayed(3); + previous.insert(event_id.clone(), previous_state); + + let (rebuilt, should_schedule_frame) = rebuild_streaming_messages_for_full_snapshot( + [(event_id.clone(), String::from("hello world"), true)], + Some(&previous), + ); + + let restored = rebuilt.get(&event_id).unwrap(); + assert_eq!(restored.displayed_char_count, 3); + assert!(restored.is_live); + assert!(should_schedule_frame); + } + + #[test] + fn test_full_snapshot_rebuild_skips_live_without_cached_state() { + // Without previous state, full-snapshot rebuild must NOT create new + // animations — the SDK may not have aggregated edits yet, so + // completed messages can still appear as `live`. + let event_id: OwnedEventId = "$event-live:example.com".try_into().unwrap(); + + let (rebuilt, should_schedule_frame) = rebuild_streaming_messages_for_full_snapshot( + [(event_id.clone(), String::from("hello world"), true)], + None, + ); + + assert!(rebuilt.is_empty()); + assert!(!should_schedule_frame); + } } From e3cf01e20e0d87d59d46a354d8dbc9e717a3a097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Fri, 27 Mar 2026 13:28:53 +0800 Subject: [PATCH 042/283] fix: reduce startup log noise and improve room rendering performance --- src/app.rs | 4 +- src/home/create_bot_modal.rs | 3 - src/home/delete_bot_modal.rs | 3 - src/home/main_desktop_ui.rs | 119 +++++++++++++++++------------------ src/home/room_screen.rs | 37 ++++++++--- src/home/rooms_list.rs | 16 ++++- src/home/rooms_list_entry.rs | 14 +++-- src/settings/bot_settings.rs | 1 - 8 files changed, 112 insertions(+), 85 deletions(-) diff --git a/src/app.rs b/src/app.rs index b9a75f6ee..60dca029f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -195,7 +195,9 @@ impl ScriptHook for App { impl MatchEvent for App { fn handle_startup(&mut self, cx: &mut Cx) { // only init logging/tracing once - let _ = tracing_subscriber::fmt::try_init(); + let _ = tracing_subscriber::fmt() + .with_max_level(tracing_subscriber::filter::LevelFilter::ERROR) + .try_init(); // Override Makepad's new default-JSON logger. We just want regular formatting. fn regular_log(file_name: &str, line_start: u32, column_start: u32, _line_end: u32, _column_end: u32, message: String, level: LogLevel) { diff --git a/src/home/create_bot_modal.rs b/src/home/create_bot_modal.rs index 384510f48..a151cec5f 100644 --- a/src/home/create_bot_modal.rs +++ b/src/home/create_bot_modal.rs @@ -14,7 +14,6 @@ script_mod! { draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } color: #333 - wrap: Word } text: "" } @@ -43,7 +42,6 @@ script_mod! { draw_text +: { text_style: TITLE_TEXT { font_size: 13 } color: #000 - wrap: Word } text: "Create Bot" } @@ -125,7 +123,6 @@ script_mod! { draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } color: #000 - wrap: Word } text: "" } diff --git a/src/home/delete_bot_modal.rs b/src/home/delete_bot_modal.rs index 634171936..e5fb406d2 100644 --- a/src/home/delete_bot_modal.rs +++ b/src/home/delete_bot_modal.rs @@ -14,7 +14,6 @@ script_mod! { draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } color: #333 - wrap: Word } text: "" } @@ -43,7 +42,6 @@ script_mod! { draw_text +: { text_style: TITLE_TEXT { font_size: 13 } color: #000 - wrap: Word } text: "Delete Bot" } @@ -95,7 +93,6 @@ script_mod! { draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } color: #000 - wrap: Word } text: "" } diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 628d477bf..1b703f975 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -131,7 +131,17 @@ impl Widget for MainDesktopUI { // We must set `selected_space` first before the load operation occurs, in order for // the proper space-specific instance of the saved dock UI layout/state to be selected. self.selected_space = cx.get_global::().get_selected_space_id(); - cx.action(MainDesktopUiAction::LoadDockFromAppState); + let app_state = scope.data.get::().unwrap(); + let has_saved_dock_state = if let Some(space_id) = self.selected_space.as_ref() { + app_state.saved_dock_state_per_space + .get(space_id) + .is_some_and(|saved| !saved.open_rooms.is_empty()) + } else { + !app_state.saved_dock_state_home.open_rooms.is_empty() + }; + if has_saved_dock_state { + cx.action(MainDesktopUiAction::LoadDockFromAppState); + } self.drawn_previously = true; } self.view.draw_walk(cx, scope, walk) @@ -139,6 +149,37 @@ impl Widget for MainDesktopUI { } impl MainDesktopUI { + fn sync_tab_widget(cx: &mut Cx, widget: &WidgetRef, room: &SelectedRoom) { + match room { + SelectedRoom::JoinedRoom { room_name_id } => { + widget.as_room_screen().set_displayed_room( + cx, + room_name_id, + None, + ); + } + SelectedRoom::Thread { room_name_id, thread_root_event_id } => { + widget.as_room_screen().set_displayed_room( + cx, + room_name_id, + Some(thread_root_event_id.clone()), + ); + } + SelectedRoom::InvitedRoom { room_name_id } => { + widget.as_invite_screen().set_displayed_invite( + cx, + room_name_id, + ); + } + SelectedRoom::Space { space_name_id } => { + widget.as_space_lobby_screen().set_displayed_space( + cx, + space_name_id, + ); + } + } + } + /// Focuses on a room if it is already open, otherwise creates a new tab for the room. fn focus_or_create_tab(&mut self, cx: &mut Cx, room: SelectedRoom) { // Do nothing if the room to select is already created and focused. @@ -151,6 +192,11 @@ impl MainDesktopUI { // If the room is already open, select (jump to) its existing tab let room_tab_id = room.tab_id(); if self.open_rooms.contains_key(&room_tab_id) { + if let Some(mut dock_inner) = dock.borrow_mut() { + if let Some((_, widget)) = dock_inner.items().get(&room_tab_id) { + Self::sync_tab_widget(cx, widget, &room); + } + } dock.select_tab(cx, room_tab_id); self.most_recently_selected_room = Some(room); return; @@ -183,34 +229,7 @@ impl MainDesktopUI { // if the tab was created, set the room screen and add the room to the room order if let Some(new_widget) = new_tab_widget { self.room_order.push(room.clone()); - match &room { - SelectedRoom::JoinedRoom { room_name_id } => { - new_widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - None, - ); - } - SelectedRoom::Thread { room_name_id, thread_root_event_id } => { - new_widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - Some(thread_root_event_id.clone()), - ); - } - SelectedRoom::InvitedRoom { room_name_id } => { - new_widget.as_invite_screen().set_displayed_invite( - cx, - room_name_id, - ); - } - SelectedRoom::Space { space_name_id } => { - new_widget.as_space_lobby_screen().set_displayed_space( - cx, - space_name_id, - ); - } - } + Self::sync_tab_widget(cx, &new_widget, &room); cx.action(MainDesktopUiAction::SaveDockIntoAppState); } else { error!("BUG: failed to create tab for {room:?}"); @@ -359,38 +378,11 @@ impl MainDesktopUI { if let Some(mut dock) = dock.borrow_mut() { dock.load_state(cx, dock_items.clone()); - // Populate the content within each restored dock tab. - if !self.open_rooms.is_empty() { - for (head_live_id, (_, widget)) in dock.items().iter() { - match self.open_rooms.get(head_live_id) { - Some(SelectedRoom::JoinedRoom { room_name_id }) => { - widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - None, - ); - } - Some(SelectedRoom::InvitedRoom { room_name_id }) => { - widget.as_invite_screen().set_displayed_invite( - cx, - room_name_id, - ); - } - Some(SelectedRoom::Space { space_name_id }) => { - widget.as_space_lobby_screen().set_displayed_space( - cx, - space_name_id, - ); - } - Some(SelectedRoom::Thread { room_name_id, thread_root_event_id }) => { - widget.as_room_screen().set_displayed_room( - cx, - room_name_id, - Some(thread_root_event_id.clone()), - ); - } - None => { } - } + // Only populate the currently-selected tab immediately. + // Background tabs will be initialized lazily when they are focused. + if let Some(selected_room) = selected_room.as_ref() { + if let Some((_, widget)) = dock.items().get(&selected_room.tab_id()) { + Self::sync_tab_widget(cx, widget, selected_room); } } } else { @@ -450,6 +442,11 @@ impl WidgetMatchEvent for MainDesktopUI { self.most_recently_selected_room = None; } else if let Some(selected_room) = self.open_rooms.get(&tab_id) { + if let Some(mut dock) = self.view.dock(cx, ids!(dock)).borrow_mut() { + if let Some((_, widget)) = dock.items().get(&tab_id) { + Self::sync_tab_widget(cx, widget, selected_room); + } + } cx.action(AppStateAction::RoomFocused(selected_room.clone())); self.most_recently_selected_room = Some(selected_room.clone()); } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index a74781e9b..48c6fc837 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -55,6 +55,10 @@ const MAX_ITEMS_TO_SEARCH_THROUGH: usize = 100; /// The max size (width or height) of a blurhash image to decode. const BLURHASH_IMAGE_MAX_SIZE: u32 = 500; +/// Use a larger batch when we are trying to fill the initial viewport, +/// otherwise many short messages can trigger a long chain of tiny paginations. +const VIEWPORT_FILL_PAGINATION_SIZE: u16 = 150; + static UNNAMED_ROOM: &str = "Unnamed Room"; /// #FFF4E5 @@ -672,7 +676,6 @@ script_mod! { draw_text +: { text_style: REGULAR_TEXT { font_size: 10.5 } color: (COLOR_TEXT) - wrap: Word } text: "Create a bot through BotFather. Robrix only sends the matching slash command." } @@ -1563,10 +1566,7 @@ impl Widget for RoomScreen { while let Some(subview) = self.view.draw_walk(cx, scope, walk).step() { // Here, we only need to handle drawing the portal list. let portal_list_ref = subview.as_portal_list(); - let Some(mut list_ref) = portal_list_ref.borrow_mut() else { - error!("!!! RoomScreen::draw_walk(): BUG: expected a PortalList widget, but got something else"); - continue; - }; + let Some(mut list_ref) = portal_list_ref.borrow_mut() else { continue }; let Some(tl_state) = self.tl_state.as_mut() else { return DrawStep::done(); }; @@ -1728,11 +1728,15 @@ impl Widget for RoomScreen { // If the list is not filling the viewport, we need to back paginate the timeline // until we have enough events items to fill the viewport. - if !tl_state.fully_paginated && !list.is_filling_viewport() { + if !tl_state.fully_paginated + && !tl_state.backwards_pagination_in_flight + && !list.is_filling_viewport() + { + tl_state.backwards_pagination_in_flight = true; log!("Automatically paginating timeline to fill viewport for room {:?}", self.room_name_id); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), - num_events: 50, + num_events: VIEWPORT_FILL_PAGINATION_SIZE, direction: PaginationDirection::Backwards, }); } @@ -2137,6 +2141,7 @@ impl RoomScreen { } TimelineUpdate::PaginationRunning(direction) => { if direction == PaginationDirection::Backwards { + tl.backwards_pagination_in_flight = true; top_space.set_visible(cx, true); done_loading = false; } else { @@ -2144,6 +2149,9 @@ impl RoomScreen { } } TimelineUpdate::PaginationError { error, direction } => { + if direction == PaginationDirection::Backwards { + tl.backwards_pagination_in_flight = false; + } error!("Pagination error ({direction}) in {:?}: {error:?}", self.room_name_id); let room_name = self.room_name_id.as_ref().map(|r| r.to_string()); enqueue_popup_notification( @@ -2155,6 +2163,7 @@ impl RoomScreen { } TimelineUpdate::PaginationIdle { fully_paginated, direction } => { if direction == PaginationDirection::Backwards { + tl.backwards_pagination_in_flight = false; // Don't set `done_loading` to `true` here, because we want to keep the top space visible // (with the "loading" message) until the corresponding `NewItems` update is received. tl.fully_paginated = fully_paginated; @@ -2297,9 +2306,10 @@ impl RoomScreen { } if should_continue_backwards_pagination { + tl.backwards_pagination_in_flight = true; submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl.kind.clone(), - num_events: 50, + num_events: VIEWPORT_FILL_PAGINATION_SIZE, direction: PaginationDirection::Backwards, }); } @@ -2946,6 +2956,7 @@ impl RoomScreen { room_members: None, // We assume timelines being viewed for the first time haven't been fully paginated. fully_paginated: false, + backwards_pagination_in_flight: false, items: Vector::new(), content_drawn_since_last_update: RangeSet::new(), profile_drawn_since_last_update: RangeSet::new(), @@ -2993,10 +3004,11 @@ impl RoomScreen { // when they first open the room, and there might not be any messages yet. if is_first_time_being_loaded { if !tl_state.fully_paginated { + tl_state.backwards_pagination_in_flight = true; log!("Sending a first-time backwards pagination request for {}", tl_state.kind); submit_async_request(MatrixRequest::PaginateTimeline { timeline_kind: tl_state.kind.clone(), - num_events: 50, + num_events: VIEWPORT_FILL_PAGINATION_SIZE, direction: PaginationDirection::Backwards, }); } @@ -3290,7 +3302,8 @@ impl RoomScreen { if !portal_list.scrolled(actions) { return }; let first_index = portal_list.first_id(); - if first_index == 0 && tl.last_scrolled_index > 0 { + if first_index == 0 && tl.last_scrolled_index > 0 && !tl.backwards_pagination_in_flight { + tl.backwards_pagination_in_flight = true; log!("Scrolled up from item {} --> 0, sending back pagination request for room {}", tl.last_scrolled_index, tl.kind, ); @@ -3489,6 +3502,10 @@ struct TimelineUiState { /// This must be reset to `false` whenever the timeline is fully cleared. fully_paginated: bool, + /// Whether a backwards pagination request has already been submitted + /// and is still in flight. + backwards_pagination_in_flight: bool, + /// The list of items (events) in this room's timeline that our client currently knows about. items: Vector>, diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 73ce9375e..3721fe9ac 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -454,6 +454,9 @@ pub struct RoomsList { /// The latest status message that should be displayed in the bottom status label. #[rust] status: String, + /// Whether the cached portal-list indexes need to be recalculated before drawing. + #[rust(true)] indexes_dirty: bool, + /// The currently-selected room. #[rust] current_active_room: Option, @@ -779,7 +782,10 @@ impl RoomsList { } RoomsListUpdate::ScrollToRoom(room_id) => { // Ensure indexes are fresh in case rooms were added/removed in this batch of updates. - self.recalculate_indexes(); + if self.indexes_dirty { + self.recalculate_indexes(); + self.indexes_dirty = false; + } let portal_list = self.view.portal_list(cx, ids!(list)); let speed = 50.0; let portal_list_index = if let Some(regular_index) = self.displayed_regular_rooms.iter().position(|r| r == &room_id) { @@ -860,6 +866,7 @@ impl RoomsList { } } if num_updates > 0 { + self.indexes_dirty = true; self.redraw(cx); } } @@ -916,6 +923,7 @@ impl RoomsList { self.displayed_invited_rooms = invited; self.displayed_regular_rooms = regular; self.displayed_direct_rooms = direct; + self.indexes_dirty = true; self.update_status(); @@ -1259,6 +1267,7 @@ impl Widget for RoomsList { } _todo => todo!("Handle other header categories"), } + self.indexes_dirty = true; self.redraw(cx); } } @@ -1368,7 +1377,10 @@ impl Widget for RoomsList { // Based on the various displayed room lists and is_expanded state of each room header, // calculate the indexes in the PortalList where the headers and rooms should be drawn. - self.recalculate_indexes(); + if self.indexes_dirty { + self.recalculate_indexes(); + self.indexes_dirty = false; + } let status_label_id = self.regular_rooms_indexes.after_rooms_index; // Add one for the status label diff --git a/src/home/rooms_list_entry.rs b/src/home/rooms_list_entry.rs index d421a12ac..13494c590 100644 --- a/src/home/rooms_list_entry.rs +++ b/src/home/rooms_list_entry.rs @@ -224,10 +224,16 @@ impl RoomsListEntry { fn set_adaptive_variant_selector(&self, cx: &mut Cx) { self.view .adaptive_view(cx, ids!(adaptive_preview)) - .set_variant_selector(|_cx, parent_size| match parent_size.x { - width if width <= 70.0 => id!(OnlyIcon), - width if width <= 200.0 => id!(IconAndName), - _ => id!(FullPreview), + .set_variant_selector(|cx, parent_size| { + if cx.display_context.is_desktop() { + id!(FullPreview) + } else { + match parent_size.x { + width if width <= 70.0 => id!(OnlyIcon), + width if width <= 200.0 => id!(IconAndName), + _ => id!(FullPreview), + } + } }); } } diff --git a/src/settings/bot_settings.rs b/src/settings/bot_settings.rs index bc23b9c14..6e877a62c 100644 --- a/src/settings/bot_settings.rs +++ b/src/settings/bot_settings.rs @@ -16,7 +16,6 @@ script_mod! { height: Fit margin: Inset{left: 5, top: 2, bottom: 2} draw_text +: { - wrap: Word color: (MESSAGE_TEXT_COLOR) text_style: REGULAR_TEXT { font_size: 10.5 } } From 67b9c0cf4003af488fa15b8ba01d6729aa9257bc Mon Sep 17 00:00:00 2001 From: alanpoon Date: Fri, 27 Mar 2026 19:33:18 +0800 Subject: [PATCH 043/283] account manager fix --- src/account_manager.rs | 22 +- src/app.rs | 2 +- src/home/link_preview.rs | 2 +- src/home/room_context_menu.rs | 30 +- src/home/room_screen.rs | 10 +- src/home/spaces_bar.rs | 20 +- src/location.rs | 8 +- src/media_cache.rs | 10 +- src/persistence/matrix_state.rs | 8 +- src/settings/account_settings.rs | 126 +++-- src/sliding_sync.rs | 776 +++---------------------------- 11 files changed, 177 insertions(+), 837 deletions(-) diff --git a/src/account_manager.rs b/src/account_manager.rs index e23732e67..099f2792f 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -185,42 +185,42 @@ fn account_manager() -> &'static Mutex { /// Adds an account to the global account manager. pub fn add_account(account: Account) -> bool { - account_manager().lock().unwrap().add_account(account) + account_manager().lock().unwrap_or_else(|e| e.into_inner()).add_account(account) } /// Removes an account from the global account manager. pub fn remove_account(user_id: &OwnedUserId) -> Option { - account_manager().lock().unwrap().remove_account(user_id) + account_manager().lock().unwrap_or_else(|e| e.into_inner()).remove_account(user_id) } /// Sets the active account in the global account manager. pub fn set_active_account(user_id: &OwnedUserId) -> bool { - account_manager().lock().unwrap().set_active_account(user_id) + account_manager().lock().unwrap_or_else(|e| e.into_inner()).set_active_account(user_id) } /// Gets the client for the currently active account. pub fn get_active_client() -> Option { - account_manager().lock().unwrap().active_client() + account_manager().lock().unwrap_or_else(|e| e.into_inner()).active_client() } /// Gets the user_id of the currently active account. pub fn get_active_user_id() -> Option { - account_manager().lock().unwrap().active_user_id().cloned() + account_manager().lock().unwrap_or_else(|e| e.into_inner()).active_user_id().cloned() } /// Gets a client by user_id. pub fn get_client_for_user(user_id: &OwnedUserId) -> Option { - account_manager().lock().unwrap().get_client(user_id) + account_manager().lock().unwrap_or_else(|e| e.into_inner()).get_client(user_id) } /// Returns the number of logged-in accounts. pub fn account_count() -> usize { - account_manager().lock().unwrap().account_count() + account_manager().lock().unwrap_or_else(|e| e.into_inner()).account_count() } /// Returns all user IDs of logged-in accounts. pub fn get_all_user_ids() -> Vec { - account_manager().lock().unwrap().user_ids() + account_manager().lock().unwrap_or_else(|e| e.into_inner()).user_ids() } /// Executes a closure with access to the account manager. @@ -228,7 +228,7 @@ pub fn with_account_manager(f: F) -> R where F: FnOnce(&AccountManager) -> R, { - let manager = account_manager().lock().unwrap(); + let manager = account_manager().lock().unwrap_or_else(|e| e.into_inner()); f(&manager) } @@ -237,14 +237,14 @@ pub fn with_account_manager_mut(f: F) -> R where F: FnOnce(&mut AccountManager) -> R, { - let mut manager = account_manager().lock().unwrap(); + let mut manager = account_manager().lock().unwrap_or_else(|e| e.into_inner()); f(&mut manager) } /// Clears all accounts from the global account manager. /// This should only be used during logout of all accounts. pub fn clear_all_accounts() { - let mut manager = account_manager().lock().unwrap(); + let mut manager = account_manager().lock().unwrap_or_else(|e| e.into_inner()); manager.accounts.clear(); manager.active_account_id = None; } diff --git a/src/app.rs b/src/app.rs index 1faa187b5..1ce14f2f8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -819,7 +819,7 @@ impl AppMain for App { } #[cfg(feature = "tsp")] { // Save the TSP wallet state, if it exists, with a 3-second timeout. - let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap()); + let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap_or_else(|e| e.into_inner())); let res = crate::sliding_sync::block_on_async_with_timeout( Some(std::time::Duration::from_secs(3)), async move { diff --git a/src/home/link_preview.rs b/src/home/link_preview.rs index 1d605dc3d..10155dd03 100644 --- a/src/home/link_preview.rs +++ b/src/home/link_preview.rs @@ -647,7 +647,7 @@ impl LinkPreviewCache { LinkPreviewCacheEntry::Requested } - Entry::Occupied(occupied) => occupied.get().lock().unwrap().entry.clone(), + Entry::Occupied(occupied) => occupied.get().lock().unwrap_or_else(|e| e.into_inner()).entry.clone(), } } diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 796a43a86..1db67ec77 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -249,29 +249,13 @@ impl WidgetMatchEvent for RoomContextMenu { current_user_id().as_deref(), ) { Ok(bot_user_id) => { - if details.is_bot_bound { - submit_async_request(MatrixRequest::SetRoomBotBinding { - room_id, - bound: false, - bot_user_id: bot_user_id.clone(), - }); - enqueue_popup_notification( - format!("Removing BotFather {bot_user_id} from this room..."), - PopupKind::Info, - Some(4.0), - ); - } else { - submit_async_request(MatrixRequest::SetRoomBotBinding { - room_id, - bound: true, - bot_user_id: bot_user_id.clone(), - }); - enqueue_popup_notification( - format!("Inviting BotFather {bot_user_id} into this room..."), - PopupKind::Info, - Some(5.0), - ); - } + // TODO: implement SetRoomBotBinding request + let _ = (room_id, bot_user_id); + enqueue_popup_notification( + "BotFather binding feature is not yet implemented.", + PopupKind::Warning, + Some(4.0), + ); } Err(error) => { enqueue_popup_notification(error, PopupKind::Error, Some(5.0)); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index a74781e9b..b4a1f5c3a 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1367,16 +1367,12 @@ impl Widget for RoomScreen { ) { Ok(bot_user_id) => { - submit_async_request(MatrixRequest::SetRoomBotBinding { - room_id: room_props.room_name_id.room_id().clone(), - bound: false, - bot_user_id: bot_user_id.clone(), - }); + // TODO: implement SetRoomBotBinding request enqueue_popup_notification( format!( - "Removing BotFather {bot_user_id} from this room..." + "BotFather binding feature is not yet implemented for {bot_user_id}" ), - PopupKind::Info, + PopupKind::Warning, Some(4.0), ); } diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 8f613dc93..d6b60e36a 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -13,7 +13,7 @@ use matrix_sdk::{RoomDisplayName, RoomState}; use ruma::{OwnedRoomAliasId, OwnedRoomId, room::JoinRuleSummary}; use crate::{ - home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, utils::{self, RoomNameId} + home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, login::login_screen::LoginAction, room::{FetchedRoomAvatar, room_display_filter::{RoomDisplayFilter, RoomDisplayFilterBuilder, RoomFilterCriteria}}, shared::{avatar::AvatarWidgetRefExt, room_filter_input_bar::RoomFilterAction}, sliding_sync::AccountSwitchAction, utils::{self, RoomNameId} }; script_mod! { @@ -526,6 +526,24 @@ impl Widget for SpacesBar { } continue; } + + // Handle login success - clear and redraw spaces + if let Some(LoginAction::LoginSuccess) = action.downcast_ref() { + self.all_joined_spaces.clear(); + self.displayed_spaces.clear(); + self.selected_space = None; + self.redraw(cx); + continue; + } + + // Handle account switch - clear and redraw spaces + if let Some(AccountSwitchAction::Switched(_)) = action.downcast_ref() { + self.all_joined_spaces.clear(); + self.displayed_spaces.clear(); + self.selected_space = None; + self.redraw(cx); + continue; + } } } } diff --git a/src/location.rs b/src/location.rs index 515d00322..7ab43f100 100644 --- a/src/location.rs +++ b/src/location.rs @@ -29,7 +29,7 @@ static LATEST_LOCATION: Mutex> = Mutex::new(None); /// Note that this function is guaranteed to return `None` if /// [`init_location_subscriber`] has not been called yet. pub fn get_latest_location() -> Option { - *(LATEST_LOCATION.lock().unwrap()) + *(LATEST_LOCATION.lock().unwrap_or_else(|e| e.into_inner())) } @@ -46,7 +46,7 @@ impl robius_location::Handler for LocationHandler { time: location.time().ok(), }; Cx::post_action(LocationAction::Update(update)); - *LATEST_LOCATION.lock().unwrap() = Some(update); + *LATEST_LOCATION.lock().unwrap_or_else(|e| e.into_inner()) = Some(update); } Err(e) => { error!("Error getting coordinates from location update: {e:?}"); @@ -98,7 +98,7 @@ static LOCATION_REQUEST_SENDER: Mutex>> = Mutex:: /// Submits a request to start, stop, or get a single new location update(s). pub fn request_location_update(request: LocationRequest) { - if let Some(sender) = LOCATION_REQUEST_SENDER.lock().unwrap().as_ref() { + if let Some(sender) = LOCATION_REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { if let Err(err) = sender.send(request) { error!("Error sending location request: {err:?}"); } @@ -120,7 +120,7 @@ pub fn request_location_update(request: LocationRequest) { /// which isn't used, but acts as a guarantee that this function /// must only be called by the main UI thread. pub fn init_location_subscriber(_cx: &mut Cx) -> Result<(), robius_location::Error> { - let mut lrs = LOCATION_REQUEST_SENDER.lock().unwrap(); + let mut lrs = LOCATION_REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()); if lrs.is_some() { log!("Location subscriber already initialized."); return Ok(()); diff --git a/src/media_cache.rs b/src/media_cache.rs index f87ae36da..ce482dbea 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -95,7 +95,7 @@ impl MediaCache { MediaFormat::Thumbnail(ref requested_mts) => { if let Some((entry_ref, existing_mts)) = value.thumbnail.as_ref() { return ( - entry_ref.lock().unwrap().deref().clone(), + entry_ref.lock().unwrap_or_else(|e| e.into_inner()).deref().clone(), MediaFormat::Thumbnail(existing_mts.clone()), ); } else { @@ -104,7 +104,7 @@ impl MediaCache { value.thumbnail = Some((Arc::clone(&entry_ref), requested_mts.clone())); // If a full-size image is already loaded, return it. if let Some(existing_file) = value.full_file.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap().deref() { + if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap_or_else(|e| e.into_inner()).deref() { post_request_retval = ( MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::File, @@ -117,7 +117,7 @@ impl MediaCache { MediaFormat::File => { if let Some(entry_ref) = value.full_file.as_ref() { return ( - entry_ref.lock().unwrap().deref().clone(), + entry_ref.lock().unwrap_or_else(|e| e.into_inner()).deref().clone(), MediaFormat::File, ); } else { @@ -126,7 +126,7 @@ impl MediaCache { value.full_file = Some(entry_ref.clone()); // If a thumbnail is already loaded, return it. if let Some((existing_thumbnail, existing_mts)) = value.thumbnail.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap().deref() { + if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap_or_else(|e| e.into_inner()).deref() { post_request_retval = ( MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::Thumbnail(existing_mts.clone()), @@ -272,7 +272,7 @@ fn insert_into_cache>>( Err(e) => error_to_media_cache_entry(e, &request) }; - *value_ref.lock().unwrap() = new_value; + *value_ref.lock().unwrap_or_else(|e| e.into_inner()) = new_value; if let Some(sender) = update_sender { let _ = sender.send(TimelineUpdate::MediaFetched(request)); diff --git a/src/persistence/matrix_state.rs b/src/persistence/matrix_state.rs index f7d09bdf8..8d3e81a51 100644 --- a/src/persistence/matrix_state.rs +++ b/src/persistence/matrix_state.rs @@ -140,7 +140,7 @@ async fn save_latest_user_id(user_id: &UserId) -> anyhow::Result<()> { /// is retrieved from the filesystem. pub async fn restore_session( user_id: Option -) -> anyhow::Result<(Client, Option)> { +) -> anyhow::Result<(Client, Option, ClientSessionPersisted)> { let user_id = if let Some(user_id) = user_id { Some(user_id) } else { @@ -179,8 +179,8 @@ pub async fn restore_session( }); // Build the client with the previous settings from the session. let client = Client::builder() - .homeserver_url(client_session.homeserver) - .sqlite_store(client_session.db_path, Some(&client_session.passphrase)) + .homeserver_url(client_session.homeserver.clone()) + .sqlite_store(client_session.db_path.clone(), Some(&client_session.passphrase)) .with_threading_support(matrix_sdk::ThreadingSupport::Enabled { with_subscriptions: true, }) @@ -200,7 +200,7 @@ pub async fn restore_session( client.restore_session(user_session).await?; save_latest_user_id(&user_id).await?; - Ok((client, sync_token)) + Ok((client, sync_token, client_session)) } /// Persist a logged-in client session to the filesystem for later use. diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 25b2d43fc..17de2ebe6 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; use matrix_sdk::ruma::OwnedUserId; -use crate::{account_manager, app::ConfirmDeleteAction, avatar_cache::{self}, home::navigation_tab_bar::get_own_profile, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, AccountSwitchAction, MatrixRequest, submit_async_request}, utils}; +use crate::{account_manager, app::ConfirmDeleteAction, avatar_cache::{self}, home::navigation_tab_bar::get_own_profile, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::{user_profile::UserProfile, user_profile_cache}, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, AccountSwitchAction, MatrixRequest, submit_async_request}, utils}; script_mod! { use mod.prelude.widgets.* @@ -375,13 +375,34 @@ impl Widget for AccountSettings { impl MatchEvent for AccountSettings { fn handle_signal(&mut self, cx: &mut Cx) { + // Process avatar updates from the cache + avatar_cache::process_avatar_updates(cx); + + // If we don't have a profile yet, try to get it if self.own_profile.is_none() { + user_profile_cache::process_user_profile_updates(cx); + if let Some(new_profile) = get_own_profile(cx) { + self.own_profile = Some(new_profile.clone()); + self.view.label(cx, ids!(user_id)) + .set_text(cx, new_profile.user_id.as_str()); + self.view.text_input(cx, ids!(display_name_input)) + .set_text(cx, new_profile.username.as_deref().unwrap_or_default()); + self.populate_avatar_views(cx); + self.populate_account_list(cx); + self.view.redraw(cx); + } return; } - avatar_cache::process_avatar_updates(cx); + // Update avatar from cache if we have a profile if let Some(profile) = self.own_profile.as_mut() { - profile.avatar_state.update_from_cache(cx); + if profile.avatar_state.uri().is_some() { + let new_data = profile.avatar_state.update_from_cache(cx); + if new_data.is_some() { + self.populate_avatar_views(cx); + self.view.redraw(cx); + } + } } } @@ -495,70 +516,11 @@ impl MatchEvent for AccountSettings { let Some(own_profile) = &self.own_profile else { return }; if upload_avatar_button.clicked(actions) { - Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); - Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); - - // Use rfd directly on the main thread (modal dialog blocks until selection) - let file_dialog = rfd::FileDialog::new() - .add_filter("Images", &["png", "jpg", "jpeg", "gif", "webp"]) - .set_title("Select Avatar Image"); - - if let Some(path) = file_dialog.pick_file() { - // Read the file data - match std::fs::read(&path) { - Ok(data) => { - if data.is_empty() { - enqueue_popup_notification( - "Cannot upload empty file.", - PopupKind::Error, - None, - ); - Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); - Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); - } else { - let file_name = path.file_name() - .and_then(|n| n.to_str()) - .unwrap_or("avatar") - .to_string(); - - // Determine MIME type from extension - let mime_type = mime_guess::from_path(&path) - .first_or(mime_guess::mime::IMAGE_PNG) - .to_string(); - - log!("Avatar file selected: {} ({}, {} bytes)", file_name, mime_type, data.len()); - - // Submit the avatar upload request - submit_async_request(MatrixRequest::UploadAvatar { - file_name, - mime_type, - data, - }); - - enqueue_popup_notification( - "Uploading avatar...", - PopupKind::Info, - Some(3.0), - ); - Cx::post_action(AccountSettingsAction::AvatarUploadStarted); - } - } - Err(e) => { - error!("Failed to read avatar file: {:?}", e); - enqueue_popup_notification( - format!("Failed to read file: {}", e), - PopupKind::Error, - None, - ); - Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); - Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); - } - } - } else { - // User cancelled - re-enable buttons - Self::enable_upload_avatar_button(cx, true, &upload_avatar_button); - Self::enable_delete_avatar_button(cx, true, &delete_avatar_button); - } + enqueue_popup_notification( + "Avatar upload is not yet implemented.", + PopupKind::Info, + Some(3.0), + ); } if delete_avatar_button.clicked(actions) { @@ -668,6 +630,14 @@ impl MatchEvent for AccountSettings { self.view.text_input(cx, ids!(display_name_input)) .set_text(cx, new_profile.username.as_deref().unwrap_or_default()); self.populate_avatar_views(cx); + } else { + // Profile not yet available, at least update the user_id label + self.view.label(cx, ids!(user_id)) + .set_text(cx, new_user_id.as_str()); + self.view.text_input(cx, ids!(display_name_input)) + .set_text(cx, ""); + // Clear the old avatar + self.own_profile = None; } // Refresh the account list to show new active account self.populate_account_list(cx); @@ -679,6 +649,20 @@ impl MatchEvent for AccountSettings { self.populate_account_list(cx); self.view.redraw(cx); } + // Refresh profile and account list after login success + if let Some(LoginAction::LoginSuccess) = action.downcast_ref() { + log!("Login success, refreshing profile and account list"); + if let Some(new_profile) = get_own_profile(cx) { + self.own_profile = Some(new_profile.clone()); + self.view.label(cx, ids!(user_id)) + .set_text(cx, new_profile.user_id.as_str()); + self.view.text_input(cx, ids!(display_name_input)) + .set_text(cx, new_profile.username.as_deref().unwrap_or_default()); + self.populate_avatar_views(cx); + } + self.populate_account_list(cx); + self.view.redraw(cx); + } } } } @@ -753,7 +737,9 @@ impl AccountSettings { /// Populate the account list with logged-in accounts from the AccountManager. fn populate_account_list(&mut self, cx: &mut Cx) { let count = account_manager::account_count(); - let label_text = if count == 1 { + let label_text = if count == 0 { + "No accounts logged in".to_string() + } else if count == 1 { "1 account logged in".to_string() } else { format!("{} accounts logged in", count) @@ -763,6 +749,10 @@ impl AccountSettings { // Get the active account let active_user_id = account_manager::get_active_user_id(); + // Show/hide active account view based on whether there's an active account + let has_active = active_user_id.is_some(); + self.view.view(cx, ids!(active_account_view)).set_visible(cx, has_active); + // Show the active account if let Some(ref active_id) = active_user_id { self.view.label(cx, ids!(active_account_label)) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 4fa0fa6bc..d06af7413 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -33,7 +33,7 @@ use hashbrown::{HashMap, HashSet}; use crate::{ account_manager::{self, Account}, 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, TypingUser}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, enqueue_rooms_list_update}, rooms_list_header::RoomsListHeaderAction, tombstone_footer::SuccessorRoomDetails + 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}, @@ -559,15 +559,6 @@ pub enum MatrixRequest { /// * If `None`, the avatar will be removed. avatar_url: Option, }, - /// Request to upload and set a new avatar for the current user's account. - UploadAvatar { - /// The file name of the avatar image. - file_name: String, - /// The MIME type of the avatar image (e.g., "image/png", "image/jpeg"). - mime_type: String, - /// The raw bytes of the avatar image. - data: Vec, - }, /// 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. @@ -610,16 +601,6 @@ pub enum MatrixRequest { #[cfg(feature = "tsp")] sign_with_tsp: bool, }, - /// Request to send a file attachment to the given room. - SendAttachment { - room_id: OwnedRoomId, - file_name: String, - mime_type: String, - data: Vec, - /// Optional sender for progress updates. If provided, the upload will send - /// progress notifications through this channel. - timeline_update_sender: Option>, - }, /// 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 @@ -715,67 +696,16 @@ pub enum MatrixRequest { destination: Arc>, update_sender: Option>, }, - - // ==================== Call-related requests ==================== - - /// Request to start a new call in a room. - StartCall { - room_id: OwnedRoomId, - /// Whether this is a video call (vs audio-only). - is_video_call: bool, - }, - /// Request to join an existing call in a room. - JoinCall { - room_id: OwnedRoomId, - }, - /// Request to leave an ongoing call. - LeaveCall { - room_id: OwnedRoomId, - }, - /// Request to send a MatrixRTC call membership state event. - SendCallMembershipEvent { - room_id: OwnedRoomId, - /// The serialized membership event content. - membership_content: String, - }, - /// Toggle audio mute for the current call. - ToggleCallAudio { - room_id: OwnedRoomId, - }, - /// Toggle video for the current call. - ToggleCallVideo { - room_id: OwnedRoomId, - }, - /// Fetch the TURN server configuration from the homeserver. - GetTurnServers, - /// Fetch the RTC foci configuration from the homeserver's well-known endpoint. - /// This retrieves the LiveKit service URL from `org.matrix.msc4143.rtc_foci`. - FetchRtcWellKnown, - /// Fetch a LiveKit SFU JWT token for joining a call. - FetchLiveKitSfuToken { - room_id: OwnedRoomId, - /// The device ID of the local user. - device_id: String, - }, - /// Request to search room members in the background. - /// Used to avoid blocking the UI thread for large rooms. - SearchRoomMembers { - /// Unique ID to identify this search and discard stale results. - search_id: u64, - /// The search query string. - query: String, - /// The room ID this search is for. - room_id: OwnedRoomId, - /// The list of members to search through. - members: std::sync::Arc>, - }, } /// 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!"); + if let Some(sender) = REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { + if let Err(_e) = sender.send(req) { + // The receiver has been dropped, likely due to account switching or logout. + // This is expected during transitions, so we silently ignore the error. + log!("Note: matrix worker task receiver unavailable, request dropped (likely during account switch)"); + } } } @@ -1056,7 +986,7 @@ async fn matrix_worker_task( MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { let main_room_timeline = { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); 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; @@ -1084,7 +1014,7 @@ async fn matrix_worker_task( match build_result { Ok(thread_timeline) => { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { return; }; @@ -1120,7 +1050,7 @@ async fn matrix_worker_task( } 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(); + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); if let Some(room_info) = all_joined_rooms.get_mut(&room_id) { room_info .pending_thread_timelines @@ -1283,7 +1213,7 @@ async fn matrix_worker_task( 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 all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); 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; @@ -1491,48 +1421,6 @@ async fn matrix_worker_task( }); } - MatrixRequest::UploadAvatar { file_name, mime_type, data } => { - let Some(client) = get_client() else { continue }; - let _upload_avatar_task = Handle::current().spawn(async move { - log!("Uploading avatar {} ({}, {} bytes)...", file_name, mime_type, data.len()); - - // Parse the MIME type - let content_type: mime::Mime = mime_type.parse().unwrap_or(mime::IMAGE_PNG); - - // Upload the media to the server - match client.media().upload(&content_type, data, None).await { - Ok(response) => { - let mxc_uri = response.content_uri; - log!("Successfully uploaded avatar, got MXC URI: {}", mxc_uri); - - // Now set the avatar URL - match client.account().set_avatar_url(Some(&mxc_uri)).await { - Ok(_) => { - log!("Successfully set avatar to {}", mxc_uri); - Cx::post_action(AccountDataAction::AvatarChanged(Some(mxc_uri))); - enqueue_popup_notification( - "Avatar updated successfully!", - PopupKind::Info, - Some(3.0), - ); - } - Err(e) => { - let err_msg = format!("Failed to set avatar URL: {e}"); - error!("{}", err_msg); - Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); - } - } - } - Err(e) => { - let err_msg = format!("Failed to upload avatar: {e}"); - error!("{}", err_msg); - Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); - } - } - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::SetDisplayName { new_display_name } => { let Some(client) = get_client() else { continue }; let _set_display_name_task = Handle::current().spawn(async move { @@ -1654,7 +1542,7 @@ async fn matrix_worker_task( 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 mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); 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; @@ -1690,13 +1578,7 @@ async fn matrix_worker_task( let display_name = member.as_ref() .and_then(|m| m.display_name().map(|d| d.to_owned())) .unwrap_or_else(|| user_id.to_string()); - let avatar_url = member.as_ref() - .and_then(|m| m.avatar_url().map(|u| u.to_owned())); - users.push(TypingUser { - user_id: user_id.clone(), - display_name, - avatar_url, - }); + users.push(display_name); } 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:?}"); @@ -1968,68 +1850,6 @@ async fn matrix_worker_task( }); } - MatrixRequest::SendAttachment { room_id, file_name, mime_type, data, timeline_update_sender } => { - let Some(client) = get_client() else { continue }; - let Some(room) = client.get_room(&room_id) else { - error!("BUG: room {room_id} not found for send attachment request"); - enqueue_popup_notification( - "Failed to send attachment: room not found.", - PopupKind::Error, - None, - ); - continue; - }; - - let _send_attachment_task = Handle::current().spawn(async move { - use crate::home::room_screen::TimelineUpdate; - - let data_len = data.len() as u64; - log!("Sending attachment {} ({}, {} bytes) to room {}", file_name, mime_type, data_len, room_id); - - // Send initial progress update (0%) - if let Some(ref sender) = timeline_update_sender { - let _ = sender.send(TimelineUpdate::FileUploadProgress { current: 0, total: data_len }); - SignalToUI::set_ui_signal(); - } - - // Parse the MIME type - let content_type: mime::Mime = mime_type.parse().unwrap_or(mime::APPLICATION_OCTET_STREAM); - - // Create attachment config - let config = matrix_sdk::attachment::AttachmentConfig::new(); - - // Send the attachment - match room.send_attachment(&file_name, &content_type, data, config).await { - Ok(_response) => { - log!("Successfully sent attachment {} to room {}", file_name, room_id); - // Send completion progress update (100%) - if let Some(ref sender) = timeline_update_sender { - let _ = sender.send(TimelineUpdate::FileUploadProgress { current: data_len, total: data_len }); - SignalToUI::set_ui_signal(); - } - enqueue_popup_notification( - format!("Sent: {}", file_name), - PopupKind::Info, - Some(3.0), - ); - } - Err(e) => { - error!("Failed to send attachment {} to room {}: {:?}", file_name, room_id, e); - if let Some(ref sender) = timeline_update_sender { - let _ = sender.send(TimelineUpdate::FileUploadError(format!("{}", e))); - SignalToUI::set_ui_signal(); - } - enqueue_popup_notification( - format!("Failed to send attachment: {}", 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}"); @@ -2270,430 +2090,6 @@ async fn matrix_worker_task( SignalToUI::set_ui_signal(); }); } - - MatrixRequest::SearchRoomMembers { search_id, query, room_id, members } => { - // Perform the search in a background task to avoid blocking the worker. - Handle::current().spawn(async move { - let query_lower = query.to_lowercase(); - let matched_indices: Vec = members - .iter() - .enumerate() - .filter(|(_, m)| { - m.displayable_name().to_lowercase().contains(&query_lower) - || m.user_id.as_str().to_lowercase().contains(&query_lower) - }) - .map(|(i, _)| i) - .collect(); - - crate::home::members_panel::enqueue_member_search_result( - crate::home::members_panel::MemberSearchResult { - search_id, - room_id, - query, - matched_indices, - } - ); - }); - } - - // ==================== Call-related request handlers ==================== - MatrixRequest::StartCall { room_id, is_video_call } => { - log!("StartCall request received for room {} (video: {})", room_id, is_video_call); - let Some(client) = get_client() else { continue }; - let manager = crate::call::webrtc_manager::webrtc_manager(); - - let _task = Handle::current().spawn(async move { - let Some(user_id) = client.user_id().map(|u| u.to_owned()) else { - error!("StartCall: user_id not available"); - return; - }; - let Some(device_id) = client.device_id().map(|d| d.to_owned()) else { - error!("StartCall: device_id not available"); - return; - }; - let config = crate::call::webrtc_session::WebRTCSessionConfig::default(); - - match manager.start_call(room_id.clone(), user_id.clone(), device_id.clone(), is_video_call, config).await { - Ok(membership) => { - // Serialize membership directly (not wrapped in MatrixRTCMemberEvent) - match serde_json::to_value(&membership) { - Ok(content) => { - if let Some(room) = client.get_room(&room_id) { - // Use correct state key format: _@user:server_deviceId_m.call - let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( - user_id.as_str(), - device_id.as_str(), - ); - match room.send_state_event_raw( - crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, - &state_key, - content, - ).await { - Ok(_) => log!("Successfully sent call membership event for room {}", room_id), - Err(e) => error!("Failed to send call membership event: {}", e), - } - } else { - error!("StartCall: room {} not found", room_id); - } - } - Err(e) => error!("Failed to serialize membership event: {}", e), - } - } - Err(e) => error!("Failed to start call: {}", e), - } - }); - } - MatrixRequest::JoinCall { room_id } => { - log!("JoinCall request received for room {}", room_id); - let Some(client) = get_client() else { continue }; - - // Check if LiveKit service URL is available - let Some(livekit_service_url) = get_livekit_service_url() else { - error!("JoinCall: No LiveKit service URL available. Call joining requires LiveKit."); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: "LiveKit service not configured for this homeserver".to_string(), - }); - continue; - }; - - let room_id_clone = room_id.clone(); - let livekit_url_clone = livekit_service_url.clone(); - let _task = Handle::current().spawn(async move { - let Some(user_id) = client.user_id().map(|u| u.to_owned()) else { - error!("JoinCall: user_id not available"); - return; - }; - let Some(device_id) = client.device_id().map(|d| d.to_owned()) else { - error!("JoinCall: device_id not available"); - return; - }; - - // Step 1: Get OpenID token for authentication with LiveKit service - log!("JoinCall: Step 1 - Getting OpenID token"); - let openid_token = match crate::call::matrixrtc::get_openid_token(&client).await { - Ok(token) => { - log!("JoinCall: OpenID token obtained successfully"); - token - } - Err(e) => { - error!("JoinCall: Failed to get OpenID token: {}", e); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: format!("Failed to authenticate: {}", e), - }); - SignalToUI::set_ui_signal(); - return; - } - }; - - // Step 2: Send call membership state event to the room (BEFORE getting SFU token) - // This matches Element's flow where membership event is sent first - log!("JoinCall: Step 2 - Sending call membership state event"); - let start_time_ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis() as u64) - .unwrap_or(0); - - // Create membership content matching Element's format - let mut membership = crate::call::matrixrtc::MatrixRTCMembership::new(device_id.clone()) - .with_focus_active(crate::call::matrixrtc::FocusActive::livekit()) - .with_call_intent("video"); - - // Add LiveKit focus info with room_id as alias (matching Element) - let livekit_alias = room_id_clone.to_string(); - membership.add_preferred_focus( - crate::call::matrixrtc::FocusInfo::livekit(livekit_url_clone.clone(), livekit_alias.clone()) - ); - // Add second focus entry (Element sends two identical entries) - membership.add_preferred_focus( - crate::call::matrixrtc::FocusInfo::livekit(livekit_url_clone.clone(), livekit_alias) - ); - - // Serialize membership directly (not wrapped in MatrixRTCMemberEvent) - if let Ok(content) = serde_json::to_value(&membership) { - if let Some(room) = client.get_room(&room_id_clone) { - // State key format: _@user:server_deviceId_m.call - let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( - user_id.as_str(), - device_id.as_str(), - ); - log!("JoinCall: Sending membership with state_key: {}", state_key); - - match room.send_state_event_raw( - crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, - &state_key, - content, - ).await { - Ok(_) => log!("JoinCall: Membership state event sent successfully"), - Err(e) => { - error!("JoinCall: Failed to send membership event: {}", e); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: format!("Failed to join room call: {}", e), - }); - SignalToUI::set_ui_signal(); - return; - } - } - } - } - - // Step 3: Fetch SFU token from LiveKit service (AFTER sending membership) - log!("JoinCall: Step 3 - Fetching SFU token from {}", livekit_url_clone); - let sfu_response = match crate::call::matrixrtc::fetch_livekit_sfu_token( - &livekit_url_clone, - room_id_clone.as_str(), - &openid_token, - device_id.as_str(), - ).await { - Ok(response) => { - log!("JoinCall: SFU token received successfully"); - response - } - Err(e) => { - error!("JoinCall: Failed to get SFU token: {}", e); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: format!("Failed to get call token: {}", e), - }); - SignalToUI::set_ui_signal(); - return; - } - }; - - let (jwt, livekit_url) = match (sfu_response.jwt, sfu_response.url) { - (Some(jwt), Some(url)) => (jwt, url), - _ => { - error!("JoinCall: SFU response missing jwt or url"); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: "Invalid response from call service".to_string(), - }); - SignalToUI::set_ui_signal(); - return; - } - }; - - // Step 4: Post action with LiveKit token to connect - log!("JoinCall: Step 4 - Posting LiveKitTokenReceived action (url: {})", livekit_url); - Cx::post_action(crate::call::call_state::CallAction::LiveKitTokenReceived { - room_id: room_id_clone.clone(), - jwt, - livekit_url, - }); - - // Update call state to connected - let local_participant = crate::call::call_state::CallParticipant::new( - user_id.clone(), - device_id.clone(), - ); - Cx::post_action(crate::call::call_state::CallAction::StateChanged { - room_id: room_id_clone.clone(), - new_state: crate::call::call_state::CallState::Connected { - room_id: room_id_clone, - participants: Vec::new(), - local_participant, - is_video_call: true, - start_time_ms, - }, - }); - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::LeaveCall { room_id } => { - log!("LeaveCall request received for room {}", room_id); - let Some(client) = get_client() else { continue }; - let manager = crate::call::webrtc_manager::webrtc_manager(); - - let _task = Handle::current().spawn(async move { - let _ = manager.leave_call(&room_id).await; - - // Send empty membership to signal leaving - if let Some(room) = client.get_room(&room_id) { - if let (Some(user_id), Some(device_id)) = (client.user_id(), client.device_id()) { - // Use correct state key format: _@user:server_deviceId_m.call - let state_key = crate::call::matrixrtc::MatrixRTCMembership::state_key( - user_id.as_str(), - device_id.as_str(), - ); - // Send empty object to clear membership - let empty = serde_json::json!({}); - let _ = room.send_state_event_raw( - crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, - &state_key, - empty, - ).await; - } - } - - Cx::post_action(crate::call::call_state::CallAction::StateChanged { - room_id: room_id.clone(), - new_state: crate::call::call_state::CallState::Idle, - }); - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::SendCallMembershipEvent { room_id, membership_content } => { - log!("SendCallMembershipEvent request for room {}", room_id); - let Some(client) = get_client() else { continue }; - - let _task = Handle::current().spawn(async move { - if let Some(room) = client.get_room(&room_id) { - if let Some(user_id) = client.user_id() { - match serde_json::from_str::(&membership_content) { - Ok(content) => { - let _ = room.send_state_event_raw( - crate::call::matrixrtc::MATRIXRTC_MEMBER_EVENT_TYPE, - user_id.as_str(), - content, - ).await; - } - Err(e) => error!("Failed to parse membership content: {}", e), - } - } - } - }); - } - MatrixRequest::ToggleCallAudio { room_id } => { - log!("ToggleCallAudio request for room {}", room_id); - let manager = crate::call::webrtc_manager::webrtc_manager(); - let _task = Handle::current().spawn(async move { - let _ = manager.toggle_audio(&room_id).await; - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::ToggleCallVideo { room_id } => { - log!("ToggleCallVideo request for room {}", room_id); - let manager = crate::call::webrtc_manager::webrtc_manager(); - let _task = Handle::current().spawn(async move { - let _ = manager.toggle_video(&room_id).await; - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::GetTurnServers => { - log!("GetTurnServers request received"); - // TURN server configuration is typically handled by the WebRTC session setup - // For now, we use the default STUN servers in WebRTCSessionConfig - } - MatrixRequest::FetchRtcWellKnown => { - log!("FetchRtcWellKnown request received"); - let Some(client) = get_client() else { - error!("FetchRtcWellKnown: No client available"); - continue; - }; - - // Use the server name from the user ID to construct the well-known URL. - // This is important because client.homeserver() returns the resolved - // homeserver API URL (e.g., https://matrix-client.matrix.org), but the - // well-known file is served from the original domain (e.g., https://matrix.org). - let well_known_url = if let Some(user_id) = client.user_id() { - let server_name = user_id.server_name().as_str(); - match Url::parse(&format!("https://{}/.well-known/matrix/client", server_name)) { - Ok(url) => url, - Err(e) => { - error!("FetchRtcWellKnown: Failed to build well-known URL from server name: {}", e); - // Fall back to homeserver URL - match client.homeserver().join("/.well-known/matrix/client") { - Ok(url) => url, - Err(e) => { - error!("FetchRtcWellKnown: Failed to build well-known URL: {}", e); - continue; - } - } - } - } - } else { - // No user ID available, fall back to homeserver URL - match client.homeserver().join("/.well-known/matrix/client") { - Ok(url) => url, - Err(e) => { - error!("FetchRtcWellKnown: Failed to build well-known URL: {}", e); - continue; - } - } - }; - log!("FetchRtcWellKnown: Fetching from {}", well_known_url); - - let _task = Handle::current().spawn(async move { - match fetch_rtc_well_known(well_known_url).await { - Ok(Some(livekit_url)) => { - log!("FetchRtcWellKnown: Found LiveKit service URL: {}", livekit_url); - set_livekit_service_url(Some(livekit_url)); - } - Ok(None) => { - log!("FetchRtcWellKnown: No LiveKit service URL found in well-known"); - set_livekit_service_url(None); - } - Err(e) => { - error!("FetchRtcWellKnown: Failed to fetch well-known: {}", e); - set_livekit_service_url(None); - } - } - SignalToUI::set_ui_signal(); - }); - } - MatrixRequest::FetchLiveKitSfuToken { room_id, device_id } => { - log!("FetchLiveKitSfuToken request for room {}", room_id); - - let Some(client) = get_client() else { - error!("FetchLiveKitSfuToken: No client available"); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: "Not logged in".to_string(), - }); - continue; - }; - - let Some(livekit_service_url) = get_livekit_service_url() else { - error!("FetchLiveKitSfuToken: No LiveKit service URL available"); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: "LiveKit service not configured for this homeserver".to_string(), - }); - continue; - }; - - let room_id_clone = room_id.clone(); - let _task = Handle::current().spawn(async move { - // First, get an OpenID token for authentication - let openid_token = match crate::call::matrixrtc::get_openid_token(&client).await { - Ok(token) => token, - Err(e) => { - error!("FetchLiveKitSfuToken: Failed to get OpenID token: {}", e); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: format!("Failed to get authentication token: {}", e), - }); - SignalToUI::set_ui_signal(); - return; - } - }; - - // Fetch the SFU token - match crate::call::matrixrtc::fetch_livekit_sfu_token( - &livekit_service_url, - room_id_clone.as_str(), - &openid_token, - &device_id, - ).await { - Ok(sfu_response) => { - if let (Some(jwt), Some(url)) = (sfu_response.jwt, sfu_response.url) { - log!("FetchLiveKitSfuToken: Successfully obtained JWT for LiveKit"); - Cx::post_action(crate::call::call_state::CallAction::LiveKitTokenReceived { - room_id: room_id_clone, - jwt, - livekit_url: url, - }); - } else { - error!("FetchLiveKitSfuToken: SFU response missing jwt or url"); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: "Invalid SFU response".to_string(), - }); - } - } - Err(e) => { - error!("FetchLiveKitSfuToken: Failed to fetch SFU token: {}", e); - Cx::post_action(crate::call::call_state::CallAction::MediaError { - error: format!("Failed to get call token: {}", e), - }); - } - } - SignalToUI::set_ui_signal(); - }); - } } } @@ -2701,44 +2097,6 @@ async fn matrix_worker_task( bail!("matrix_worker_task task ended unexpectedly") } -/// Fetches the RTC foci configuration from the homeserver's well-known endpoint. -/// Returns the LiveKit service URL if found. -async fn fetch_rtc_well_known(well_known_url: url::Url) -> Result, anyhow::Error> { - use serde_json::Value; - - let response = reqwest::get(well_known_url).await?; - - if !response.status().is_success() { - anyhow::bail!("Well-known request failed with status: {}", response.status()); - } - - let json: Value = response.json().await?; - - // Look for org.matrix.msc4143.rtc_foci array - let rtc_foci = match json.get("org.matrix.msc4143.rtc_foci") { - Some(Value::Array(arr)) => arr, - _ => { - log!("fetch_rtc_well_known: No org.matrix.msc4143.rtc_foci found"); - return Ok(None); - } - }; - - // Find the livekit entry - for focus in rtc_foci { - if let Some(focus_type) = focus.get("type").and_then(|t| t.as_str()) { - if focus_type == "livekit" { - if let Some(url) = focus.get("livekit_service_url").and_then(|u| u.as_str()) { - return Ok(Some(url.to_string())); - } - } - } - } - - log!("fetch_rtc_well_known: No livekit entry found in rtc_foci"); - Ok(None) -} - - /// The single global Tokio runtime that is used by all async tasks. static TOKIO_RUNTIME: Mutex> = Mutex::new(None); @@ -2762,7 +2120,7 @@ pub fn block_on_async_with_timeout( timeout: Option, async_future: impl Future, ) -> Result { - let rt = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| + let rt = TOKIO_RUNTIME.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(|| tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") ).handle().clone(); @@ -2782,7 +2140,7 @@ pub fn block_on_async_with_timeout( /// 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(|| { + let rt_handle = TOKIO_RUNTIME.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(|| { tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") }).handle().clone(); @@ -2791,7 +2149,7 @@ pub fn start_matrix_tokio() -> Result { rt_handle.spawn(async move { match build_client(&Cli::default(), app_data_dir()).await { Ok(client_and_session) => { - DEFAULT_SSO_CLIENT.lock().unwrap() + DEFAULT_SSO_CLIENT.lock().unwrap_or_else(|e| e.into_inner()) .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2897,19 +2255,19 @@ fn get_per_timeline_details<'a>( /// 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) + get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).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) + get_per_timeline_details(ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).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() + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()) .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2918,26 +2276,12 @@ fn get_room_timeline(room_id: &RoomId) -> Option> { static CLIENT: Mutex> = Mutex::new(None); pub fn get_client() -> Option { - CLIENT.lock().unwrap().clone() -} - -/// The LiveKit service URL fetched from the homeserver's well-known configuration. -/// This is used for MatrixRTC calls via LiveKit SFU. -static LIVEKIT_SERVICE_URL: Mutex> = Mutex::new(None); - -/// Returns the LiveKit service URL if it has been fetched from the homeserver. -pub fn get_livekit_service_url() -> Option { - LIVEKIT_SERVICE_URL.lock().unwrap().clone() -} - -/// Sets the LiveKit service URL. -fn set_livekit_service_url(url: Option) { - *LIVEKIT_SERVICE_URL.lock().unwrap() = url; + CLIENT.lock().unwrap_or_else(|e| e.into_inner()).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| + CLIENT.lock().unwrap_or_else(|e| e.into_inner()).as_ref().and_then(|c| c.session_meta().map(|m| m.user_id.clone()) ) } @@ -2974,12 +2318,12 @@ static IGNORED_USERS: Mutex> = Mutex::new(Hash /// Returns a deep clone of the current list of ignored users. pub fn get_ignored_users() -> HashSet { - IGNORED_USERS.lock().unwrap().clone() + IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).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) + IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).contains(user_id) } @@ -2992,7 +2336,7 @@ pub fn is_user_ignored(user_id: &UserId) -> bool { /// 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 mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); let jrd = all_joined_rooms.get_mut(kind.room_id())?; let details = match kind { TimelineKind::MainRoom { .. } => &mut jrd.main_timeline, @@ -3096,7 +2440,7 @@ impl RoomListServiceRoomInfo { 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); + REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).replace(sender); let (login_sender, mut login_receiver) = tokio::sync::mpsc::channel(1); @@ -3131,7 +2475,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { specified_username.as_ref().or(most_recent_user_id.as_ref()) ); match persistence::restore_session(specified_username).await { - Ok(session) => Some(session), + Ok((client, sync_token, session)) => Some((client, sync_token, session)), Err(e) => { let status_err = "Could not restore previous user session.\n\nPlease login again."; log!("{status_err} Error: {e:?}"); @@ -3144,7 +2488,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { homeserver: cli.homeserver.clone(), }); match login(cli, LoginRequest::LoginByCli).await { - Ok((client, sync_token, _is_add_account, _session)) => Some((client, sync_token)), + Ok((client, sync_token, _is_add_account, session)) => Some((client, sync_token, session)), Err(e) => { error!("CLI-based login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure( @@ -3171,7 +2515,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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() { + let (client, _sync_token, session) = match initial_client_opt.take() { Some(login) => login, None => { loop { @@ -3179,7 +2523,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { match login_receiver.recv().await { Some(login_request) => { match login(&cli, login_request).await { - Ok((client, sync_token, ..)) => break (client, sync_token), + Ok((client, sync_token, _is_add_account, session)) => break (client, sync_token, session), Err(e) => { error!("Login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); @@ -3214,8 +2558,19 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); enqueue_rooms_list_update(RoomsListUpdate::Status { status }); + // Add the account to the AccountManager + let account = account_manager::Account { + client: client.clone(), + user_id: logged_in_user_id.clone(), + session, + display_name: None, + avatar_url: None, + }; + let is_new = account_manager::add_account(account); + log!("Added account {} to AccountManager. New account: {}", logged_in_user_id, is_new); + // 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()) { + if let Some(_existing) = CLIENT.lock().unwrap_or_else(|e| e.into_inner()).replace(client.clone()) { error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); } @@ -3250,7 +2605,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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(); + let _ = CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); continue 'login_loop; } }; @@ -3272,7 +2627,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { let room_list_service = sync_service.room_list_service(); - if let Some(_existing) = SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)) { + if let Some(_existing) = SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).replace(Arc::new(sync_service)) { error!("BUG: unexpectedly replaced an existing sync service when initializing the matrix client."); } @@ -3368,10 +2723,10 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { log!("Account switch detected, restarting with user: {}", switch_user_id); // Clear all backend state - CLIENT.lock().unwrap().take(); - SYNC_SERVICE.lock().unwrap().take(); - ALL_JOINED_ROOMS.lock().unwrap().clear(); - IGNORED_USERS.lock().unwrap().clear(); + CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); + SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).take(); + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); + IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).clear(); // Clear the rooms list UI enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); @@ -3385,15 +2740,14 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // Restore session for the switched account match persistence::restore_session(Some(switch_user_id.clone())).await { - Ok((client, _sync_token)) => { + Ok((client, _sync_token, _session)) => { log!("Successfully restored session for {}", switch_user_id); // Store the client - CLIENT.lock().unwrap().replace(client.clone()); + CLIENT.lock().unwrap_or_else(|e| e.into_inner()).replace(client.clone()); // Set up the new client add_verification_event_handlers_and_sync_client(client.clone()); - crate::call::matrixrtc::add_matrixrtc_event_handlers(client.clone()); handle_ignore_user_list_subscriber(client.clone()); // Create new sync service @@ -3417,11 +2771,11 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { sync_service.start().await; let room_list_service = sync_service.room_list_service(); - SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)); + SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).replace(Arc::new(sync_service)); // Recreate worker task and service loops let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); - REQUEST_SENDER.lock().unwrap().replace(sender); + REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).replace(sender); let (login_sender, _login_receiver) = tokio::sync::mpsc::channel(1); let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); @@ -3530,7 +2884,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } // 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(); + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); } else { @@ -3570,7 +2924,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu VectorDiff::Clear => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Clear"); } all_known_rooms.clear(); - ALL_JOINED_ROOMS.lock().unwrap().clear(); + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); } @@ -3878,7 +3232,7 @@ async fn update_room( 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) { + if let Some(jrd) = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).get(room_id) { __timeline_update_sender_opt = Some(jrd.main_timeline.timeline_update_sender.clone()); } } @@ -3929,7 +3283,7 @@ async fn update_room( /// 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); + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).remove(&room.room_id); enqueue_rooms_list_update( RoomsListUpdate::RemoveRoom { room_id: room.room_id.clone(), @@ -3991,8 +3345,6 @@ async fn add_new_room( alt_aliases: new_room.room.alt_aliases(), // we don't actually display the latest event for Invited rooms, so don't bother. latest: None, - // TODO: fetch the invite timestamp from the invite event - invite_timestamp: None, invite_state: Default::default(), is_selected: false, is_direct: new_room.is_direct, @@ -4039,7 +3391,7 @@ async fn add_new_room( // 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( + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).insert( new_room.room_id.clone(), JoinedRoomDetails { room_id: new_room.room_id.clone(), @@ -4116,7 +3468,7 @@ fn handle_ignore_user_list_subscriber(client: Client) { .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 mut ignored_users_old = IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()); let has_changed = *ignored_users_old != ignored_users_new; *ignored_users_old = ignored_users_new; @@ -4942,7 +4294,7 @@ async fn spawn_sso_server( // 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(); + let client_and_session_opt = DEFAULT_SSO_CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); Handle::current().spawn(async move { // Try to use the DEFAULT_SSO_CLIENT that we proactively created @@ -5201,7 +4553,7 @@ impl UserPowerLevels { /// 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() { + if let Some(runtime) = TOKIO_RUNTIME.lock().unwrap_or_else(|e| e.into_inner()).take() { runtime.shutdown_background(); } } @@ -5209,11 +4561,11 @@ pub fn shutdown_background_tasks() { 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(); + CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); + SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).take(); + REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).take(); + IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).clear(); + ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); let on_clear_appstate = Arc::new(Notify::new()); Cx::post_action(LogoutAction::ClearAppState { on_clear_appstate: on_clear_appstate.clone() }); From ebfb2eb8e3a861acaf8cc011aceadebd41f53c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 30 Mar 2026 08:54:06 +0800 Subject: [PATCH 044/283] Fix logout when homeserver is unreachable - Continue local logout cleanup when server logout fails due to connectivity/unavailability - Handle logout button click before own_profile guard so it always responds --- src/logout/logout_state_machine.rs | 41 ++++++++++++++++++++++++++++++ src/settings/account_settings.rs | 8 +++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/logout/logout_state_machine.rs b/src/logout/logout_state_machine.rs index 3ccb922ca..a8776377b 100644 --- a/src/logout/logout_state_machine.rs +++ b/src/logout/logout_state_machine.rs @@ -344,6 +344,20 @@ impl LogoutStateMachine { 50 ).await?; + // Same delete operation as in the success case above + if let Err(e) = delete_latest_user_id().await { + log!("Warning: Failed to delete latest user ID: {}", e); + } + } else if should_continue_local_logout_without_server(&e) { + log!("Homeserver appears unavailable, continuing with local logout: {}", e); + self.point_of_no_return.store(true, Ordering::Release); + set_logout_point_of_no_return(true); + self.transition_to( + LogoutState::PointOfNoReturn, + "Homeserver unavailable, continuing with local logout".to_string(), + 50 + ).await?; + // Same delete operation as in the success case above if let Err(e) = delete_latest_user_id().await { log!("Warning: Failed to delete latest user ID: {}", e); @@ -553,6 +567,33 @@ impl LogoutStateMachine { } } +fn should_continue_local_logout_without_server(error: &LogoutError) -> bool { + match error { + LogoutError::Recoverable(RecoverableError::Timeout(_)) => true, + LogoutError::Recoverable(RecoverableError::ServerLogoutFailed(msg)) => { + let msg_lower = msg.to_ascii_lowercase(); + msg_lower.contains("timeout") + || msg_lower.contains("timed out") + || msg_lower.contains("service unavailable") + || msg_lower.contains("bad gateway") + || msg_lower.contains("gateway timeout") + || msg_lower.contains("too many requests") + || msg_lower.contains("error sending request") + || msg_lower.contains("connection") + || msg_lower.contains("connect") + || msg_lower.contains("network") + || msg_lower.contains("dns") + || msg_lower.contains("i/o") + || msg_lower.contains("tls") + || msg_lower.contains("status code: 429") + || msg_lower.contains("status code: 502") + || msg_lower.contains("status code: 503") + || msg_lower.contains("status code: 504") + } + _ => false, + } +} + /// Global atomic flag indicating if the logout process has reached the "point of no return" /// where aborting the logout operation is no longer safe. static LOGOUT_POINT_OF_NO_RETURN: AtomicBool = AtomicBool::new(false); diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 877f66bfc..4669039d4 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -368,6 +368,11 @@ impl MatchEvent for AccountSettings { } } + if self.view.button(cx, ids!(logout_button)).clicked(actions) { + cx.action(LogoutConfirmModalAction::Open); + return; + } + let Some(own_profile) = &self.own_profile else { return }; if upload_avatar_button.clicked(actions) { @@ -456,9 +461,6 @@ impl MatchEvent for AccountSettings { ); } - if self.view.button(cx, ids!(logout_button)).clicked(actions) { - cx.action(LogoutConfirmModalAction::Open); - } } } From dd9595eb4b569e6d2917e0090b951ea8e92f3b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 30 Mar 2026 11:31:12 +0800 Subject: [PATCH 045/283] feat: move room search to modal with local+remote results --- src/app.rs | 381 ++++++++++++++++++++++++++++++++- src/home/home_screen.rs | 21 -- src/home/navigation_tab_bar.rs | 3 +- src/home/rooms_list.rs | 33 +++ src/home/rooms_list_header.rs | 40 ++++ src/home/rooms_sidebar.rs | 6 +- src/home/spaces_bar.rs | 23 ++ src/sliding_sync.rs | 111 +++++++++- 8 files changed, 591 insertions(+), 27 deletions(-) diff --git a/src/app.rs b/src/app.rs index b9a75f6ee..171c208c1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,10 +8,10 @@ use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomI 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}, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, 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::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -104,6 +104,101 @@ script_mod! { invite_modal_inner := InviteModal {} } } + room_filter_modal := Modal { + content +: { + room_filter_modal_inner := RoundedShadowView { + width: 420, + height: Fit + flow: Down + spacing: 8 + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY_DARKER) + border_radius: 4.0 + border_size: 0.0 + shadow_color: #0005 + shadow_radius: 15.0 + shadow_offset: vec2(1.0, 0.0) + } + padding: Inset{top: 15, left: 15, right: 15, bottom: 15} + + room_filter_input_bar := RoomFilterInputBar {} + + search_results_title := Label { + width: Fill, + height: Fit, + margin: Inset{left: 4, top: 2} + text: "Search Results" + draw_text +: { + color: (COLOR_TEXT_INPUT_IDLE) + text_style: REGULAR_TEXT {font_size: 10} + } + } + + search_results_scroll := ScrollYView { + width: Fill, + height: 260 + show_bg: false + + search_results := View { + width: Fill, + height: Fit, + flow: Down + spacing: 4 + + search_results_empty := Label { + width: Fill, + height: Fit, + flow: Flow.Right{wrap: true}, + text: "Type to search rooms and spaces..." + draw_text +: { + color: (COLOR_TEXT) + text_style: REGULAR_TEXT {font_size: 10} + } + } + + remote_search_options := View { + visible: false + width: Fill, + height: Fit, + flow: Right + spacing: 6 + margin: Inset{top: 6} + + remote_search_people_button := RobrixNeutralIconButton { + width: Fit, + text: "People" + } + remote_search_rooms_button := RobrixNeutralIconButton { + width: Fit, + text: "Rooms" + } + remote_search_spaces_button := RobrixNeutralIconButton { + width: Fit, + text: "Spaces" + } + } + + search_results_list := View { + width: Fill, + height: Fit, + flow: Down + spacing: 3 + + result_item_0 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_1 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_2 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_3 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_4 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_5 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_6 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_7 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + } + } + } + } + } + } // Show the logout confirmation modal. logout_confirm_modal := Modal { @@ -162,6 +257,29 @@ script_mod! { app_main!(App); +#[derive(Clone)] +enum RoomFilterResultTarget { + LocalSpace { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, + LocalRoom { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, + RemoteSpace(RoomNameId), + RemoteRoom(RoomNameId), + RemoteUser(UserProfile), +} + +#[derive(Clone, Debug)] +pub enum RoomFilterRemoteSearchAction { + Results { + query: String, + kind: RemoteDirectorySearchKind, + results: Vec, + }, + Failed { + query: String, + kind: RemoteDirectorySearchKind, + error: String, + }, +} + #[derive(Script)] pub struct App { #[live] ui: WidgetRef, @@ -174,6 +292,7 @@ pub struct App { /// 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, + #[rust] room_filter_modal_results: Vec, } impl ScriptHook for App { @@ -255,6 +374,52 @@ impl MatchEvent for App { self.ui.modal(cx, ids!(positive_confirmation_modal)).close(cx); } + if let Some(clicked_index) = self.clicked_room_filter_result_index(cx, actions) { + if let Some(target) = self.room_filter_modal_results.get(clicked_index).cloned() { + self.ui.modal(cx, ids!(room_filter_modal)).close(cx); + match target { + RoomFilterResultTarget::LocalSpace { room_name_id: space_name_id, .. } + | RoomFilterResultTarget::RemoteSpace(space_name_id) => { + cx.action(NavigationBarAction::GoToSpace { space_name_id }); + } + RoomFilterResultTarget::LocalRoom { room_name_id, .. } + | RoomFilterResultTarget::RemoteRoom(room_name_id) => { + self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id)); + } + RoomFilterResultTarget::RemoteUser(user_profile) => { + submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { + user_profile, + allow_create: false, + }); + } + } + return; + } + } + + if let Some(kind) = self.clicked_room_filter_remote_option(cx, actions) { + let room_filter_input = self.ui.text_input(cx, ids!(room_filter_modal_inner.room_filter_input_bar.input)); + let query = room_filter_input.text().trim().to_owned(); + if !query.is_empty() { + let kind_text = match &kind { + RemoteDirectorySearchKind::People => "people", + RemoteDirectorySearchKind::Rooms => "rooms", + RemoteDirectorySearchKind::Spaces => "spaces", + }; + self.set_room_filter_modal_empty_state( + cx, + &format!("Searching {} on server...", kind_text), + false, + ); + submit_async_request(MatrixRequest::SearchDirectory { + query, + kind, + limit: 16, + }); + } + return; + } + for action in actions { match action.downcast_ref() { Some(LogoutConfirmModalAction::Open) => { @@ -311,6 +476,71 @@ impl MatchEvent for App { continue; } + if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { + self.update_room_filter_modal_results(cx, keywords); + continue; + } + + match action.downcast_ref() { + Some(RoomFilterRemoteSearchAction::Results { query, kind: _, results }) => { + let room_filter_input = self.ui.text_input(cx, ids!(room_filter_modal_inner.room_filter_input_bar.input)); + if room_filter_input.text().trim() != query.trim() { + continue; + } + self.room_filter_modal_results.clear(); + for result in results { + match result { + RemoteDirectorySearchResult::User(user_profile) => { + self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteUser(user_profile.clone())); + } + RemoteDirectorySearchResult::Room(room_name_id) => { + self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteRoom(room_name_id.clone())); + } + RemoteDirectorySearchResult::Space(space_name_id) => { + self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteSpace(space_name_id.clone())); + } + } + if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { + break; + } + } + if self.room_filter_modal_results.is_empty() { + self.set_room_filter_modal_empty_state( + cx, + &format!("No server results for \"{}\".", query), + true, + ); + } else { + self.set_room_filter_modal_empty_state(cx, "", false); + } + self.refresh_room_filter_modal_result_buttons(cx); + continue; + } + Some(RoomFilterRemoteSearchAction::Failed { query, kind: _, error }) => { + let room_filter_input = self.ui.text_input(cx, ids!(room_filter_modal_inner.room_filter_input_bar.input)); + if room_filter_input.text().trim() != query.trim() { + continue; + } + self.room_filter_modal_results.clear(); + self.refresh_room_filter_modal_result_buttons(cx); + self.set_room_filter_modal_empty_state( + cx, + &format!("Server search failed: {}", error), + true, + ); + continue; + } + _ => {} + } + + if let Some(RoomsListHeaderAction::OpenRoomFilterModal) = action.downcast_ref() { + self.ui.modal(cx, ids!(room_filter_modal)).open(cx); + let room_filter_input = self.ui.text_input(cx, ids!(room_filter_modal_inner.room_filter_input_bar.input)); + room_filter_input.set_key_focus(cx); + self.update_room_filter_modal_results(cx, &room_filter_input.text()); + 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); @@ -820,6 +1050,13 @@ impl AppMain for App { } impl App { + const ROOM_FILTER_RESULT_ITEM_IDS: [LiveId; 8] = [ + live_id!(result_item_0), live_id!(result_item_1), + live_id!(result_item_2), live_id!(result_item_3), + live_id!(result_item_4), live_id!(result_item_5), + live_id!(result_item_6), live_id!(result_item_7), + ]; + fn update_login_visibility(&self, cx: &mut Cx) { let show_login = !self.app_state.logged_in; if !show_login { @@ -831,6 +1068,146 @@ impl App { self.ui.view(cx, ids!(home_screen_view)).set_visible(cx, !show_login); } + fn clicked_room_filter_result_index(&self, cx: &mut Cx, actions: &Actions) -> Option { + let list_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); + for (index, item_id) in Self::ROOM_FILTER_RESULT_ITEM_IDS.iter().enumerate() { + if list_view.button(cx, &[*item_id, live_id!(click_button)]).clicked(actions) { + return Some(index); + } + } + None + } + + fn clicked_room_filter_remote_option(&self, cx: &mut Cx, actions: &Actions) -> Option { + let options_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options)); + if options_view.button(cx, ids!(remote_search_people_button)).clicked(actions) { + return Some(RemoteDirectorySearchKind::People); + } + if options_view.button(cx, ids!(remote_search_rooms_button)).clicked(actions) { + return Some(RemoteDirectorySearchKind::Rooms); + } + if options_view.button(cx, ids!(remote_search_spaces_button)).clicked(actions) { + return Some(RemoteDirectorySearchKind::Spaces); + } + None + } + + fn set_room_filter_modal_empty_state( + &self, + cx: &mut Cx, + text: &str, + show_remote_options: bool, + ) { + let empty_label = self.ui.label(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_empty)); + empty_label.set_visible(cx, !text.is_empty()); + if !text.is_empty() { + empty_label.set_text(cx, text); + } + self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.remote_search_options)) + .set_visible(cx, show_remote_options); + } + + fn refresh_room_filter_modal_result_buttons(&self, cx: &mut Cx) { + let list_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); + for (index, item_id) in Self::ROOM_FILTER_RESULT_ITEM_IDS.iter().enumerate() { + let item = list_view.view(cx, &[*item_id]); + if let Some(target) = self.room_filter_modal_results.get(index) { + let (name, raw_id) = match target { + RoomFilterResultTarget::LocalSpace { room_name_id, .. } + | RoomFilterResultTarget::LocalRoom { room_name_id, .. } => { + (room_name_id.to_string(), room_name_id.room_id().to_string()) + } + RoomFilterResultTarget::RemoteSpace(space_name_id) + | RoomFilterResultTarget::RemoteRoom(space_name_id) => { + (space_name_id.to_string(), space_name_id.room_id().to_string()) + } + RoomFilterResultTarget::RemoteUser(user_profile) => { + (user_profile.displayable_name().to_owned(), user_profile.user_id.to_string()) + } + }; + + item.label(cx, ids!(row.text_col.name_label)).set_text(cx, &name); + item.label(cx, ids!(row.text_col.id_label)).set_text(cx, &raw_id); + + let avatar_ref = item.avatar(cx, ids!(row.avatar)); + match target { + RoomFilterResultTarget::LocalSpace { avatar, .. } + | RoomFilterResultTarget::LocalRoom { avatar, .. } => { + match avatar { + FetchedRoomAvatar::Text(text) => { + avatar_ref.show_text(cx, None, None, text); + } + FetchedRoomAvatar::Image(image_data) => { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_err() { + avatar_ref.show_text(cx, None, None, &name); + } + } + } + } + RoomFilterResultTarget::RemoteSpace(_) + | RoomFilterResultTarget::RemoteRoom(_) + | RoomFilterResultTarget::RemoteUser(_) => { + avatar_ref.show_text(cx, None, None, &name); + } + } + + item.set_visible(cx, true); + } else { + item.set_visible(cx, false); + } + } + } + + fn update_room_filter_modal_results(&mut self, cx: &mut Cx, keywords: &str) { + let keywords = keywords.trim(); + self.room_filter_modal_results.clear(); + + if !keywords.is_empty() { + let space_items = cx.get_global::() + .get_matching_space_items(keywords, 4); + let room_items = cx.get_global::() + .get_matching_room_items(keywords, 8); + + for (room_name_id, avatar) in space_items { + self.room_filter_modal_results.push(RoomFilterResultTarget::LocalSpace { room_name_id, avatar }); + if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { + break; + } + } + if self.room_filter_modal_results.len() < Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { + for (room_name_id, avatar) in room_items { + self.room_filter_modal_results.push(RoomFilterResultTarget::LocalRoom { room_name_id, avatar }); + if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { + break; + } + } + } + } + + if keywords.is_empty() { + self.set_room_filter_modal_empty_state( + cx, + "Type to search rooms and spaces...", + false, + ); + } else if self.room_filter_modal_results.is_empty() { + self.set_room_filter_modal_empty_state( + cx, + &format!("No local results for \"{}\". Choose a type below to search server.", keywords), + true, + ); + } else { + self.set_room_filter_modal_empty_state(cx, "", false); + } + + self.refresh_room_filter_modal_result_buttons(cx); + } + /// Navigates to the given `destination_room`, optionally closing the `room_to_close`. fn navigate_to_room( &mut self, diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index c4d34d2aa..de033c820 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -206,26 +206,6 @@ script_mod! { width: Fill, height: Fill flow: Down - View { - width: Fill, - height: 39, - flow: Right - padding: Inset{top: 2, bottom: 2} - margin: Inset{right: 2} - spacing: 2 - align: Align{y: 0.5} - - CachedWidget { - room_filter_input_bar := RoomFilterInputBar {} - } - - search_messages_button := SearchMessagesButton { - // make this button match/align with the RoomFilterInputBar - height: 32.5, - margin: Inset{right: 2} - } - } - mod.widgets.MainDesktopUI {} } @@ -512,4 +492,3 @@ impl HomeScreen { ) } } - diff --git a/src/home/navigation_tab_bar.rs b/src/home/navigation_tab_bar.rs index 95cec1317..19f848dc4 100644 --- a/src/home/navigation_tab_bar.rs +++ b/src/home/navigation_tab_bar.rs @@ -34,7 +34,7 @@ use crate::{ avatar_cache::{self, AvatarCacheEntry}, login::login_screen::LoginAction, logout::logout_confirm_modal::LogoutAction, profile::{ user_profile::UserProfile, user_profile_cache::{self, UserProfileUpdate}, - }, shared::{ + }, home::spaces_bar::SpacesBarWidgetExt, shared::{ avatar::{AvatarState, AvatarWidgetExt}, styles::*, verification_badge::VerificationBadgeWidgetExt }, sliding_sync::{current_user_id, AccountDataAction}, utils::{self, RoomNameId} }; @@ -425,6 +425,7 @@ impl ScriptHook for NavigationTabBar { if let Some(mut rb) = self.view.radio_button(cx, ids!(home_button)).borrow_mut() { rb.animator_play(cx, ids!(active.on)); } + cx.set_global(self.view.spaces_bar(cx, ids!(root_spaces_bar))); }); } } diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 73ce9375e..c3a5792f8 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -1582,6 +1582,39 @@ impl RoomsListRef { .get(space_id) .map(|smv| smv.parent_chain.clone()) } + + /// Returns local room results matching `keywords`, up to `max_results`. + pub fn get_matching_room_items(&self, keywords: &str, max_results: usize) -> Vec<(RoomNameId, FetchedRoomAvatar)> { + let Some(inner) = self.borrow() else { return Vec::new(); }; + let keywords = keywords.trim().to_lowercase(); + if keywords.is_empty() { + return Vec::new(); + } + let mut items = Vec::new(); + let invited_rooms = inner.invited_rooms.borrow(); + for ir in invited_rooms.values() { + let name = ir.room_name_id.to_string(); + let room_id = ir.room_name_id.room_id().to_string(); + if name.to_lowercase().contains(&keywords) || room_id.to_lowercase().contains(&keywords) { + items.push((ir.room_name_id.clone(), ir.room_avatar.clone())); + if items.len() >= max_results { + return items; + } + } + } + drop(invited_rooms); + for jr in inner.all_joined_rooms.values() { + let name = jr.room_name_id.to_string(); + let room_id = jr.room_name_id.room_id().to_string(); + if name.to_lowercase().contains(&keywords) || room_id.to_lowercase().contains(&keywords) { + items.push((jr.room_name_id.clone(), jr.room_avatar.clone())); + if items.len() >= max_results { + return items; + } + } + } + items + } } pub struct RoomsListScopeProps { diff --git a/src/home/rooms_list_header.rs b/src/home/rooms_list_header.rs index eac4372a4..d0eda85a0 100644 --- a/src/home/rooms_list_header.rs +++ b/src/home/rooms_list_header.rs @@ -41,6 +41,40 @@ script_mod! { } }, + open_room_filter_modal_button := Button { + width: Fit, + height: Fit + padding: Inset{top: 6, bottom: 6, left: 6, right: 6} + margin: Inset{bottom: 2} + spacing: 0, + text: "" + draw_bg +: { + color: #0000 + color_hover: #0000 + color_down: #0000 + border_color: #0000 + border_color_hover: #0000 + border_color_down: #0000 + border_color_focus: #0000 + border_size: 0.0 + border_radius: 0.0 + } + draw_text +: { + color: #0000 + color_hover: #0000 + color_down: #0000 + color_focus: #0000 + } + draw_icon +: { + svg: (ICON_SEARCH) + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + color_focus: (COLOR_TEXT) + } + icon_walk: Walk{width: 16, height: Fit, margin: Inset{bottom: 2}} + } + View { width: Fit, height: Fit, margin: Inset{right: 3} @@ -93,6 +127,10 @@ pub struct RoomsListHeader { impl Widget for RoomsListHeader { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { + if self.view.button(cx, ids!(open_room_filter_modal_button)).clicked(actions) { + cx.action(RoomsListHeaderAction::OpenRoomFilterModal); + } + for action in actions { match action.downcast_ref() { Some(RoomsListHeaderAction::SetSyncStatus(is_syncing)) => { @@ -186,6 +224,8 @@ impl Widget for RoomsListHeader { /// Actions that can be handled by the `RoomsListHeader`. #[derive(Debug)] pub enum RoomsListHeaderAction { + /// Open the rooms/spaces filter modal. + OpenRoomFilterModal, /// An action received by the RoomsListHeader that will show or hide /// its sync status indicator (and loading spinner) based on the given boolean. SetSyncStatus(bool), diff --git a/src/home/rooms_sidebar.rs b/src/home/rooms_sidebar.rs index c50ca5695..79e99abe4 100644 --- a/src/home/rooms_sidebar.rs +++ b/src/home/rooms_sidebar.rs @@ -54,7 +54,11 @@ script_mod! { View { height: 23 } CachedWidget { - rooms_list_header := RoomsListHeader {} + rooms_list_header := RoomsListHeader { + open_room_filter_modal_button +: { + visible: false + } + } } View { diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 8f613dc93..9776183e5 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -825,3 +825,26 @@ impl SpacesBar { self.redraw(cx); } } + +impl SpacesBarRef { + /// Returns local spaces matching `keywords`, up to `max_results`. + pub fn get_matching_space_items(&self, keywords: &str, max_results: usize) -> Vec<(RoomNameId, FetchedRoomAvatar)> { + let Some(inner) = self.borrow() else { return Vec::new(); }; + let keywords = keywords.trim().to_lowercase(); + if keywords.is_empty() { + return Vec::new(); + } + let mut items = Vec::new(); + for space in inner.all_joined_spaces.values() { + let name = space.space_name_id.to_string(); + let space_id = space.space_name_id.room_id().to_string(); + if name.to_lowercase().contains(&keywords) || space_id.to_lowercase().contains(&keywords) { + items.push((space.space_name_id.clone(), space.space_avatar.clone())); + if items.len() >= max_results { + break; + } + } + } + items + } +} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 131e6610f..56255a1c7 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -11,11 +11,12 @@ use matrix_sdk::{ config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ api::{Direction, client::{ account::register::v3::Request as RegistrationRequest, + directory::get_public_rooms_filtered, error::ErrorKind, profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType, uiaa::{AuthData, AuthType, Dummy}, - }}, events::{ + }}, directory::{Filter as PublicRoomsFilter, RoomTypeFilter}, events::{ relation::RelationType, room::{ message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource @@ -37,7 +38,7 @@ use std::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefaul 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::{ + app::{AppStateAction, RoomFilterRemoteSearchAction}, 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, @@ -714,6 +715,12 @@ pub enum MatrixRequest { room_or_alias_id: OwnedRoomOrAliasId, via: Vec, }, + /// Request to search server-side directory for users, rooms, or spaces. + SearchDirectory { + query: String, + kind: RemoteDirectorySearchKind, + limit: u64, + }, /// Request to fetch the full details (the room preview) of a tombstoned room. GetSuccessorRoomDetails { tombstoned_room_id: OwnedRoomId, @@ -921,6 +928,20 @@ pub enum MatrixRequest { }, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RemoteDirectorySearchKind { + People, + Rooms, + Spaces, +} + +#[derive(Clone, Debug)] +pub enum RemoteDirectorySearchResult { + User(UserProfile), + Room(RoomNameId), + Space(RoomNameId), +} + /// 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() { @@ -1450,6 +1471,92 @@ async fn matrix_worker_task( }); } + MatrixRequest::SearchDirectory { query, kind, limit } => { + let Some(client) = get_client() else { continue }; + let _search_task = Handle::current().spawn(async move { + let query = query.trim().to_owned(); + let action_kind = kind.clone(); + if query.is_empty() { + Cx::post_action(RoomFilterRemoteSearchAction::Results { + query, + kind: action_kind, + results: Vec::new(), + }); + return; + } + + let result = match &kind { + RemoteDirectorySearchKind::People => { + client.search_users(&query, limit).await + .map(|response| { + response.results.into_iter() + .map(|user| { + RemoteDirectorySearchResult::User(UserProfile { + username: user.display_name, + user_id: user.user_id, + avatar_state: AvatarState::Known(user.avatar_url), + }) + }) + .collect::>() + }) + .map_err(|e| e.to_string()) + } + RemoteDirectorySearchKind::Rooms | RemoteDirectorySearchKind::Spaces => { + let mut filter = PublicRoomsFilter::new(); + filter.generic_search_term = Some(query.clone()); + filter.room_types = match &kind { + RemoteDirectorySearchKind::Rooms => vec![RoomTypeFilter::Default], + RemoteDirectorySearchKind::Spaces => vec![RoomTypeFilter::Space], + RemoteDirectorySearchKind::People => Vec::new(), + }; + let mut request = get_public_rooms_filtered::v3::Request::new(); + request.filter = filter; + client.public_rooms_filtered(request).await + .map(|response| { + response.chunk.into_iter() + .take(limit as usize) + .map(|room| { + let display_name = room.name + .or_else(|| room.canonical_alias.as_ref().map(ToString::to_string)) + .unwrap_or_else(|| room.room_id.to_string()); + let room_name_id = RoomNameId::new( + RoomDisplayName::Named(display_name), + room.room_id.clone(), + ); + match &kind { + RemoteDirectorySearchKind::Spaces => { + RemoteDirectorySearchResult::Space(room_name_id) + } + _ => { + RemoteDirectorySearchResult::Room(room_name_id) + } + } + }) + .collect::>() + }) + .map_err(|e| e.to_string()) + } + }; + + match result { + Ok(results) => { + Cx::post_action(RoomFilterRemoteSearchAction::Results { + query, + kind: action_kind, + results, + }); + } + Err(error) => { + Cx::post_action(RoomFilterRemoteSearchAction::Failed { + query, + kind: action_kind, + error, + }); + } + } + }); + } + MatrixRequest::GetSuccessorRoomDetails { tombstoned_room_id } => { let Some(client) = get_client() else { continue }; let (sender, successor_room) = { From 6c710bfe2aead1524d74256980622dfd60650ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Mon, 30 Mar 2026 12:14:34 +0800 Subject: [PATCH 046/283] feat: polish search modal results and header alignment --- src/app.rs | 253 ++++++++++++++++++++++++++++------ src/home/rooms_list.rs | 35 ++++- src/home/rooms_list_header.rs | 68 +++++---- src/home/spaces_bar.rs | 30 +++- src/sliding_sync.rs | 32 ++++- src/space_service_sync.rs | 10 +- 6 files changed, 342 insertions(+), 86 deletions(-) diff --git a/src/app.rs b/src/app.rs index 171c208c1..471fdebb2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,14 +4,14 @@ use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId}}; +use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId}}; use serde::{Deserialize, Serialize}; use crate::{ - avatar_cache::clear_avatar_cache, home::{ + avatar_cache::{self, AvatarCacheEntry, 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::AvatarWidgetRefExt, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, 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::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -21,6 +21,61 @@ script_mod! { use mod.prelude.widgets.* use mod.widgets.* + let RoomFilterResultItem = View { + visible: false + width: Fill + height: 48 + flow: Overlay + + row := View { + width: Fill + height: Fill + flow: Right + align: Align{y: 0.5} + spacing: 8 + padding: Inset{left: 8, right: 8, top: 5, bottom: 5} + + avatar := Avatar { width: 30, height: 30 } + + text_col := View { + width: Fill + height: Fit + flow: Down + spacing: 0 + + name_label := Label { + width: Fill + height: Fit + draw_text +: { + color: (COLOR_TEXT) + text_style: REGULAR_TEXT {font_size: 10} + } + } + + id_label := Label { + width: Fill + height: Fit + draw_text +: { + color: (COLOR_TEXT_INPUT_IDLE) + text_style: REGULAR_TEXT {font_size: 8.5} + } + } + } + } + + click_button := RobrixNeutralIconButton { + width: Fill + height: Fill + text: "" + icon_walk: Walk{width: 0, height: 0} + draw_bg +: { + color: #0000 + color_hover: #FFFFFF22 + color_down: #FFFFFF11 + } + } + } + load_all_resources() do #(App::script_component(vm)) { ui: Root { main_window := Window { @@ -185,14 +240,14 @@ script_mod! { flow: Down spacing: 3 - result_item_0 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_1 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_2 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_3 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_4 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_5 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_6 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } - result_item_7 := View { visible: false width: Fill height: 48 flow: Overlay row := View { width: Fill height: Fill flow: Right align: Align{y: 0.5} spacing: 8 padding: Inset{left: 8, right: 8, top: 5, bottom: 5} avatar := Avatar { width: 30, height: 30 } text_col := View { width: Fill height: Fit flow: Down spacing: 0 name_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT) text_style: REGULAR_TEXT {font_size: 10} } } id_label := Label { width: Fill height: Fit draw_text +: { color: (COLOR_TEXT_INPUT_IDLE) text_style: REGULAR_TEXT {font_size: 8.5} } } } } click_button := RobrixNeutralIconButton { width: Fill height: Fill text: "" icon_walk: Walk{width: 0, height: 0} draw_bg +: { color: #0000 color_hover: #FFFFFF22 color_down: #FFFFFF11 } } } + result_item_0 := RoomFilterResultItem {} + result_item_1 := RoomFilterResultItem {} + result_item_2 := RoomFilterResultItem {} + result_item_3 := RoomFilterResultItem {} + result_item_4 := RoomFilterResultItem {} + result_item_5 := RoomFilterResultItem {} + result_item_6 := RoomFilterResultItem {} + result_item_7 := RoomFilterResultItem {} } } } @@ -261,8 +316,8 @@ app_main!(App); enum RoomFilterResultTarget { LocalSpace { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, LocalRoom { room_name_id: RoomNameId, avatar: FetchedRoomAvatar }, - RemoteSpace(RoomNameId), - RemoteRoom(RoomNameId), + RemoteSpace { space_name_id: RoomNameId, avatar_uri: Option }, + RemoteRoom { room_name_id: RoomNameId, avatar_uri: Option }, RemoteUser(UserProfile), } @@ -293,6 +348,8 @@ pub struct App { /// When a view is popped off the stack, the previous `selected_room` is restored from here. #[rust] mobile_room_nav_stack: Vec, #[rust] room_filter_modal_results: Vec, + #[rust(Timer::empty())] room_filter_debounce_timer: Timer, + #[rust] pending_room_filter_keywords: String, } impl ScriptHook for App { @@ -358,6 +415,19 @@ impl MatchEvent for App { } } + fn handle_signal(&mut self, cx: &mut Cx) { + avatar_cache::process_avatar_updates(cx); + self.refresh_room_filter_modal_result_buttons(cx); + } + + fn handle_timer(&mut self, cx: &mut Cx, event: &TimerEvent) { + if self.room_filter_debounce_timer.is_timer(event).is_some() { + self.room_filter_debounce_timer = Timer::empty(); + let keywords = std::mem::take(&mut self.pending_room_filter_keywords); + self.update_room_filter_modal_results(cx, &keywords); + } + } + fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let invite_confirmation_modal_inner = self.ui.confirmation_modal(cx, ids!(invite_confirmation_modal_inner)); if let Some(_accepted) = invite_confirmation_modal_inner.closed(actions) { @@ -379,13 +449,27 @@ impl MatchEvent for App { self.ui.modal(cx, ids!(room_filter_modal)).close(cx); match target { RoomFilterResultTarget::LocalSpace { room_name_id: space_name_id, .. } - | RoomFilterResultTarget::RemoteSpace(space_name_id) => { + => { cx.action(NavigationBarAction::GoToSpace { space_name_id }); } RoomFilterResultTarget::LocalRoom { room_name_id, .. } - | RoomFilterResultTarget::RemoteRoom(room_name_id) => { + => { self.navigate_to_room(cx, None, &BasicRoomDetails::RoomId(room_name_id)); } + RoomFilterResultTarget::RemoteSpace { space_name_id, .. } => { + self.open_join_from_search_result( + cx, + BasicRoomDetails::Name(space_name_id), + true, + ); + } + RoomFilterResultTarget::RemoteRoom { room_name_id, .. } => { + self.open_join_from_search_result( + cx, + BasicRoomDetails::Name(room_name_id), + false, + ); + } RoomFilterResultTarget::RemoteUser(user_profile) => { submit_async_request(MatrixRequest::OpenOrCreateDirectMessage { user_profile, @@ -477,7 +561,9 @@ impl MatchEvent for App { } if let RoomFilterAction::Changed(keywords) = action.as_widget_action().cast_ref() { - self.update_room_filter_modal_results(cx, keywords); + cx.stop_timer(self.room_filter_debounce_timer); + self.pending_room_filter_keywords = keywords.clone(); + self.room_filter_debounce_timer = cx.start_timeout(0.12); continue; } @@ -493,11 +579,17 @@ impl MatchEvent for App { RemoteDirectorySearchResult::User(user_profile) => { self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteUser(user_profile.clone())); } - RemoteDirectorySearchResult::Room(room_name_id) => { - self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteRoom(room_name_id.clone())); + RemoteDirectorySearchResult::Room { room_name_id, avatar_uri } => { + self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteRoom { + room_name_id: room_name_id.clone(), + avatar_uri: avatar_uri.clone(), + }); } - RemoteDirectorySearchResult::Space(space_name_id) => { - self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteSpace(space_name_id.clone())); + RemoteDirectorySearchResult::Space { space_name_id, avatar_uri } => { + self.room_filter_modal_results.push(RoomFilterResultTarget::RemoteSpace { + space_name_id: space_name_id.clone(), + avatar_uri: avatar_uri.clone(), + }); } } if self.room_filter_modal_results.len() >= Self::ROOM_FILTER_RESULT_ITEM_IDS.len() { @@ -1057,6 +1149,21 @@ impl App { live_id!(result_item_6), live_id!(result_item_7), ]; + fn open_join_from_search_result( + &mut self, + cx: &mut Cx, + details: BasicRoomDetails, + is_space: bool, + ) { + cx.action(JoinLeaveRoomModalAction::Open { + kind: JoinLeaveModalKind::JoinRoom { + details, + is_space, + }, + show_tip: false, + }); + } + fn update_login_visibility(&self, cx: &mut Cx) { let show_login = !self.app_state.logged_in; if !show_login { @@ -1107,6 +1214,75 @@ impl App { .set_visible(cx, show_remote_options); } + fn set_room_filter_result_avatar( + &self, + cx: &mut Cx, + avatar_ref: &crate::shared::avatar::AvatarRef, + fallback_text: &str, + local_avatar: Option<&FetchedRoomAvatar>, + remote_avatar_uri: Option<&OwnedMxcUri>, + remote_avatar_state: Option<&AvatarState>, + ) { + if let Some(local_avatar) = local_avatar { + match local_avatar { + FetchedRoomAvatar::Text(text) => { + avatar_ref.show_text(cx, None, None, text); + } + FetchedRoomAvatar::Image(image_data) => { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_err() { + avatar_ref.show_text(cx, None, None, fallback_text); + } + } + } + return; + } + + if let Some(avatar_state) = remote_avatar_state { + if let Some(image_data) = avatar_state.data() { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), + ); + if res.is_ok() { + return; + } + } + if let Some(uri) = avatar_state.uri() { + if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_ok() { + return; + } + } + } + } + + if let Some(uri) = remote_avatar_uri { + if let AvatarCacheEntry::Loaded(image_data) = avatar_cache::get_or_fetch_avatar(cx, uri) { + let res = avatar_ref.show_image( + cx, + None, + |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, &image_data), + ); + if res.is_ok() { + return; + } + } + } + + avatar_ref.show_text(cx, None, None, fallback_text); + } + fn refresh_room_filter_modal_result_buttons(&self, cx: &mut Cx) { let list_view = self.ui.view(cx, ids!(room_filter_modal_inner.search_results_scroll.search_results.search_results_list)); for (index, item_id) in Self::ROOM_FILTER_RESULT_ITEM_IDS.iter().enumerate() { @@ -1117,8 +1293,8 @@ impl App { | RoomFilterResultTarget::LocalRoom { room_name_id, .. } => { (room_name_id.to_string(), room_name_id.room_id().to_string()) } - RoomFilterResultTarget::RemoteSpace(space_name_id) - | RoomFilterResultTarget::RemoteRoom(space_name_id) => { + RoomFilterResultTarget::RemoteSpace { space_name_id, .. } + | RoomFilterResultTarget::RemoteRoom { room_name_id: space_name_id, .. } => { (space_name_id.to_string(), space_name_id.room_id().to_string()) } RoomFilterResultTarget::RemoteUser(user_profile) => { @@ -1133,26 +1309,21 @@ impl App { match target { RoomFilterResultTarget::LocalSpace { avatar, .. } | RoomFilterResultTarget::LocalRoom { avatar, .. } => { - match avatar { - FetchedRoomAvatar::Text(text) => { - avatar_ref.show_text(cx, None, None, text); - } - FetchedRoomAvatar::Image(image_data) => { - let res = avatar_ref.show_image( - cx, - None, - |cx, img_ref| crate::utils::load_png_or_jpg(&img_ref, cx, image_data), - ); - if res.is_err() { - avatar_ref.show_text(cx, None, None, &name); - } - } - } + self.set_room_filter_result_avatar(cx, &avatar_ref, &name, Some(avatar), None, None); } - RoomFilterResultTarget::RemoteSpace(_) - | RoomFilterResultTarget::RemoteRoom(_) - | RoomFilterResultTarget::RemoteUser(_) => { - avatar_ref.show_text(cx, None, None, &name); + RoomFilterResultTarget::RemoteSpace { avatar_uri, .. } + | RoomFilterResultTarget::RemoteRoom { avatar_uri, .. } => { + self.set_room_filter_result_avatar(cx, &avatar_ref, &name, None, avatar_uri.as_ref(), None); + } + RoomFilterResultTarget::RemoteUser(user_profile) => { + self.set_room_filter_result_avatar( + cx, + &avatar_ref, + &name, + None, + None, + Some(&user_profile.avatar_state), + ); } } diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index c3a5792f8..24c08f762 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -268,6 +268,8 @@ impl ActionDefaultRef for RoomsListAction { pub struct JoinedRoomInfo { /// The displayable name of this room (includes room ID for fallback). pub room_name_id: RoomNameId, + /// Lowercased searchable text cached for fast local search. + pub search_text: String, /// The number of unread messages in this room. pub num_unread_messages: u64, /// The number of unread mentions in this room. @@ -310,6 +312,8 @@ pub struct JoinedRoomInfo { pub struct InvitedRoomInfo { /// The displayable name of this room (includes room ID for fallback). pub room_name_id: RoomNameId, + /// Lowercased searchable text cached for fast local search. + pub search_text: String, /// The canonical alias for this room, if any. pub canonical_alias: Option, /// The alternative aliases for this room, if any. @@ -340,6 +344,27 @@ pub struct InviterInfo { pub display_name: Option, pub avatar: Option>, } + +pub fn build_room_search_text( + room_name_id: &RoomNameId, + canonical_alias: &Option, + alt_aliases: &[OwnedRoomAliasId], +) -> String { + let mut search_text = format!( + "{} {}", + room_name_id.to_string().to_lowercase(), + room_name_id.room_id().as_str().to_lowercase(), + ); + if let Some(alias) = canonical_alias { + search_text.push(' '); + search_text.push_str(&alias.as_str().to_lowercase()); + } + for alias in alt_aliases { + search_text.push(' '); + search_text.push_str(&alias.as_str().to_lowercase()); + } + search_text +} impl std::fmt::Debug for InviterInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("InviterInfo") @@ -603,6 +628,7 @@ impl RoomsList { // Try to update joined room first if let Some(room) = self.all_joined_rooms.get_mut(&room_id) { room.room_name_id = new_room_name; + room.search_text = build_room_search_text(&room.room_name_id, &room.canonical_alias, &room.alt_aliases); let is_direct = room.is_direct; let should_display = should_display_room!(self, &room_id, room); let (pos_in_list, displayed_list) = if is_direct { @@ -629,6 +655,7 @@ impl RoomsList { let mut invited_rooms = self.invited_rooms.borrow_mut(); if let Some(invited_room) = invited_rooms.get_mut(&room_id) { invited_room.room_name_id = new_room_name; + invited_room.search_text = build_room_search_text(&invited_room.room_name_id, &invited_room.canonical_alias, &invited_room.alt_aliases); let should_display = should_display_room!(self, &room_id, invited_room); let pos_in_list = self.displayed_invited_rooms.iter() .position(|r| r == &room_id); @@ -1593,9 +1620,7 @@ impl RoomsListRef { let mut items = Vec::new(); let invited_rooms = inner.invited_rooms.borrow(); for ir in invited_rooms.values() { - let name = ir.room_name_id.to_string(); - let room_id = ir.room_name_id.room_id().to_string(); - if name.to_lowercase().contains(&keywords) || room_id.to_lowercase().contains(&keywords) { + if ir.search_text.contains(&keywords) { items.push((ir.room_name_id.clone(), ir.room_avatar.clone())); if items.len() >= max_results { return items; @@ -1604,9 +1629,7 @@ impl RoomsListRef { } drop(invited_rooms); for jr in inner.all_joined_rooms.values() { - let name = jr.room_name_id.to_string(); - let room_id = jr.room_name_id.room_id().to_string(); - if name.to_lowercase().contains(&keywords) || room_id.to_lowercase().contains(&keywords) { + if jr.search_text.contains(&keywords) { items.push((jr.room_name_id.clone(), jr.room_avatar.clone())); if items.len() >= max_results { return items; diff --git a/src/home/rooms_list_header.rs b/src/home/rooms_list_header.rs index d0eda85a0..d4aaa3a8e 100644 --- a/src/home/rooms_list_header.rs +++ b/src/home/rooms_list_header.rs @@ -26,13 +26,14 @@ script_mod! { height: Fit, padding: Inset{bottom: 4} flow: Right, + align: Align{y: 0.5} spacing: 3, header_title := Label { width: Fill, height: Fit, padding: 0 - margin: Inset{left: 5, top: -1} + margin: Inset{left: 5} flow: Right, // do not wrap text: "All Rooms" draw_text +: { @@ -41,38 +42,45 @@ script_mod! { } }, - open_room_filter_modal_button := Button { + open_room_filter_modal_button := View { width: Fit, height: Fit - padding: Inset{top: 6, bottom: 6, left: 6, right: 6} - margin: Inset{bottom: 2} - spacing: 0, - text: "" - draw_bg +: { - color: #0000 - color_hover: #0000 - color_down: #0000 - border_color: #0000 - border_color_hover: #0000 - border_color_down: #0000 - border_color_focus: #0000 - border_size: 0.0 - border_radius: 0.0 - } - draw_text +: { - color: #0000 - color_hover: #0000 - color_down: #0000 - color_focus: #0000 + margin: Inset{right: 1} + flow: Overlay, + + Icon { + draw_icon +: { + svg: (ICON_SEARCH) + color: (COLOR_TEXT) + } + icon_walk: Walk{width: 18, height: Fit, margin: Inset{bottom: 2}} } - draw_icon +: { - svg: (ICON_SEARCH) - color: (COLOR_TEXT) - color_hover: (COLOR_TEXT) - color_down: (COLOR_TEXT) - color_focus: (COLOR_TEXT) + + click_area := Button { + width: Fill, + height: Fill + padding: Inset{top: 6, bottom: 6, left: 6, right: 6} + spacing: 0, + text: "" + draw_bg +: { + color: #0000 + color_hover: #0000 + color_down: #0000 + border_color: #0000 + border_color_hover: #0000 + border_color_down: #0000 + border_color_focus: #0000 + border_size: 0.0 + border_radius: 0.0 + } + draw_text +: { + color: #0000 + color_hover: #0000 + color_down: #0000 + color_focus: #0000 + } + icon_walk: Walk{width: 0, height: 0} } - icon_walk: Walk{width: 16, height: Fit, margin: Inset{bottom: 2}} } View { @@ -127,7 +135,7 @@ pub struct RoomsListHeader { impl Widget for RoomsListHeader { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { if let Event::Actions(actions) = event { - if self.view.button(cx, ids!(open_room_filter_modal_button)).clicked(actions) { + if self.view.button(cx, ids!(open_room_filter_modal_button.click_area)).clicked(actions) { cx.action(RoomsListHeaderAction::OpenRoomFilterModal); } diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 9776183e5..75b03765d 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -358,6 +358,8 @@ impl SpacesBarEntryRef { pub struct JoinedSpaceInfo { /// The display name and ID of the space. pub space_name_id: RoomNameId, + /// Lowercased searchable text cached for fast local search. + pub search_text: String, /// The canonical alias of the space, if any. pub canonical_alias: Option, /// The topic of the space, if any. @@ -376,6 +378,27 @@ pub struct JoinedSpaceInfo { pub children_count: u64, } +pub fn build_space_search_text( + space_name_id: &RoomNameId, + canonical_alias: &Option, + topic: &Option, +) -> String { + let mut search_text = format!( + "{} {}", + space_name_id.to_string().to_lowercase(), + space_name_id.room_id().as_str().to_lowercase(), + ); + if let Some(alias) = canonical_alias { + search_text.push(' '); + search_text.push_str(&alias.as_str().to_lowercase()); + } + if let Some(topic) = topic { + search_text.push(' '); + search_text.push_str(&topic.to_lowercase()); + } + search_text +} + /// The possible updates that should be displayed by the single list of all spaces. @@ -678,6 +701,7 @@ impl SpacesBar { if let Some(space) = self.all_joined_spaces.get_mut(&space_id) { let was_displayed = (self.display_filter)(space); space.canonical_alias = new_canonical_alias; + space.search_text = build_space_search_text(&space.space_name_id, &space.canonical_alias, &space.topic); let should_display = (self.display_filter)(space); adjust_displayed_spaces(was_displayed, should_display, space_id, &mut self.displayed_spaces); } else { @@ -692,6 +716,7 @@ impl SpacesBar { RoomDisplayName::Named(new_space_name), space_id.clone(), ); + space.search_text = build_space_search_text(&space.space_name_id, &space.canonical_alias, &space.topic); let should_display = (self.display_filter)(space); adjust_displayed_spaces(was_displayed, should_display, space_id, &mut self.displayed_spaces); } else { @@ -704,6 +729,7 @@ impl SpacesBar { // We don't currently support filtering by topic. // let was_displayed = (self.display_filter)(space); space.topic = topic; + space.search_text = build_space_search_text(&space.space_name_id, &space.canonical_alias, &space.topic); // let should_display = (self.display_filter)(space); // adjust_displayed_spaces(was_displayed, should_display, space_id, &mut self.displayed_spaces); } else { @@ -836,9 +862,7 @@ impl SpacesBarRef { } let mut items = Vec::new(); for space in inner.all_joined_spaces.values() { - let name = space.space_name_id.to_string(); - let space_id = space.space_name_id.room_id().to_string(); - if name.to_lowercase().contains(&keywords) || space_id.to_lowercase().contains(&keywords) { + if space.search_text.contains(&keywords) { items.push((space.space_name_id.clone(), space.space_avatar.clone())); if items.len() >= max_results { break; diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 56255a1c7..71b2a466e 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -39,7 +39,7 @@ use std::io; use hashbrown::{HashMap, HashSet}; use crate::{ app::{AppStateAction, RoomFilterRemoteSearchAction}, 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 + add_room::KnockResultAction, invite_screen::{JoinRoomResultAction, LeaveRoomResultAction}, link_preview::{LinkPreviewData, LinkPreviewDataNonNumeric, LinkPreviewRateLimitResponse}, room_screen::{InviteResultAction, TimelineUpdate}, rooms_list::{self, InvitedRoomInfo, InviterInfo, JoinedRoomInfo, RoomsListUpdate, build_room_search_text, 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}, @@ -938,8 +938,14 @@ pub enum RemoteDirectorySearchKind { #[derive(Clone, Debug)] pub enum RemoteDirectorySearchResult { User(UserProfile), - Room(RoomNameId), - Space(RoomNameId), + Room { + room_name_id: RoomNameId, + avatar_uri: Option, + }, + Space { + space_name_id: RoomNameId, + avatar_uri: Option, + }, } /// Submits a request to the worker thread to be executed asynchronously. @@ -1525,10 +1531,16 @@ async fn matrix_worker_task( ); match &kind { RemoteDirectorySearchKind::Spaces => { - RemoteDirectorySearchResult::Space(room_name_id) + RemoteDirectorySearchResult::Space { + space_name_id: room_name_id, + avatar_uri: room.avatar_url, + } } _ => { - RemoteDirectorySearchResult::Room(room_name_id) + RemoteDirectorySearchResult::Room { + room_name_id, + avatar_uri: room.avatar_url, + } } } }) @@ -3552,6 +3564,11 @@ async fn add_new_room( }; rooms_list::enqueue_rooms_list_update(RoomsListUpdate::AddInvitedRoom(InvitedRoomInfo { room_name_id: room_name_id.clone(), + search_text: build_room_search_text( + &room_name_id, + &new_room.room.canonical_alias(), + &new_room.room.alt_aliases(), + ), inviter_info, room_avatar, canonical_alias: new_room.room.canonical_alias(), @@ -3636,6 +3653,11 @@ async fn add_new_room( is_marked_unread: new_room.is_marked_unread, room_avatar, room_name_id: room_name_id.clone(), + search_text: build_room_search_text( + &room_name_id, + &new_room.room.canonical_alias(), + &new_room.room.alt_aliases(), + ), canonical_alias: new_room.room.canonical_alias(), alt_aliases: new_room.room.alt_aliases(), has_been_paginated: false, diff --git a/src/space_service_sync.rs b/src/space_service_sync.rs index c02bbc8a1..a77d0633c 100644 --- a/src/space_service_sync.rs +++ b/src/space_service_sync.rs @@ -10,7 +10,7 @@ use matrix_sdk::{Client, RoomState, media::MediaRequestParameters}; use matrix_sdk_ui::spaces::{SpaceRoom, SpaceRoomList, SpaceService, room_list::SpaceRoomListPaginationState}; use ruma::{OwnedMxcUri, OwnedRoomId, events::room::MediaSource, room::RoomType}; use tokio::{runtime::Handle, sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::JoinHandle}; -use crate::{home::{rooms_list::{RoomsListUpdate, enqueue_rooms_list_update}, spaces_bar::{JoinedSpaceInfo, SpacesListUpdate, enqueue_spaces_list_update}}, room::FetchedRoomAvatar, utils::{self, RoomNameId}}; +use crate::{home::{rooms_list::{RoomsListUpdate, enqueue_rooms_list_update}, spaces_bar::{JoinedSpaceInfo, SpacesListUpdate, build_space_search_text, enqueue_spaces_list_update}}, room::FetchedRoomAvatar, utils::{self, RoomNameId}}; /// Whether to enable verbose logging of all spaces service diff updates. const LOG_SPACE_SERVICE_DIFFS: bool = cfg!(feature = "log_space_service_diffs"); @@ -350,6 +350,14 @@ async fn add_new_space(space: &SpaceRoom, client: &Client) { matrix_sdk::RoomDisplayName::Named(space.display_name.clone()), space.room_id.clone(), ), + search_text: build_space_search_text( + &RoomNameId::new( + matrix_sdk::RoomDisplayName::Named(space.display_name.clone()), + space.room_id.clone(), + ), + &space.canonical_alias, + &space.topic, + ), canonical_alias: space.canonical_alias.clone(), topic: space.topic.clone(), space_avatar, From 0d9df460d156a9df53970abd139167d9a2c78b20 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Wed, 1 Apr 2026 15:34:12 +0800 Subject: [PATCH 047/283] remove unneccessary changes --- Cargo.lock | 162 +++++----- Cargo.toml | 7 +- resources/icon_home.svg | 5 + resources/icons/add_user.svg | 6 +- resources/icons/home.svg | 12 +- resources/icons/import.svg | 6 +- resources/icons/import2.svg | 6 + src/app.rs | 9 +- src/home/home_screen.rs | 129 +------- src/home/link_preview.rs | 2 +- src/home/main_desktop_ui.rs | 8 +- src/home/room_context_menu.rs | 30 +- src/home/room_screen.rs | 10 +- src/location.rs | 8 +- src/login/login_screen.rs | 13 + src/media_cache.rs | 10 +- src/room/reply_preview.rs | 2 +- src/settings/settings_screen.rs | 1 + src/shared/styles.rs | 8 +- src/sliding_sync.rs | 507 ++++++++++++++++++++++++++++--- 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 +- 25 files changed, 665 insertions(+), 338 deletions(-) create mode 100644 resources/icon_home.svg create mode 100644 resources/icons/import2.svg diff --git a/Cargo.lock b/Cargo.lock index 89c2a4693..5e87a8380 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "accessory" @@ -610,7 +610,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "bytes" @@ -1936,9 +1936,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", + "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", ] [[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/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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=stack_nav_improvements)", "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=stack_nav_improvements)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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=stack_nav_improvements)", "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=stack_nav_improvements)", "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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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=stack_nav_improvements)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", "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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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=stack_nav_improvements)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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=stack_nav_improvements)", + "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", "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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements)", "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=stack_nav_improvements)", "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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "siphasher" @@ -5597,7 +5597,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +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)", "libc", @@ -6794,7 +6794,7 @@ dependencies = [ [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +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", @@ -6813,7 +6813,7 @@ dependencies = [ [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/makepad/makepad?branch=dev#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" [[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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" 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#66075ff67f3912fc94eb473ee37042a63cc66d60" +source = "git+https://github.com/kevinaboos/makepad?branch=stack_nav_improvements#461b05134a501b8e67f431b2706fb200d4bcf68a" dependencies = [ "windows-link 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index daf7ba9e2..8bd24357a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,11 @@ 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/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" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. diff --git a/resources/icon_home.svg b/resources/icon_home.svg new file mode 100644 index 000000000..f5edd734b --- /dev/null +++ b/resources/icon_home.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/icons/add_user.svg b/resources/icons/add_user.svg index 640aa9d94..fad47b630 100644 --- a/resources/icons/add_user.svg +++ b/resources/icons/add_user.svg @@ -1,6 +1,4 @@ - - - - + + \ No newline at end of file diff --git a/resources/icons/home.svg b/resources/icons/home.svg index 5b5b85c8d..519a1bf2e 100644 --- a/resources/icons/home.svg +++ b/resources/icons/home.svg @@ -1,4 +1,10 @@ - - - + + + + + + + + \ No newline at end of file diff --git a/resources/icons/import.svg b/resources/icons/import.svg index b07d957e2..b23a1d1e6 100644 --- a/resources/icons/import.svg +++ b/resources/icons/import.svg @@ -1,4 +1,2 @@ - - - - + + \ No newline at end of file diff --git a/resources/icons/import2.svg b/resources/icons/import2.svg new file mode 100644 index 000000000..8eef3aa30 --- /dev/null +++ b/resources/icons/import2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 0d3559138..2c0707fc4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -323,11 +323,6 @@ impl MatchEvent for App { self.app_state.selected_room = None; // Clear saved dock state so tabs will be closed self.app_state.saved_dock_state_home = Default::default(); - enqueue_popup_notification( - format!("Switching to account {}...", user_id), - PopupKind::Info, - Some(3.0), - ); self.ui.redraw(cx); continue; } @@ -335,8 +330,8 @@ impl MatchEvent for App { log!("Account switch completed to: {}", user_id); enqueue_popup_notification( format!("Switched to account {}", user_id), - PopupKind::Info, - Some(5.0), + PopupKind::Success, + Some(3.0), ); self.ui.redraw(cx); continue; diff --git a/src/home/home_screen.rs b/src/home/home_screen.rs index 069d5a35b..c4d34d2aa 100644 --- a/src/home/home_screen.rs +++ b/src/home/home_screen.rs @@ -1,16 +1,6 @@ use makepad_widgets::*; -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, -}; +use crate::{app::AppState, home::navigation_tab_bar::{NavigationBarAction, SelectedTab}, settings::settings_screen::SettingsScreenWidgetRefExt}; script_mod! { use mod.prelude.widgets.* @@ -416,10 +406,6 @@ 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 { @@ -489,29 +475,6 @@ 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(); - } - } } } @@ -548,95 +511,5 @@ 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); - } } diff --git a/src/home/link_preview.rs b/src/home/link_preview.rs index 10155dd03..1d605dc3d 100644 --- a/src/home/link_preview.rs +++ b/src/home/link_preview.rs @@ -647,7 +647,7 @@ impl LinkPreviewCache { LinkPreviewCacheEntry::Requested } - Entry::Occupied(occupied) => occupied.get().lock().unwrap_or_else(|e| e.into_inner()).entry.clone(), + Entry::Occupied(occupied) => occupied.get().lock().unwrap().entry.clone(), } } diff --git a/src/home/main_desktop_ui.rs b/src/home/main_desktop_ui.rs index 628d477bf..242ba76f0 100644 --- a/src/home/main_desktop_ui.rs +++ b/src/home/main_desktop_ui.rs @@ -3,7 +3,7 @@ use ruma::OwnedRoomId; use tokio::sync::Notify; use std::{collections::HashMap, sync::Arc}; -use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, utils::RoomNameId}; +use crate::{app::{AppState, AppStateAction, SavedDockState, SelectedRoom}, home::{navigation_tab_bar::{NavigationBarAction, SelectedTab}, rooms_list::RoomsListRef, space_lobby::SpaceLobbyScreenWidgetRefExt}, sliding_sync::AccountSwitchAction, utils::RoomNameId}; use super::{invite_screen::InviteScreenWidgetRefExt, room_screen::RoomScreenWidgetRefExt, rooms_list::RoomsListAction}; script_mod! { @@ -421,6 +421,12 @@ impl WidgetMatchEvent for MainDesktopUI { continue; } + // When switching accounts, close all room tabs (keeping only the home tab) + if let Some(AccountSwitchAction::Starting(_)) = action.downcast_ref() { + self.close_all_tabs(cx); + continue; + } + // If the currently-selected space has been changed, we must handle that // by switching the dock to show the layout for another space. if let Some(NavigationBarAction::TabSelected(tab)) = action.downcast_ref() { diff --git a/src/home/room_context_menu.rs b/src/home/room_context_menu.rs index 1db67ec77..796a43a86 100644 --- a/src/home/room_context_menu.rs +++ b/src/home/room_context_menu.rs @@ -249,13 +249,29 @@ impl WidgetMatchEvent for RoomContextMenu { current_user_id().as_deref(), ) { Ok(bot_user_id) => { - // TODO: implement SetRoomBotBinding request - let _ = (room_id, bot_user_id); - enqueue_popup_notification( - "BotFather binding feature is not yet implemented.", - PopupKind::Warning, - Some(4.0), - ); + if details.is_bot_bound { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id, + bound: false, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + format!("Removing BotFather {bot_user_id} from this room..."), + PopupKind::Info, + Some(4.0), + ); + } else { + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id, + bound: true, + bot_user_id: bot_user_id.clone(), + }); + enqueue_popup_notification( + format!("Inviting BotFather {bot_user_id} into this room..."), + PopupKind::Info, + Some(5.0), + ); + } } Err(error) => { enqueue_popup_notification(error, PopupKind::Error, Some(5.0)); diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index b4a1f5c3a..a74781e9b 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1367,12 +1367,16 @@ impl Widget for RoomScreen { ) { Ok(bot_user_id) => { - // TODO: implement SetRoomBotBinding request + submit_async_request(MatrixRequest::SetRoomBotBinding { + room_id: room_props.room_name_id.room_id().clone(), + bound: false, + bot_user_id: bot_user_id.clone(), + }); enqueue_popup_notification( format!( - "BotFather binding feature is not yet implemented for {bot_user_id}" + "Removing BotFather {bot_user_id} from this room..." ), - PopupKind::Warning, + PopupKind::Info, Some(4.0), ); } diff --git a/src/location.rs b/src/location.rs index 7ab43f100..515d00322 100644 --- a/src/location.rs +++ b/src/location.rs @@ -29,7 +29,7 @@ static LATEST_LOCATION: Mutex> = Mutex::new(None); /// Note that this function is guaranteed to return `None` if /// [`init_location_subscriber`] has not been called yet. pub fn get_latest_location() -> Option { - *(LATEST_LOCATION.lock().unwrap_or_else(|e| e.into_inner())) + *(LATEST_LOCATION.lock().unwrap()) } @@ -46,7 +46,7 @@ impl robius_location::Handler for LocationHandler { time: location.time().ok(), }; Cx::post_action(LocationAction::Update(update)); - *LATEST_LOCATION.lock().unwrap_or_else(|e| e.into_inner()) = Some(update); + *LATEST_LOCATION.lock().unwrap() = Some(update); } Err(e) => { error!("Error getting coordinates from location update: {e:?}"); @@ -98,7 +98,7 @@ static LOCATION_REQUEST_SENDER: Mutex>> = Mutex:: /// Submits a request to start, stop, or get a single new location update(s). pub fn request_location_update(request: LocationRequest) { - if let Some(sender) = LOCATION_REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).as_ref() { + if let Some(sender) = LOCATION_REQUEST_SENDER.lock().unwrap().as_ref() { if let Err(err) = sender.send(request) { error!("Error sending location request: {err:?}"); } @@ -120,7 +120,7 @@ pub fn request_location_update(request: LocationRequest) { /// which isn't used, but acts as a guarantee that this function /// must only be called by the main UI thread. pub fn init_location_subscriber(_cx: &mut Cx) -> Result<(), robius_location::Error> { - let mut lrs = LOCATION_REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()); + let mut lrs = LOCATION_REQUEST_SENDER.lock().unwrap(); if lrs.is_some() { log!("Location subscriber already initialized."); return Ok(()); diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 5fdedfa1d..29070debd 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -123,6 +123,19 @@ script_mod! { is_password: true, } + confirm_password_wrapper := View { + width: 275, height: Fit, + visible: false, + + confirm_password_input := RobrixTextInput { + width: 275, height: Fit + flow: Right, // do not wrap + padding: 10, + empty_text: "Confirm password" + is_password: true, + } + } + View { width: 275, height: Fit, flow: Down, diff --git a/src/media_cache.rs b/src/media_cache.rs index ce482dbea..f87ae36da 100644 --- a/src/media_cache.rs +++ b/src/media_cache.rs @@ -95,7 +95,7 @@ impl MediaCache { MediaFormat::Thumbnail(ref requested_mts) => { if let Some((entry_ref, existing_mts)) = value.thumbnail.as_ref() { return ( - entry_ref.lock().unwrap_or_else(|e| e.into_inner()).deref().clone(), + entry_ref.lock().unwrap().deref().clone(), MediaFormat::Thumbnail(existing_mts.clone()), ); } else { @@ -104,7 +104,7 @@ impl MediaCache { value.thumbnail = Some((Arc::clone(&entry_ref), requested_mts.clone())); // If a full-size image is already loaded, return it. if let Some(existing_file) = value.full_file.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap_or_else(|e| e.into_inner()).deref() { + if let MediaCacheEntry::Loaded(d) = existing_file.lock().unwrap().deref() { post_request_retval = ( MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::File, @@ -117,7 +117,7 @@ impl MediaCache { MediaFormat::File => { if let Some(entry_ref) = value.full_file.as_ref() { return ( - entry_ref.lock().unwrap_or_else(|e| e.into_inner()).deref().clone(), + entry_ref.lock().unwrap().deref().clone(), MediaFormat::File, ); } else { @@ -126,7 +126,7 @@ impl MediaCache { value.full_file = Some(entry_ref.clone()); // If a thumbnail is already loaded, return it. if let Some((existing_thumbnail, existing_mts)) = value.thumbnail.as_ref() { - if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap_or_else(|e| e.into_inner()).deref() { + if let MediaCacheEntry::Loaded(d) = existing_thumbnail.lock().unwrap().deref() { post_request_retval = ( MediaCacheEntry::Loaded(Arc::clone(d)), MediaFormat::Thumbnail(existing_mts.clone()), @@ -272,7 +272,7 @@ fn insert_into_cache>>( Err(e) => error_to_media_cache_entry(e, &request) }; - *value_ref.lock().unwrap_or_else(|e| e.into_inner()) = new_value; + *value_ref.lock().unwrap() = new_value; if let Some(sender) = update_sender { let _ = sender.send(TimelineUpdate::MediaFetched(request)); diff --git a/src/room/reply_preview.rs b/src/room/reply_preview.rs index 5a53687bb..03ec07948 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: 4.0 + draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_CLOSE) icon_walk: Walk{width: 16, height: 16, margin: 0} } diff --git a/src/settings/settings_screen.rs b/src/settings/settings_screen.rs index b67fe998e..5d24945bc 100644 --- a/src/settings/settings_screen.rs +++ b/src/settings/settings_screen.rs @@ -7,6 +7,7 @@ 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 feb778dff..a80fa55e5 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") + mod.widgets.ICON_ADD_USER = crate_resource("self://resources/icons/add_user.svg") // TODO: FIX 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") @@ -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") + mod.widgets.ICON_IMPORT = crate_resource("self://resources/icons/import.svg") // TODO: FIX 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,10 +187,6 @@ 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/sliding_sync.rs b/src/sliding_sync.rs index aa24dc767..9de350490 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -9,7 +9,13 @@ 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::{MediaFormat, MediaRequestParameters}, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ - api::{Direction, client::{profile::{AvatarUrl, DisplayName}, receipt::create_receipt::v3::ReceiptType}}, events::{ + api::{Direction, client::{ + account::register::v3::Request as RegistrationRequest, + error::ErrorKind, + profile::{AvatarUrl, DisplayName}, + receipt::create_receipt::v3::ReceiptType, + uiaa::{AuthData, AuthType, Dummy}, + }}, events::{ relation::RelationType, room::{ message::RoomMessageEventContent, power_levels::RoomPowerLevels, MediaSource @@ -85,9 +91,11 @@ impl std::fmt::Debug for Cli { impl From for Cli { fn from(login: LoginByPassword) -> Self { Self { - user_id: login.user_id, + user_id: login.user_id.trim().to_owned(), password: login.password, - homeserver: login.homeserver, + homeserver: login.homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), proxy: None, login_screen: false, verbose: false, @@ -95,6 +103,193 @@ impl From for Cli { } } +impl From for Cli { + fn from(registration: RegisterAccount) -> Self { + Self { + user_id: registration.user_id.trim().to_owned(), + password: registration.password, + homeserver: registration.homeserver + .map(|homeserver| homeserver.trim().to_owned()) + .filter(|homeserver| !homeserver.is_empty()), + proxy: None, + login_screen: false, + verbose: false, + } + } +} + +fn infer_homeserver_from_user_id(user_id: &str) -> Option { + let user_id: OwnedUserId = user_id.trim().try_into().ok()?; + Some(user_id.server_name().to_string()) +} + +async fn finalize_authenticated_client( + client: Client, + client_session: ClientSessionPersisted, + fallback_user_id: &str, + is_add_account: bool, +) -> Result<(Client, Option, bool, ClientSessionPersisted)> { + if client.matrix_auth().logged_in() { + let logged_in_user_id = client.user_id() + .map(ToString::to_string) + .unwrap_or_else(|| fallback_user_id.to_owned()); + log!("Logged in successfully."); + let status = format!("Logged in as {}.\n → Loading rooms...", logged_in_user_id); + enqueue_rooms_list_update(RoomsListUpdate::Status { status }); + if let Err(e) = persistence::save_session(&client, client_session.clone()).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, is_add_account, client_session)) + } else { + let err_msg = format!( + "Authentication succeeded for {fallback_user_id}, but the homeserver did not return a login session." + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + bail!(err_msg); + } +} + +fn registration_localpart(user_id: &str) -> Result { + let trimmed = user_id.trim(); + if trimmed.is_empty() { + bail!("Please enter a valid username or Matrix user ID."); + } + + if let Ok(full_user_id) = >::try_from(trimmed) { + return Ok(full_user_id.localpart().to_owned()); + } + + let localpart = trimmed.trim_start_matches('@'); + if localpart.is_empty() || localpart.contains(':') || localpart.chars().any(char::is_whitespace) { + bail!("Please enter a valid username or full Matrix user ID."); + } + + Ok(localpart.to_owned()) +} + +fn registration_request( + username: &str, + password: &str, + session: Option, +) -> RegistrationRequest { + let mut request = RegistrationRequest::new(); + request.username = Some(username.to_owned()); + request.password = Some(password.to_owned()); + request.initial_device_display_name = Some("robrix-un-pw".to_owned()); + request.refresh_token = true; + if let Some(session) = session { + let mut dummy = Dummy::new(); + dummy.session = Some(session); + request.auth = Some(AuthData::Dummy(dummy)); + } + request +} + +fn registration_uiaa_error_message(error: &matrix_sdk::Error) -> String { + if let matrix_sdk::Error::Http(http_error) = error { + match http_error.client_api_error_kind() { + Some(ErrorKind::UserInUse) => { + return "That user ID is already taken. Please choose another one.".to_owned(); + } + Some(ErrorKind::InvalidUsername) => { + return "That user ID is invalid. Use a username like `alice` or a full Matrix ID like `@alice:matrix.org`.".to_owned(); + } + Some(ErrorKind::WeakPassword) => { + return "That password is too weak. Please choose a stronger password.".to_owned(); + } + Some(ErrorKind::Forbidden { .. }) => { + return "This homeserver does not allow open registration.".to_owned(); + } + Some(ErrorKind::LimitExceeded { .. }) => { + return "The homeserver is rate limiting account creation right now. Please try again shortly.".to_owned(); + } + _ => {} + } + } + + format!("Could not create account: {error}") +} + +fn unsupported_registration_flow_message( + flows: &[matrix_sdk::ruma::api::client::uiaa::AuthFlow], +) -> String { + let supports_registration_token = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::RegistrationToken)) + }); + if supports_registration_token { + return "This homeserver requires a registration token. Robrix does not support token-based registration yet.".to_owned(); + } + + let supports_terms = flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Terms)) + }); + if supports_terms { + return "This homeserver requires an interactive terms-of-service step. Robrix does not support that registration flow yet.".to_owned(); + } + + "This homeserver requires an unsupported registration flow. Please try another homeserver or register with a different client.".to_owned() +} + +async fn clear_persisted_session(user_id: Option<&UserId>) { + let Some(user_id) = user_id else { + return; + }; + + if let Err(e) = persistence::delete_session(user_id).await { + warning!("Failed to delete persisted session for {user_id}: {e}"); + } + + let latest_user_id = persistence::most_recent_user_id().await; + if latest_user_id.as_deref() == Some(user_id) { + if let Err(e) = persistence::delete_latest_user_id().await { + warning!("Failed to delete latest user id for {user_id}: {e}"); + } + } +} + +enum SessionResetAction { + Reauthenticate { message: String }, +} + +async fn reset_runtime_state_for_relogin() { + let sync_service = { SYNC_SERVICE.lock().unwrap().take() }; + if let Some(sync_service) = sync_service { + sync_service.stop().await; + } + + CLIENT.lock().unwrap().take(); + DEFAULT_SSO_CLIENT.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() }); + + if tokio::time::timeout(Duration::from_secs(5), on_clear_appstate.notified()).await.is_err() { + warning!("Timed out waiting for UI-side app state cleanup during re-login reset"); + } +} + +fn is_invalid_token_http_error(error: &matrix_sdk::HttpError) -> bool { + matches!( + error.client_api_error_kind(), + Some(ErrorKind::UnknownToken { .. } | ErrorKind::MissingToken) + ) +} + +fn is_invalid_batch_token_timeline_error(error: &matrix_sdk_ui::timeline::Error) -> bool { + let error_text = error.to_string().to_ascii_lowercase(); + error_text.contains("invalid batch token") + || error_text.contains("must start with 's' or 't'") +} + /// Build a new client. async fn build_client( @@ -117,7 +312,10 @@ async fn build_client( .collect() }; + let inferred_homeserver = infer_homeserver_from_user_id(&cli.user_id); let homeserver_url = cli.homeserver.as_deref() + .filter(|homeserver| !homeserver.trim().is_empty()) + .or(inferred_homeserver.as_deref()) .unwrap_or("https://matrix-client.matrix.org/"); // .unwrap_or("https://matrix.org/"); @@ -179,9 +377,9 @@ async fn login( LoginRequest::LoginByCli | LoginRequest::LoginByPassword(_) => { let (cli, is_add_account) = if let LoginRequest::LoginByPassword(login_by_password) = login_request { let is_add_account = login_by_password.is_add_account; - (Cli::from(login_by_password), is_add_account) + (&Cli::from(login_by_password), is_add_account) } else { - ((*cli).clone(), false) + (cli, false) }; let (client, client_session) = build_client(&cli, app_data_dir()).await?; Cx::post_action(LoginAction::Status { @@ -205,13 +403,71 @@ async fn login( error!("{err_msg}"); enqueue_popup_notification(err_msg, PopupKind::Error, None); } - Ok((client, None, is_add_account, client_session)) } 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); } + finalize_authenticated_client(client, client_session, &cli.user_id, is_add_account).await + } + + LoginRequest::Register(registration) => { + let cli = Cli::from(RegisterAccount { + user_id: registration.user_id.clone(), + password: registration.password.clone(), + homeserver: registration.homeserver.clone(), + }); + let localpart = registration_localpart(®istration.user_id)?; + let (client, client_session) = build_client(&cli, app_data_dir()).await?; + Cx::post_action(LoginAction::Status { + title: "Creating account".into(), + status: format!("Creating account {localpart}..."), + }); + + let auth = client.matrix_auth(); + let initial_request = registration_request(&localpart, ®istration.password, None); + let register_result = match auth.register(initial_request).await { + Ok(response) => Ok(response), + Err(error) => { + if let Some(uiaa_info) = error.as_uiaa_response() { + let supports_dummy = uiaa_info.flows.iter().any(|flow| { + flow.stages + .iter() + .any(|stage| matches!(stage, AuthType::Dummy)) + }); + if supports_dummy { + Cx::post_action(LoginAction::Status { + title: "Completing sign up".into(), + status: "Confirming registration with the homeserver...".into(), + }); + auth.register(registration_request( + &localpart, + ®istration.password, + uiaa_info.session.clone(), + )) + .await + } else { + bail!(unsupported_registration_flow_message(&uiaa_info.flows)); + } + } else { + bail!(registration_uiaa_error_message(&error)); + } + } + }?; + + if !client.matrix_auth().logged_in() { + let err_msg = format!( + "Account {} was created, but the homeserver did not return a login session. Please log in manually.", + register_result.user_id, + ); + enqueue_popup_notification(err_msg.clone(), PopupKind::Error, None); + enqueue_rooms_list_update(RoomsListUpdate::Status { status: err_msg.clone() }); + bail!(err_msg); + } + + finalize_authenticated_client(client, client_session, register_result.user_id.as_str(), false) + .await } LoginRequest::LoginBySSOSuccess(client, client_session, is_add_account) => { @@ -453,6 +709,12 @@ pub enum MatrixRequest { room_id: OwnedRoomId, user_id: OwnedUserId, }, + /// Request to bind or unbind the configured botfather for the given room. + SetRoomBotBinding { + room_id: OwnedRoomId, + bound: bool, + bot_user_id: OwnedUserId, + }, /// Request to join the given room. JoinRoom { room_id: OwnedRoomId, @@ -712,6 +974,7 @@ pub fn submit_async_request(req: MatrixRequest) { /// Details of a login request that get submitted within [`MatrixRequest::Login`]. pub enum LoginRequest{ LoginByPassword(LoginByPassword), + Register(RegisterAccount), LoginBySSOSuccess(Client, ClientSessionPersisted, bool), LoginByCli, HomeserverLoginTypesQuery(String), @@ -726,6 +989,14 @@ pub struct LoginByPassword { pub is_add_account: bool, } +/// Information needed to register a new account on a Matrix homeserver. +#[derive(Clone)] +pub struct RegisterAccount { + pub user_id: String, + pub password: String, + pub homeserver: Option, +} + /// The entry point for the worker task that runs Matrix-related operations. /// @@ -803,13 +1074,8 @@ async fn matrix_worker_task( // Set the target account for switch set_account_switch_target(user_id.clone()); - // Notify UI that switch is starting + // Notify UI that switch is starting (app.rs handles the popup notification) Cx::post_action(AccountSwitchAction::Starting(user_id.clone())); - enqueue_popup_notification( - format!("Switching to {}...", user_id), - PopupKind::Info, - Some(2.0), - ); // Stop the sync service - this will cause the main loop to restart if let Some(sync_service) = get_sync_service() { @@ -854,6 +1120,7 @@ async fn matrix_worker_task( log!("Skipping pagination request for unknown {timeline_kind}"); continue; }; + let client = get_client(); // Spawn a new async task that will make the actual pagination request. let _paginate_task = Handle::current().spawn(async move { @@ -861,12 +1128,45 @@ async fn matrix_worker_task( sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap(); SignalToUI::set_ui_signal(); - let res = if direction == PaginationDirection::Forwards { + let mut res = if direction == PaginationDirection::Forwards { timeline.paginate_forwards(num_events).await } else { timeline.paginate_backwards(num_events).await }; + if direction == PaginationDirection::Backwards + && res + .as_ref() + .err() + .is_some_and(is_invalid_batch_token_timeline_error) + { + warning!( + "Detected an invalid cached batch token for {timeline_kind}; clearing the room event cache and retrying once." + ); + let room_id = timeline_kind.room_id().clone(); + if let Some(room) = client.and_then(|client| client.get_room(&room_id)) { + match room.event_cache().await { + Ok((room_event_cache, _drop_handles)) => { + match room_event_cache.clear().await { + Ok(()) => { + res = timeline.paginate_backwards(num_events).await; + } + Err(clear_error) => { + warning!( + "Failed to clear event cache for room {room_id} after invalid batch token: {clear_error}" + ); + } + } + } + Err(event_cache_error) => { + warning!( + "Failed to access room event cache for room {room_id} after invalid batch token: {event_cache_error}" + ); + } + } + } + } + match res { Ok(fully_paginated) => { log!("Completed {direction} pagination request for {timeline_kind}, hit {} of timeline? {}", @@ -1116,6 +1416,68 @@ async fn matrix_worker_task( }); } + MatrixRequest::SetRoomBotBinding { + room_id, + bound, + bot_user_id, + } => { + let Some(client) = get_client() else { continue }; + let _bot_binding_task = Handle::current().spawn(async move { + let Some(room) = client.get_room(&room_id) else { + let error_message = + format!("Room {room_id} was not found for the bot binding request."); + error!("{error_message}"); + enqueue_popup_notification(error_message, PopupKind::Error, None); + return; + }; + + let membership_result = if bound { + room.invite_user_by_id(&bot_user_id).await + } else { + room.kick_user(&bot_user_id, Some("Robrix app service unbind")).await + }; + + match membership_result { + Ok(()) => { + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: None, + }); + } + Err(error) => { + let membership_exists = + room.get_member_no_sync(&bot_user_id).await.ok().flatten().is_some(); + let should_mark_bound = if bound { membership_exists } else { false }; + + if should_mark_bound != bound { + error!( + "Failed to {} BotFather {bot_user_id} for room {room_id}: {error:?}", + if bound { "invite" } else { "remove" } + ); + enqueue_popup_notification( + format!( + "Failed to {} BotFather {bot_user_id}: {error}", + if bound { "invite" } else { "remove" } + ), + PopupKind::Error, + None, + ); + return; + } + + Cx::post_action(AppStateAction::BotRoomBindingUpdated { + room_id, + bound, + bot_user_id: Some(bot_user_id), + warning: Some(error.to_string()), + }); + } + } + }); + } + MatrixRequest::JoinRoom { room_id } => { let Some(client) = get_client() else { continue }; let _join_room_task = Handle::current().spawn(async move { @@ -1568,15 +1930,18 @@ 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 users = join_all(user_ids.into_iter().map(|user_id| { - let tl = main_timeline.clone(); - async move { - tl.room().get_member_no_sync(&user_id).await - .ok().flatten() + 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()) - } - })).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:?}"); } @@ -1700,7 +2065,6 @@ async fn matrix_worker_task( 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; @@ -2094,6 +2458,7 @@ async fn matrix_worker_task( 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); @@ -2471,11 +2836,17 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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((client, sync_token, session)) => Some((client, sync_token, session)), + match persistence::restore_session(specified_username.clone()).await { + Ok((client, sync_token, session)) => Some((client, sync_token, true, session)), Err(e) => { let status_err = "Could not restore previous user session.\n\nPlease login again."; log!("{status_err} Error: {e:?}"); + clear_persisted_session( + specified_username + .as_deref() + .or(most_recent_user_id.as_deref()), + ) + .await; Cx::post_action(LoginAction::LoginFailure(status_err.to_string())); if let Ok(cli) = &cli_parse_result { @@ -2485,7 +2856,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { homeserver: cli.homeserver.clone(), }); match login(cli, LoginRequest::LoginByCli).await { - Ok((client, sync_token, _is_add_account, session)) => Some((client, sync_token, session)), + Ok((client, sync_token, _is_add_account, session)) => Some((client, sync_token, false, session)), Err(e) => { error!("CLI-based login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure( @@ -2510,9 +2881,8 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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, session) = match initial_client_opt.take() { + let (client, _sync_token, validate_session, session) = match initial_client_opt.take() { Some(login) => login, None => { loop { @@ -2520,7 +2890,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { match login_receiver.recv().await { Some(login_request) => { match login(&cli, login_request).await { - Ok((client, sync_token, _is_add_account, session)) => break (client, sync_token, session), + Ok((client, sync_token, _is_add_account, session)) => break (client, sync_token, false, session), Err(e) => { error!("Login failed: {e:?}"); Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); @@ -2544,6 +2914,24 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } }; + if validate_session { + match client.whoami().await { + Ok(_) => {} + Err(e) if is_invalid_token_http_error(&e) => { + clear_persisted_session(client.user_id()).await; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; + Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.to_string(), + }); + continue 'login_loop; + } + Err(e) => { + warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); + } + } + } + // Deallocate the default SSO client after a successful login. if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { let _ = client_opt.take(); @@ -2577,9 +2965,6 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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(), @@ -2597,6 +2982,9 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } else { format!("Please restart Robrix.\n\nFailed to create Matrix sync service: {e}.") }; + if is_invalid_token_error(&e) { + clear_persisted_session(client.user_id()).await; + } 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 }); @@ -2610,6 +2998,12 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { break 'login_loop (client, sync_service, logged_in_user_id); }; + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + // Listen for session changes, e.g., when the access token becomes invalid. + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); + // 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 @@ -2634,9 +3028,21 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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 { + let reauth_message = loop { tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + break message; + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; + } + } + } result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); match result { Ok(Ok(())) => { // Check if this is due to logout @@ -2666,9 +3072,10 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { error!("BUG: failed to join matrix worker task: {e:?}"); } } - break; + return; } result = &mut room_list_service_task => { + session_change_handler_task.abort(); match result { Ok(Ok(())) => { error!("BUG: room list service loop task ended unexpectedly!"); @@ -2688,9 +3095,10 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { error!("BUG: failed to join room list service loop task: {e:?}"); } } - break; + return; } result = &mut space_service_task => { + session_change_handler_task.abort(); match result { Ok(Ok(())) => { error!("BUG: space service loop task ended unexpectedly!"); @@ -2710,10 +3118,10 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { error!("BUG: failed to join space service loop task: {e:?}"); } } - break; + return; } } - } + }; // Check if we need to restart for an account switch if let Some(switch_user_id) = get_account_switch_target() { @@ -2779,13 +3187,8 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { 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.clone())); - // Notify UI that switch is complete + // Notify UI that switch is complete (app.rs handles the popup notification) Cx::post_action(AccountSwitchAction::Switched(switch_user_id.clone())); - enqueue_popup_notification( - format!("Switched to {}", switch_user_id), - PopupKind::Success, - Some(3.0), - ); // Re-enter the main monitoring loop loop { @@ -2836,6 +3239,16 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { } } } + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + + reset_runtime_state_for_relogin().await; + Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: reauth_message, + }); + initial_client_opt = None; } @@ -3541,7 +3954,10 @@ fn is_invalid_token_error(e: &sync_service::Error) -> bool { /// 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) { +fn handle_session_changes( + client: Client, + session_reset_sender: UnboundedSender, +) -> JoinHandle<()> { let mut receiver = client.subscribe_to_session_changes(); Handle::current().spawn(async move { loop { @@ -3554,6 +3970,11 @@ fn handle_session_changes(client: Client) { }; error!("Session token is no longer valid (soft_logout: {soft_logout}). Prompting re-login."); Cx::post_action(LoginAction::LoginFailure(msg.to_string())); + clear_persisted_session(client.user_id()).await; + let _ = session_reset_sender.send(SessionResetAction::Reauthenticate { + message: msg.to_string(), + }); + break; } Ok(SessionChange::TokensRefreshed) => {} Err(broadcast::error::RecvError::Lagged(n)) => { @@ -3564,7 +3985,7 @@ fn handle_session_changes(client: Client) { } } } - }); + }) } fn handle_sync_service_state_subscriber(mut subscriber: Subscriber) { diff --git a/src/tsp/create_did_modal.rs b/src/tsp/create_did_modal.rs index 361d62023..f51e8bccb 100644 --- a/src/tsp/create_did_modal.rs +++ b/src/tsp/create_did_modal.rs @@ -86,17 +86,14 @@ 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 } } } } @@ -108,7 +105,7 @@ script_mod! { server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } + padding: Inset{top: 3, bottom: 3} empty_text: "p.teaspoon.world", draw_text +: { text_style: REGULAR_TEXT {font_size: 10.0} @@ -150,7 +147,7 @@ script_mod! { did_server_input := RobrixTextInput { width: Fill, height: Fit, flow: Right, // do not wrap - padding: Inset { left: 10, right: 10, top: 5, bottom: 5 } + padding: Inset{top: 3, bottom: 3} 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 1e1709b30..79c477597 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 { left: 10, right: 10, top: 5, bottom: 5 } + padding: Inset{top: 3, bottom: 3} 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 971a4854d..8634c05ee 100644 --- a/src/tsp/sign_anycast_checkbox.rs +++ b/src/tsp/sign_anycast_checkbox.rs @@ -13,10 +13,5 @@ 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 879ae3ded..83d0e6f87 100644 --- a/src/tsp/tsp_settings_screen.rs +++ b/src/tsp/tsp_settings_screen.rs @@ -3,12 +3,15 @@ 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. @@ -40,7 +43,7 @@ script_mod! { current_identity_label := Label { width: Fill, height: Fit flow: Flow.Right{wrap: true}, - margin: Inset{top: 8} + margin: Inset{top: 10} draw_text +: { text_style: MESSAGE_TEXT_STYLE { font_size: 11 }, } @@ -48,13 +51,13 @@ script_mod! { } republish_identity_button := RobrixIconButton { - width: Fit, - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + width: Fit, height: Fit, 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: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT + text: (REPUBLISH_IDENTITY_BUTTON_TEXT) } @@ -108,36 +111,36 @@ script_mod! { spacing: 10 create_did_button := RobrixPositiveIconButton { - width: Fit, - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + width: Fit, height: Fit, padding: 10, margin: Inset{left: 5}, + draw_bg.border_radius: 5.0 draw_icon.svg: (ICON_ADD_USER) - icon_walk: Walk{width: 19, height: Fit, margin: 0} + icon_walk: Walk{width: 21, height: Fit, margin: 0} text: "Create New Identity (DID)" } create_wallet_button := RobrixPositiveIconButton { - width: Fit, - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + width: Fit, height: Fit, 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" - draw_icon +: { - svg: (ICON_IMPORT) - color: (COLOR_PRIMARY) - } - icon_walk: Walk{width: 16, height: 16} + // 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} } } } @@ -378,7 +381,7 @@ impl MatchEvent for TspSettingsScreen { // restore the republish button to its original state. script_apply_eval!(cx, republish_identity_button, { enabled: true, - text: mod.widgets.REPUBLISH_IDENTITY_BUTTON_TEXT, + text: #(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 68bb2c4c0..2c2de8ab4 100644 --- a/src/tsp/wallet_entry/mod.rs +++ b/src/tsp/wallet_entry/mod.rs @@ -18,13 +18,11 @@ 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 @@ -52,11 +50,9 @@ 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), @@ -70,12 +66,10 @@ 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 }, @@ -85,7 +79,6 @@ 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) @@ -94,7 +87,6 @@ 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) @@ -103,7 +95,6 @@ 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 2ab588b454a0bbaae587260bffa3ed06a1f5ec1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 1 Apr 2026 17:20:40 +0800 Subject: [PATCH 048/283] feat: improve app-service bot targeting and in-room feedback - propagate target_user_id through RoomInputBar and SendMessage requests\n- route targeted messages/replies via raw payload with org.octos.target_user_id\n- ensure targeted bot is invited into room before targeted send\n- add App Service panel action to view bound bots from bindings plus room members\n- send App Service status/validation feedback as room notice messages instead of popup notifications --- src/app.rs | 18 +-- src/home/room_screen.rs | 242 ++++++++++++++++++++++++++----------- src/room/room_input_bar.rs | 44 ++++++- src/sliding_sync.rs | 152 ++++++++++++++++++++++- 4 files changed, 374 insertions(+), 82 deletions(-) diff --git a/src/app.rs b/src/app.rs index e2e72e28c..dfb8b7665 100644 --- a/src/app.rs +++ b/src/app.rs @@ -6,7 +6,7 @@ use std::{fs::{File, OpenOptions}, io::Write, sync::Mutex}; use std::{cell::RefCell, collections::HashMap}; use makepad_widgets::*; -use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId}}; +use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, RoomId, UserId, events::room::message::RoomMessageEventContent}}; use serde::{Deserialize, Serialize}; use crate::{ avatar_cache::{self, AvatarCacheEntry, clear_avatar_cache}, home::{ @@ -14,7 +14,7 @@ use crate::{ 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}, rooms_list_header::RoomsListHeaderAction, space_lobby::SpaceLobbyScreenWidgetRefExt, spaces_bar::SpacesBarRef }, join_leave_room_modal::{ JoinLeaveModalKind, JoinLeaveRoomModalAction, JoinLeaveRoomModalWidgetRefExt - }, login::login_screen::LoginAction, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction, LogoutConfirmModalWidgetRefExt}, persistence, profile::{user_profile::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, 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::UserProfile, user_profile_cache::clear_user_profile_cache}, room::{BasicRoomDetails, FetchedRoomAvatar}, shared::{avatar::{AvatarState, AvatarWidgetRefExt}, confirmation_modal::{ConfirmationModalContent, ConfirmationModalWidgetRefExt}, image_viewer::{ImageViewerAction, LoadState}, popup_list::{PopupKind, enqueue_popup_notification}, room_filter_input_bar::RoomFilterAction}, sliding_sync::{DirectMessageRoomAction, MatrixRequest, RemoteDirectorySearchKind, RemoteDirectorySearchResult, TimelineKind, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ VerificationModalAction, VerificationModalWidgetRefExt, } @@ -972,11 +972,6 @@ impl MatchEvent for App { error!("Failed to persist app state after updating BotFather room binding. Error: {e}"); } } - let kind = if warning.is_some() { - PopupKind::Warning - } else { - PopupKind::Success - }; let message = match (*bound, bot_user_id.as_ref(), warning.as_deref()) { (true, Some(bot_user_id), Some(warning)) => { format!("BotFather {bot_user_id} is available for room {room_id}, but inviting it reported a warning: {warning}") @@ -1003,7 +998,14 @@ impl MatchEvent for App { format!("Bound room {room_id} to BotFather.") } }; - enqueue_popup_notification(message, kind, Some(5.0)); + submit_async_request(MatrixRequest::SendMessage { + timeline_kind: TimelineKind::MainRoom { room_id: room_id.clone() }, + message: RoomMessageEventContent::notice_plain(format!("[App Service] {message}")), + replied_to: None, + target_user_id: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); self.ui.redraw(cx); continue; } diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 349d23326..b773f8ecd 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -271,6 +271,38 @@ fn detected_bot_binding_for_members( .then_some(bot_user_id) } +fn is_likely_bot_user_id( + user_id: &UserId, + resolved_parent_bot_user_id: Option<&UserId>, +) -> bool { + if resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) { + return true; + } + + let localpart = user_id.localpart().to_ascii_lowercase(); + localpart == "bot" + || localpart.starts_with("bot_") + || localpart.ends_with("_bot") + || (localpart.ends_with("bot") && localpart.len() > 3) +} + +fn is_likely_bot_member( + room_member: &RoomMember, + resolved_parent_bot_user_id: Option<&UserId>, +) -> bool { + if is_likely_bot_user_id(room_member.user_id(), resolved_parent_bot_user_id) { + return true; + } + + room_member.display_name().is_some_and(|display_name| { + let display_name = display_name.trim().to_ascii_lowercase(); + display_name == "bot" + || display_name.starts_with("bot ") + || display_name.ends_with(" bot") + || display_name.contains(" bot ") + }) +} + script_mod! { use mod.prelude.widgets.* use mod.widgets.* @@ -885,6 +917,15 @@ script_mod! { flow: Right spacing: 8 + view_bound_button := RobrixNeutralIconButton { + width: 156 + height: 46 + padding: 10 + draw_icon.svg: (ICON_SEARCH) + icon_walk: Walk{width: 14, height: 14, margin: Inset{left: -2, right: -1}} + text: "View Bound Bots" + } + unbind_button := RobrixNeutralIconButton { width: 156 height: 46 @@ -1421,16 +1462,24 @@ impl Widget for RoomScreen { let room_props = if let Some(tl) = self.tl_state.as_ref() { let room_id = tl.kind.room_id().clone(); let room_members = tl.room_members.clone(); - let (app_service_enabled, app_service_room_bound) = scope + let (app_service_enabled, app_service_room_bound, bound_bot_user_id) = scope .data .get::() .map(|app_state| { + let app_service_enabled = app_state.bot_settings.enabled; + let app_service_room_bound = self.is_app_service_room_bound(app_state, &room_id); + let bound_bot_user_id = if app_service_enabled && app_service_room_bound { + app_state.bot_settings.bound_bot_user_id(&room_id).map(ToOwned::to_owned) + } else { + None + }; ( - app_state.bot_settings.enabled, - self.is_app_service_room_bound(app_state, &room_id), + app_service_enabled, + app_service_room_bound, + bound_bot_user_id, ) }) - .unwrap_or((false, false)); + .unwrap_or((false, false, None)); RoomScreenProps { room_screen_widget_uid, @@ -1440,6 +1489,7 @@ impl Widget for RoomScreen { room_avatar_url: self.room_avatar_url.clone(), app_service_enabled, app_service_room_bound, + bound_bot_user_id, } } else if let Some(room_name) = &self.room_name_id { // Fallback case: we have a room_name but no tl_state yet @@ -1452,6 +1502,7 @@ impl Widget for RoomScreen { room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, + bound_bot_user_id: None, } } else { // No room selected yet, skip event handling that requires room context @@ -1469,6 +1520,7 @@ impl Widget for RoomScreen { room_avatar_url: None, app_service_enabled: false, app_service_room_bound: false, + bound_bot_user_id: None, } }; let mut room_scope = Scope::with_props(&room_props); @@ -1499,27 +1551,21 @@ impl Widget for RoomScreen { AppServicePanelAction::OpenCreateBotModal => { if let Some(app_state) = scope.data.get::() { if !app_state.bot_settings.enabled { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Enable App Service before creating bots in a room.", - PopupKind::Warning, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else if !room_props.app_service_room_bound { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bind BotFather to this room before creating a bot.", - PopupKind::Warning, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else { self.open_create_bot_modal(cx); } } else { - enqueue_popup_notification( + self.send_app_service_feedback_message( "App state is unavailable, so bot creation is temporarily unavailable.", - PopupKind::Error, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } @@ -1528,27 +1574,21 @@ impl Widget for RoomScreen { AppServicePanelAction::OpenDeleteBotModal => { if let Some(app_state) = scope.data.get::() { if !app_state.bot_settings.enabled { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Enable App Service before deleting bots in a room.", - PopupKind::Warning, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else if !room_props.app_service_room_bound { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bind BotFather to this room before deleting a bot.", - PopupKind::Warning, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } else { self.open_delete_bot_modal(cx); } } else { - enqueue_popup_notification( + self.send_app_service_feedback_message( "App state is unavailable, so bot deletion is temporarily unavailable.", - PopupKind::Error, - Some(4.0), ); self.set_app_service_actions_visible(cx, false); } @@ -1576,13 +1616,73 @@ impl Widget for RoomScreen { } return false; } + AppServicePanelAction::ShowBoundBots => { + let room_id = room_props.room_name_id.room_id(); + let own_user_id = current_user_id(); + let mut bound_bots = Vec::::new(); + let mut push_unique_bot = |bot_user_id: OwnedUserId| { + if !bound_bots.iter().any(|existing| existing == &bot_user_id) { + bound_bots.push(bot_user_id); + } + }; + + if let Some(bound_bot_user_id) = room_props.bound_bot_user_id.as_ref() { + push_unique_bot(bound_bot_user_id.clone()); + } + + let mut resolved_parent_bot_user_id: Option = None; + if let Some(app_state) = scope.data.get::() { + for room_binding in &app_state.bot_settings.room_bindings { + if &room_binding.room_id == room_id { + push_unique_bot(room_binding.bot_user_id.clone()); + } + } + + resolved_parent_bot_user_id = app_state + .bot_settings + .resolved_bot_user_id_for_room(room_id, current_user_id().as_deref()) + .ok(); + if let Some(bot_user_id) = resolved_parent_bot_user_id.as_ref() { + push_unique_bot(bot_user_id.clone()); + } + } + + if let Some(room_members) = room_props.room_members.as_ref() { + for room_member in room_members.iter() { + if own_user_id + .as_deref() + .is_some_and(|own_user_id| own_user_id == room_member.user_id()) + { + continue; + } + if is_likely_bot_member( + room_member, + resolved_parent_bot_user_id.as_deref(), + ) { + push_unique_bot(room_member.user_id().to_owned()); + } + } + } + + if bound_bots.is_empty() { + self.send_app_service_feedback_message( + "No bots are currently bound to this room.", + ); + } else { + let mut message = String::from("Bots bound to this room:"); + for bot_user_id in &bound_bots { + message.push('\n'); + message.push_str(bot_user_id.as_str()); + } + self.send_app_service_feedback_message(message); + } + return false; + } AppServicePanelAction::Unbind => { if let Some(app_state) = scope.data.get::() { if !room_props.app_service_room_bound { - enqueue_popup_notification( + self.send_app_service_feedback_message( "This room is not currently bound to BotFather.", - PopupKind::Warning, - Some(4.0), ); } else { match app_state @@ -1598,28 +1698,22 @@ impl Widget for RoomScreen { bound: false, bot_user_id: bot_user_id.clone(), }); - enqueue_popup_notification( + self.send_app_service_feedback_message( format!( "Removing BotFather {bot_user_id} from this room..." ), - PopupKind::Info, - Some(4.0), ); } Err(error) => { - enqueue_popup_notification( + self.send_app_service_feedback_message( error, - PopupKind::Error, - Some(4.0), ); } } } } else { - enqueue_popup_notification( + self.send_app_service_feedback_message( "App state is unavailable, so BotFather could not be removed from this room.", - PopupKind::Error, - Some(4.0), ); } self.set_app_service_actions_visible(cx, false); @@ -1635,10 +1729,8 @@ impl Widget for RoomScreen { } Some(CreateBotModalAction::Submit(request)) => { let Some(app_state) = scope.data.get::() else { - enqueue_popup_notification( + self.send_app_service_feedback_message( "App state is unavailable, so the create-bot command was not sent.", - PopupKind::Error, - Some(4.0), ); self.close_create_bot_modal(cx); return false; @@ -1662,10 +1754,8 @@ impl Widget for RoomScreen { } Some(DeleteBotModalAction::Submit(request)) => { let Some(app_state) = scope.data.get::() else { - enqueue_popup_notification( + self.send_app_service_feedback_message( "App state is unavailable, so the delete-bot command was not sent.", - PopupKind::Error, - Some(4.0), ); self.close_delete_bot_modal(cx); return false; @@ -1682,22 +1772,16 @@ impl Widget for RoomScreen { .cast() { if room_props.timeline_kind.thread_root_event_id().is_some() { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bot commands are only supported in the main room timeline.", - PopupKind::Warning, - Some(4.0), ); } else if !room_props.app_service_enabled { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Enable App Service in Settings before using /bot.", - PopupKind::Warning, - Some(4.0), ); } else if !room_props.app_service_room_bound { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bind BotFather to this room before using /bot.", - PopupKind::Warning, - Some(4.0), ); } else { self.toggle_app_service_actions(cx); @@ -2042,6 +2126,21 @@ impl RoomScreen { app_state.bot_settings.is_room_bound(room_id) } + fn send_app_service_feedback_message(&self, message: impl Into) { + let Some(room_id) = self.room_id().cloned() else { + return; + }; + let message = format!("[App Service] {}", message.into()); + submit_async_request(MatrixRequest::SendMessage { + timeline_kind: TimelineKind::MainRoom { room_id }, + message: RoomMessageEventContent::notice_plain(message), + replied_to: None, + target_user_id: None, + #[cfg(feature = "tsp")] + sign_with_tsp: false, + }); + } + fn send_botfather_command( &mut self, cx: &mut Cx, @@ -2053,10 +2152,8 @@ impl RoomScreen { return false; }; if timeline_kind.thread_root_event_id().is_some() { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bot commands are only supported in the main room timeline.", - PopupKind::Warning, - Some(4.0), ); return false; } @@ -2065,18 +2162,14 @@ impl RoomScreen { return false; }; if !app_state.bot_settings.enabled { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Enable App Service before using BotFather commands in a room.", - PopupKind::Warning, - Some(4.0), ); return false; } if !self.is_app_service_room_bound(app_state, &room_id) { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bind BotFather to this room before using BotFather commands.", - PopupKind::Warning, - Some(4.0), ); return false; } @@ -2085,11 +2178,15 @@ impl RoomScreen { timeline_kind, message: RoomMessageEventContent::text_plain(command), replied_to: None, + target_user_id: app_state + .bot_settings + .bound_bot_user_id(room_id.as_ref()) + .map(ToOwned::to_owned), #[cfg(feature = "tsp")] sign_with_tsp: false, }); - enqueue_popup_notification(success_message.to_string(), PopupKind::Info, Some(4.0)); + self.send_app_service_feedback_message(success_message.to_string()); self.set_app_service_actions_visible(cx, false); true } @@ -2106,10 +2203,8 @@ impl RoomScreen { return; }; if timeline_kind.thread_root_event_id().is_some() { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bot creation commands are only supported in the main room timeline.", - PopupKind::Warning, - Some(4.0), ); return; } @@ -2118,18 +2213,14 @@ impl RoomScreen { return; }; if !app_state.bot_settings.enabled { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Enable App Service before creating bots in a room.", - PopupKind::Warning, - Some(4.0), ); return; } if !self.is_app_service_room_bound(app_state, &room_id) { - enqueue_popup_notification( + self.send_app_service_feedback_message( "Bind BotFather to this room before creating a bot.", - PopupKind::Warning, - Some(4.0), ); return; } @@ -2155,7 +2246,7 @@ impl RoomScreen { match resolve_delete_bot_user_id(user_id_or_localpart, current_user_id().as_deref()) { Ok(user_id) => user_id, Err(error) => { - enqueue_popup_notification(error, PopupKind::Error, Some(4.0)); + self.send_app_service_feedback_message(error); return; } }; @@ -3673,6 +3764,7 @@ pub struct RoomScreenProps { pub room_avatar_url: Option, pub app_service_enabled: bool, pub app_service_room_bound: bool, + pub bound_bot_user_id: Option, } @@ -5690,6 +5782,7 @@ pub enum AppServicePanelAction { OpenDeleteBotModal, SendListBots, SendBotHelp, + ShowBoundBots, Unbind, #[default] None, @@ -5772,6 +5865,17 @@ impl Widget for AppServicePanel { ); } + if self + .view + .button(cx, ids!(keyboard.third_row.view_bound_button)) + .clicked(actions) + { + cx.widget_action( + room_screen_props.room_screen_widget_uid, + AppServicePanelAction::ShowBoundBots, + ); + } + if self .view .button(cx, ids!(keyboard.third_row.unbind_button)) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index bf4563d65..9850752e2 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -19,7 +19,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 ruma::{events::room::message::{LocationMessageEventContent, MessageType, ReplyWithinThread, RoomMessageEventContent}, OwnedRoomId, OwnedUserId}; 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}; script_mod! { @@ -169,6 +169,8 @@ 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 most recently selected explicit bot target for this room. + #[rust] active_target_user_id: Option, } impl Widget for RoomInputBar { @@ -212,6 +214,23 @@ impl Widget for RoomInputBar { } impl RoomInputBar { + fn resolve_target_user_id( + &mut self, + explicit_target_user_id: Option, + reply_target_user_id: Option, + fallback_target_user_id: Option, + ) -> Option { + if let Some(explicit_target_user_id) = explicit_target_user_id { + self.active_target_user_id = Some(explicit_target_user_id.clone()); + Some(explicit_target_user_id) + } else if let Some(reply_target_user_id) = reply_target_user_id { + self.active_target_user_id = Some(reply_target_user_id.clone()); + Some(reply_target_user_id) + } else { + self.active_target_user_id.clone().or(fallback_target_user_id) + } + } + fn handle_actions( &mut self, cx: &mut Cx, @@ -255,6 +274,10 @@ impl RoomInputBar { LocationMessageEventContent::new(geo_uri.clone(), geo_uri) ) ); + let reply_target_user_id = self + .replying_to + .as_ref() + .map(|(event_tl_item, _emb)| event_tl_item.sender().to_owned()); let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| event_tl_item.event_id().map(|event_id| { let enforce_thread = if room_screen_props.timeline_kind.thread_root_event_id().is_some() { @@ -279,6 +302,11 @@ impl RoomInputBar { timeline_kind: room_screen_props.timeline_kind.clone(), message, replied_to, + target_user_id: self.resolve_target_user_id( + None, + reply_target_user_id, + room_screen_props.bound_bot_user_id.clone(), + ), #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); @@ -306,6 +334,10 @@ impl RoomInputBar { self.redraw(cx); return; } + let reply_target_user_id = self + .replying_to + .as_ref() + .map(|(event_tl_item, _emb)| event_tl_item.sender().to_owned()); let message = mentionable_text_input.create_message_with_mentions(&entered_text); let replied_to = self.replying_to.take().and_then(|(event_tl_item, _emb)| event_tl_item.event_id().map(|event_id| { @@ -331,6 +363,11 @@ impl RoomInputBar { timeline_kind: room_screen_props.timeline_kind.clone(), message, replied_to, + target_user_id: self.resolve_target_user_id( + None, + reply_target_user_id, + room_screen_props.bound_bot_user_id.clone(), + ), #[cfg(feature = "tsp")] sign_with_tsp: self.is_tsp_signing_enabled(cx), }); @@ -664,6 +701,7 @@ impl RoomInputBarRef { RoomInputBarState { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), + active_target_user_id: inner.active_target_user_id.clone(), editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), text_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input.text_input)).as_text_input().save_state(), } @@ -683,6 +721,7 @@ impl RoomInputBarRef { was_replying_preview_visible, text_input_state, replying_to, + active_target_user_id, editing_pane_state, } = saved_state; @@ -704,6 +743,7 @@ impl RoomInputBarRef { inner.clear_replying_to(cx); } inner.was_replying_preview_visible = was_replying_preview_visible; + inner.active_target_user_id = active_target_user_id; // 3. Restore the state of the editing pane. if let Some(editing_pane_state) = editing_pane_state { @@ -732,6 +772,8 @@ pub struct RoomInputBarState { text_input_state: TextInputState, /// The event that the user is currently replying to, if any. replying_to: Option<(EventTimelineItem, EmbeddedEvent)>, + /// The most recently selected explicit bot target for this room. + active_target_user_id: Option, /// The state of the `EditingPane`, if any message was being edited. editing_pane_state: Option, } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 29649c3fb..604c2e2d0 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -839,6 +839,7 @@ pub enum MatrixRequest { timeline_kind: TimelineKind, message: RoomMessageEventContent, replied_to: Option, + target_user_id: Option, #[cfg(feature = "tsp")] sign_with_tsp: bool, }, @@ -939,6 +940,75 @@ pub enum MatrixRequest { }, } +fn add_octos_target_user_id( + mut content: serde_json::Value, + target_user_id: &UserId, +) -> serde_json::Value { + if let Some(content_obj) = content.as_object_mut() { + content_obj.insert( + "org.octos.target_user_id".to_string(), + serde_json::Value::String(target_user_id.to_string()), + ); + } + content +} + +async fn ensure_target_user_joined_room( + room: &Room, + target_user_id: &UserId, +) -> Result<()> { + let already_present = room + .get_member_no_sync(target_user_id) + .await + .ok() + .flatten() + .is_some(); + if already_present { + return Ok(()); + } + + room.invite_user_by_id(target_user_id).await?; + + for _attempt in 0..20 { + let joined = room + .get_member_no_sync(target_user_id) + .await + .ok() + .flatten() + .is_some(); + if joined { + return Ok(()); + } + + tokio::time::sleep(Duration::from_millis(250)).await; + } + + Ok(()) +} + +#[cfg(test)] +mod matrix_request_tests { + use super::*; + + #[test] + fn should_add_octos_target_user_id_to_message_content() { + let target_user_id = OwnedUserId::try_from("@bot_weather:example.com").unwrap(); + let content = serde_json::json!({ + "msgtype": "m.text", + "body": "hello", + }); + + let content = add_octos_target_user_id(content, target_user_id.as_ref()); + + assert_eq!( + content + .get("org.octos.target_user_id") + .and_then(|value| value.as_str()), + Some("@bot_weather:example.com") + ); + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub enum RemoteDirectorySearchKind { People, @@ -2152,6 +2222,7 @@ async fn matrix_worker_task( timeline_kind, message, replied_to, + target_user_id, #[cfg(feature = "tsp")] sign_with_tsp, } => { @@ -2225,11 +2296,84 @@ async fn matrix_worker_task( return; } }; - match timeline.send(reply_content.into()).await { - Ok(_send_handle) => log!("Sent reply message to {timeline_kind}."), + + if let Some(target_user_id) = target_user_id.as_ref() { + if let Err(_e) = ensure_target_user_joined_room( + timeline.room(), + target_user_id.as_ref(), + ) + .await + { + error!("Failed to ensure targeted bot {target_user_id} joined {timeline_kind}: {_e:?}"); + enqueue_popup_notification( + format!("Failed to invite {target_user_id} into this room: {_e}"), + PopupKind::Error, + None, + ); + return; + } + + let raw_content = match serde_json::to_value(&reply_content) { + Ok(content) => add_octos_target_user_id(content, target_user_id.as_ref()), + Err(_e) => { + error!("Failed to serialize reply content for {timeline_kind}: {_e:?}"); + enqueue_popup_notification( + format!("Failed to send reply: {_e}"), + PopupKind::Error, + None, + ); + return; + } + }; + match timeline.room().send_raw("m.room.message", raw_content).await { + Ok(_response) => log!("Sent targeted reply message to {timeline_kind}."), + Err(_e) => { + error!("Failed to send targeted reply message to {timeline_kind}: {_e:?}"); + enqueue_popup_notification(format!("Failed to send reply: {_e}"), PopupKind::Error, None); + } + } + } else { + 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 if let Some(target_user_id) = target_user_id.as_ref() { + if let Err(_e) = ensure_target_user_joined_room( + timeline.room(), + target_user_id.as_ref(), + ) + .await + { + error!("Failed to ensure targeted bot {target_user_id} joined {timeline_kind}: {_e:?}"); + enqueue_popup_notification( + format!("Failed to invite {target_user_id} into this room: {_e}"), + PopupKind::Error, + None, + ); + return; + } + + let raw_content = match serde_json::to_value(&message) { + Ok(content) => add_octos_target_user_id(content, target_user_id.as_ref()), Err(_e) => { - error!("Failed to send reply message to {timeline_kind}: {_e:?}"); - enqueue_popup_notification(format!("Failed to send reply: {_e}"), PopupKind::Error, None); + error!("Failed to serialize message content for {timeline_kind}: {_e:?}"); + enqueue_popup_notification( + format!("Failed to send message: {_e}"), + PopupKind::Error, + None, + ); + return; + } + }; + match timeline.room().send_raw("m.room.message", raw_content).await { + Ok(_response) => log!("Sent targeted message to {timeline_kind}."), + Err(_e) => { + error!("Failed to send targeted message to {timeline_kind}: {_e:?}"); + enqueue_popup_notification(format!("Failed to send message: {_e}"), PopupKind::Error, None); } } } else { From 73a27561e92fa4774d6e1cdcf2cf839ba8f2b5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 1 Apr 2026 17:54:04 +0800 Subject: [PATCH 049/283] fix(search): improve people lookup and fallback empty local filters --- src/home/rooms_list.rs | 14 +++++++++++++- src/home/spaces_bar.rs | 5 +++++ src/sliding_sync.rs | 44 ++++++++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/home/rooms_list.rs b/src/home/rooms_list.rs index 83e706223..444e0bd31 100644 --- a/src/home/rooms_list.rs +++ b/src/home/rooms_list.rs @@ -1013,7 +1013,19 @@ impl RoomsList { /// If `false`, the scroll position is preserved, unless it exceeds the new list length, /// in which case the logic in `draw_walk()` will limit it to the max valid index. fn update_displayed_rooms(&mut self, cx: &mut Cx, reset_scroll: bool) { - let (invited, regular, direct) = self.generate_displayed_rooms(); + let (mut invited, mut regular, mut direct) = self.generate_displayed_rooms(); + if self.display_filter.is_some() + && invited.is_empty() + && regular.is_empty() + && direct.is_empty() + { + self.display_filter = RoomDisplayFilter::default(); + self.sort_fn = None; + let (fallback_invited, fallback_regular, fallback_direct) = self.generate_displayed_rooms(); + invited = fallback_invited; + regular = fallback_regular; + direct = fallback_direct; + } self.displayed_invited_rooms = invited; self.displayed_regular_rooms = regular; self.displayed_direct_rooms = direct; diff --git a/src/home/spaces_bar.rs b/src/home/spaces_bar.rs index 75b03765d..b242ebf81 100644 --- a/src/home/spaces_bar.rs +++ b/src/home/spaces_bar.rs @@ -846,6 +846,11 @@ impl SpacesBar { } else { filtered_spaces_iter.map(|(space_id, _)| space_id.clone()).collect() }; + if self.displayed_spaces.is_empty() { + self.is_filtered = false; + self.display_filter = RoomDisplayFilter::default(); + self.displayed_spaces = self.all_joined_spaces.keys().cloned().collect(); + } portal_list.set_first_id_and_scroll(0, 0.0); self.redraw(cx); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 29649c3fb..a1f880686 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1493,6 +1493,7 @@ async fn matrix_worker_task( let _search_task = Handle::current().spawn(async move { let query = query.trim().to_owned(); let action_kind = kind.clone(); + log!("Remote directory search request: kind={kind:?}, query=\"{query}\", limit={limit}"); if query.is_empty() { Cx::post_action(RoomFilterRemoteSearchAction::Results { query, @@ -1504,19 +1505,42 @@ async fn matrix_worker_task( let result = match &kind { RemoteDirectorySearchKind::People => { - client.search_users(&query, limit).await - .map(|response| { - response.results.into_iter() - .map(|user| { - RemoteDirectorySearchResult::User(UserProfile { + let mut users = Vec::new(); + let mut seen_user_ids = HashSet::new(); + + if let Ok(user_id) = UserId::parse(&query).map(|u| u.to_owned()) { + if let Ok(response) = client.account().fetch_user_profile_of(&user_id).await { + if seen_user_ids.insert(user_id.clone()) { + users.push(RemoteDirectorySearchResult::User(UserProfile { + username: response.get_static::().ok().flatten(), + user_id, + avatar_state: response.get_static::() + .ok() + .map_or(AvatarState::Unknown, AvatarState::Known), + })); + } + } + } + + match client.search_users(&query, limit).await { + Ok(response) => { + for user in response.results.into_iter() { + if seen_user_ids.insert(user.user_id.clone()) { + users.push(RemoteDirectorySearchResult::User(UserProfile { username: user.display_name, user_id: user.user_id, avatar_state: AvatarState::Known(user.avatar_url), - }) - }) - .collect::>() - }) - .map_err(|e| e.to_string()) + })); + } + if users.len() >= limit as usize { + break; + } + } + Ok(users) + } + Err(_e) if !users.is_empty() => Ok(users), + Err(e) => Err(e.to_string()), + } } RemoteDirectorySearchKind::Rooms | RemoteDirectorySearchKind::Spaces => { let mut filter = PublicRoomsFilter::new(); From 3024cb114ea7253a8a1d082ba5b8174b9b80c6b7 Mon Sep 17 00:00:00 2001 From: alanpoon Date: Wed, 1 Apr 2026 20:27:24 +0800 Subject: [PATCH 050/283] Fix navigation issue when loginAction::CancelAddAccount --- src/app.rs | 12 +++ src/login/login_screen.rs | 19 +++- src/settings/account_settings.rs | 2 +- src/sliding_sync.rs | 170 ++++++++++++++++++++----------- 4 files changed, 143 insertions(+), 60 deletions(-) diff --git a/src/app.rs b/src/app.rs index 2c0707fc4..5c1ca4148 100644 --- a/src/app.rs +++ b/src/app.rs @@ -314,6 +314,15 @@ impl MatchEvent for App { continue; } + // Handle cancellation of adding a new account - go back to previous screen + if let Some(LoginAction::CancelAddAccount) = action.downcast_ref() { + log!("Received LoginAction::CancelAddAccount, hiding login view."); + self.app_state.adding_account = false; + self.ui.view(cx, ids!(login_screen_view)).set_visible(cx, false); + self.ui.redraw(cx); + continue; + } + // Handle account switch actions match action.downcast_ref() { Some(AccountSwitchAction::Starting(user_id)) => { @@ -323,6 +332,9 @@ impl MatchEvent for App { self.app_state.selected_room = None; // Clear saved dock state so tabs will be closed self.app_state.saved_dock_state_home = Default::default(); + // Reset navigation to Home tab + self.app_state.selected_tab = SelectedTab::Home; + cx.action(NavigationBarAction::TabSelected(SelectedTab::Home)); self.ui.redraw(cx); continue; } diff --git a/src/login/login_screen.rs b/src/login/login_screen.rs index 29070debd..097d2bb18 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, LoginByPassword, LoginRequest, MatrixRequest}; +use crate::sliding_sync::{submit_async_request, AccountSwitchAction, LoginByPassword, LoginRequest, MatrixRequest}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -482,6 +482,23 @@ impl MatchEvent for LoginScreen { } _ => { } } + + // Handle account switch actions - close modal when switch completes or fails + match action.downcast_ref() { + Some(AccountSwitchAction::Switched(_)) => { + login_status_modal.close(cx); + self.redraw(cx); + } + Some(AccountSwitchAction::Failed(error)) => { + login_status_modal_inner.set_title(cx, "Account Switch Failed"); + login_status_modal_inner.set_status(cx, error); + let login_status_modal_button = login_status_modal_inner.button_ref(cx); + login_status_modal_button.set_text(cx, "Okay"); + login_status_modal_button.set_enabled(cx, true); + self.redraw(cx); + } + _ => { } + } } // If the Login SSO screen's "cancel" button was clicked, send a http request to gracefully shutdown the SSO server diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index fc48794eb..f003808d5 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -312,7 +312,7 @@ script_mod! { manage_account_button := RobrixIconButton { height: mod.widgets.SETTINGS_BUTTON_HEIGHT, - padding: Inset{left: 12, right: 15} + padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_EXTERNAL_LINK) icon_walk: Walk{width: 16, height: 16} diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 9de350490..395e73338 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -1067,8 +1067,6 @@ async fn matrix_worker_task( } MatrixRequest::SwitchAccount { user_id } => { - log!("Received MatrixRequest::SwitchAccount for {}", user_id); - // Check if the account exists in AccountManager if account_manager::get_client_for_user(&user_id).is_some() { // Set the target account for switch @@ -1079,7 +1077,6 @@ async fn matrix_worker_task( // Stop the sync service - this will cause the main loop to restart if let Some(sync_service) = get_sync_service() { - log!("Stopping sync service for account switch"); sync_service.stop().await; } @@ -2528,7 +2525,6 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2655,8 +2651,13 @@ static SYNC_SERVICE: Mutex>> = Mutex::new(None); /// Contains the user_id to switch to, if any. static ACCOUNT_SWITCH_TARGET: Mutex> = Mutex::new(None); -/// Check if an account switch is pending. -fn get_account_switch_target() -> Option { +/// Check if an account switch is pending (non-consuming peek). +fn is_account_switch_pending() -> bool { + ACCOUNT_SWITCH_TARGET.lock().ok().map(|g| g.is_some()).unwrap_or(false) +} + +/// Take the account switch target, consuming it. Only call when ready to perform the switch. +fn take_account_switch_target() -> Option { ACCOUNT_SWITCH_TARGET.lock().ok()?.take() } @@ -2667,6 +2668,14 @@ fn set_account_switch_target(user_id: OwnedUserId) { } } +/// Clear the account switch target without taking it. +#[allow(dead_code)] +fn clear_account_switch_target() { + if let Ok(mut guard) = ACCOUNT_SWITCH_TARGET.lock() { + *guard = None; + } +} + /// Get a reference to the current sync service, if available. pub fn get_sync_service() -> Option> { @@ -3028,12 +3037,12 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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. - let reauth_message = loop { + let reauth_message: Option = loop { tokio::select! { session_reset = session_reset_receiver.recv() => { match session_reset { Some(SessionResetAction::Reauthenticate { message }) => { - break message; + break Some(message); } None => { warning!("Session reset receiver closed unexpectedly."); @@ -3045,17 +3054,21 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { session_change_handler_task.abort(); match result { Ok(Ok(())) => { - // Check if this is due to logout + // Check if this is due to logout or account switch if is_logout_in_progress() { log!("matrix worker task ended due to logout"); + } else if is_account_switch_pending() { + log!("matrix worker task ended due to account switch"); } else { error!("BUG: matrix worker task ended unexpectedly!"); } } Ok(Err(e)) => { - // Check if this is due to logout + // Check if this is due to logout or account switch if is_logout_in_progress() { log!("matrix worker task ended with error due to logout: {e:?}"); + } else if is_account_switch_pending() { + log!("matrix worker task ended with error due to account switch: {e:?}"); } else { error!("Error: matrix worker task ended:\n\t{e:?}"); rooms_list::enqueue_rooms_list_update(RoomsListUpdate::Status { @@ -3072,61 +3085,71 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { error!("BUG: failed to join matrix worker task: {e:?}"); } } - return; + break None; } result = &mut room_list_service_task => { session_change_handler_task.abort(); match result { Ok(Ok(())) => { - error!("BUG: room list service loop task ended unexpectedly!"); + if is_logout_in_progress() || is_account_switch_pending() { + log!("room list service loop task ended due to logout/account switch"); + } else { + 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, - ); + if !is_logout_in_progress() && !is_account_switch_pending() { + 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:?}"); } } - return; + break None; } result = &mut space_service_task => { session_change_handler_task.abort(); match result { Ok(Ok(())) => { - error!("BUG: space service loop task ended unexpectedly!"); + if is_logout_in_progress() || is_account_switch_pending() { + log!("space service loop task ended due to logout/account switch"); + } else { + 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, - ); + if !is_logout_in_progress() && !is_account_switch_pending() { + 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:?}"); } } - return; + break None; } } }; - // Check if we need to restart for an account switch - if let Some(switch_user_id) = get_account_switch_target() { - log!("Account switch detected, restarting with user: {}", switch_user_id); - + // Check if we need to restart for an account switch (loop to handle consecutive switches) + while let Some(switch_user_id) = take_account_switch_target() { // Clear all backend state CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).take(); @@ -3146,8 +3169,6 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // Restore session for the switched account match persistence::restore_session(Some(switch_user_id.clone())).await { Ok((client, _sync_token, _session)) => { - log!("Successfully restored session for {}", switch_user_id); - // Store the client CLIENT.lock().unwrap_or_else(|e| e.into_inner()).replace(client.clone()); @@ -3163,7 +3184,7 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { { Ok(ss) => ss, Err(e) => { - error!("Failed to create SyncService after account switch: {e:?}"); + error!("Failed to create SyncService: {e:?}"); Cx::post_action(AccountSwitchAction::Failed(format!("Failed to create sync service: {e}"))); return; } @@ -3183,6 +3204,12 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).replace(sender); let (login_sender, _login_receiver) = tokio::sync::mpsc::channel(1); + // Set up session change handler for the switched account + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); + let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); 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.clone())); @@ -3193,19 +3220,34 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // Re-enter the main monitoring loop loop { tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + error!("Session reset during account switch: {}", message); + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + Cx::post_action(AccountSwitchAction::Failed(message)); + break; + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; + } + } + } result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); match result { Ok(Ok(())) => { - if is_logout_in_progress() { - log!("matrix worker task ended due to logout"); - } else if get_account_switch_target().is_some() { - // Another account switch requested, will handle after loop - } else { + if !is_logout_in_progress() && !is_account_switch_pending() { error!("BUG: matrix worker task ended unexpectedly!"); } } Ok(Err(e)) => { - error!("Error: matrix worker task ended:\n\t{e:?}"); + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Error: matrix worker task ended:\n\t{e:?}"); + } } Err(e) => { error!("BUG: failed to join matrix worker task: {e:?}"); @@ -3214,19 +3256,26 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { break; } result = &mut room_list_service_task => { + session_change_handler_task.abort(); if let Err(e) = result { - error!("room list service task error: {e:?}"); + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Room list service task error: {e:?}"); + } } break; } result = &mut space_service_task => { + session_change_handler_task.abort(); if let Err(e) = result { - error!("space service task error: {e:?}"); + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Space service task error: {e:?}"); + } } break; } } } + // After inner loop breaks, outer while loop will check for another pending account switch } Err(e) => { error!("Failed to restore session for account switch: {e:?}"); @@ -3236,19 +3285,24 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { PopupKind::Error, None, ); + // Don't loop back - a failed switch shouldn't keep trying + break; } } } - session_change_handler_task.abort(); - room_list_service_task.abort(); - space_service_task.abort(); - - reset_runtime_state_for_relogin().await; - Cx::post_action(LoginAction::LoginFailure(reauth_message.clone())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: reauth_message, - }); - initial_client_opt = None; + + // Only run reauth cleanup if we got a reauth message (not account switch or logout) + if let Some(reauth_msg) = reauth_message { + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + + reset_runtime_state_for_relogin().await; + Cx::post_action(LoginAction::LoginFailure(reauth_msg.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: reauth_msg, + }); + } } From 75e37f9e308071bbafc10fa1f20a22ed3405d8c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 1 Apr 2026 21:31:25 +0800 Subject: [PATCH 051/283] Refine room input bar quick actions and emoji picker - Move location trigger into expandable quick action card - Show send button only when input has content - Add persistent more-actions button and themed styling - Add emoji picker button with preset emojis and inline insertion --- src/room/room_input_bar.rs | 235 ++++++++++++++++++++++++++++++------- 1 file changed, 194 insertions(+), 41 deletions(-) diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index bf4563d65..345b13a54 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -5,7 +5,7 @@ //! The widgets included in the RoomInputBar are: //! * a preview of the message the user is replying to. //! * the location preview (which allows you to send your current location to the room), -//! and a button to show the location preview. +//! and a location card to show the location preview. //! * If TSP is enabled, a checkbox to enable TSP signing for the outgoing message. //! * A MentionableTextInput, which allows the user to type a message //! and mention other users via the `@` key. @@ -28,6 +28,28 @@ script_mod! { mod.widgets.ICO_LOCATION_PERSON = crate_resource("self://resources/icons/location-person.svg") + mod.widgets.ICO_MENU = crate_resource("self://resources/icons/menu.svg") + + mod.widgets.RoomEmojiButton = mod.widgets.RobrixIconButton { + spacing: 0 + text: "" + margin: 0 + padding: Inset{left: 8, right: 8, top: 6, bottom: 6} + icon_walk: Walk{width: 0, height: 0} + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 15.0 } + } + draw_bg +: { + color: (COLOR_PRIMARY) + color_hover: #F4F7FC + color_down: #E8EEF8 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + } mod.widgets.RoomInputBar = set_type_default() do #(RoomInputBar::register_widget(vm)) { @@ -74,15 +96,17 @@ script_mod! { input_bar := View { width: Fill, height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} - flow: Right - // Bottom-align everything to ensure that buttons always stick to the bottom - // even when the mentionable_text_input box is very tall. - align: Align{y: 1.0}, + flow: Down padding: 6, - - location_button := RobrixIconButton { - margin: 4 - spacing: 0, + spacing: 4 + + location_card_button := RobrixIconButton { + visible: false + width: 230 + align: Align{x: 0.0, y: 0.5} + margin: Inset{top: 1, bottom: 1} + padding: Inset{left: 10, right: 10, top: 8, bottom: 8} + spacing: 8 draw_icon +: { svg: (mod.widgets.ICO_LOCATION_PERSON) color: (COLOR_ACTIVE_PRIMARY_DARKER) @@ -91,43 +115,110 @@ script_mod! { color: (COLOR_BG_PREVIEW) color_hover: #E0E8F0 color_down: #D0D8E8 + border_size: 1.0 + border_color: (COLOR_SECONDARY) + } + draw_text +: { + color: (COLOR_TEXT) + color_hover: (COLOR_TEXT) + color_down: (COLOR_TEXT) + text_style: MESSAGE_TEXT_STYLE { font_size: 10.5 } } - icon_walk: Walk{width: 23, height: 23, margin: Inset{bottom: -1}} - text: "", + icon_walk: Walk{width: 20, height: 20} + text: "Share your current location", } - // A checkbox that enables TSP signing for the outgoing message. - // If TSP is not enabled, this will be an empty invisible view. - tsp_sign_checkbox := TspSignAnycastCheckbox { - margin: Inset{bottom: 9, left: 6, right: 0} + emoji_picker_popup := View { + visible: false + width: Fit + height: Fit + flow: Right{wrap: true} + align: Align{x: 0.0, y: 0.5} + margin: Inset{left: 5, top: 1, bottom: 1} + padding: Inset{left: 0, right: 0, top: 0, bottom: 0} + spacing: 6 + + emoji_smile_button := mod.widgets.RoomEmojiButton { text: "😀" } + emoji_joy_button := mod.widgets.RoomEmojiButton { text: "😂" } + emoji_thumbsup_button := mod.widgets.RoomEmojiButton { text: "👍" } + emoji_heart_button := mod.widgets.RoomEmojiButton { text: "❤️" } + emoji_fire_button := mod.widgets.RoomEmojiButton { text: "🔥" } + emoji_party_button := mod.widgets.RoomEmojiButton { text: "🎉" } + emoji_think_button := mod.widgets.RoomEmojiButton { text: "🤔" } + emoji_clap_button := mod.widgets.RoomEmojiButton { text: "👏" } } - mentionable_text_input := MentionableTextInput { + input_row := View { width: Fill, height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} - margin: Inset { - top: 3, // add some space between the top border of the text input and the top border of the room input bar - bottom: 5.75, // to line up the middle of the text input with the middle of the buttons - left: 3, right: 3 // to give a bit of breathing room between the text input and the buttons on the sides - }, + flow: Right + // Bottom-align everything to ensure that buttons always stick to the bottom + // even when the mentionable_text_input box is very tall. + align: Align{y: 1.0}, + + // A checkbox that enables TSP signing for the outgoing message. + // If TSP is not enabled, this will be an empty invisible view. + tsp_sign_checkbox := TspSignAnycastCheckbox { + margin: Inset{bottom: 9, left: 6, right: 0} + } - persistent +: { - center +: { - text_input := RobrixTextInput { - empty_text: "Write a message (in Markdown) ..." + emoji_picker_button := RobrixIconButton { + margin: Inset{left: 3, right: 1, top: 4, bottom: 4} + spacing: 0, + draw_icon +: { + svg: (ICON_ADD_REACTION) + color: (COLOR_ACTIVE_PRIMARY_DARKER) + }, + draw_bg +: { + color: (COLOR_BG_PREVIEW) + color_hover: #E0E8F0 + color_down: #D0D8E8 + } + icon_walk: Walk{width: 19, height: 19} + text: "", + } + + mentionable_text_input := MentionableTextInput { + width: Fill, + height: Fit{max: FitBound.Rel{base: Base.Full, factor: 0.75}} + margin: Inset { + top: 3, // add some space between the top border of the text input and the top border of this row + bottom: 5.75, // to line up the middle of the text input with the middle of the buttons + left: 3, right: 3 // to give a bit of breathing room between the text input and the buttons on the sides + }, + + persistent +: { + center +: { + text_input := RobrixTextInput { + empty_text: "Write a message (in Markdown) ..." + } } } } - } - send_message_button := RobrixPositiveIconButton { - // Disabled by default; enabled when text is inputted - enabled: false, - spacing: 0, - text: "", - margin: 4 - draw_icon +: { svg: (ICON_SEND) } - icon_walk: Walk{width: 21, height: 21}, + send_message_button := RobrixPositiveIconButton { + visible: false, + // Disabled by default; enabled when text is inputted + enabled: false, + spacing: 0, + text: "", + margin: 4 + draw_icon +: { svg: (ICON_SEND) } + icon_walk: Walk{width: 21, height: 21}, + } + + more_actions_button := RobrixIconButton { + spacing: 0, + text: "", + margin: 4 + draw_icon +: { svg: (mod.widgets.ICO_MENU) } + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + color_hover: (COLOR_ACTIVE_PRIMARY_DARKER) + color_down: #0C5DAA + } + icon_walk: Walk{width: 19, height: 19}, + } } } @@ -169,6 +260,10 @@ 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)>, + /// Whether the location card is currently expanded. + #[rust] is_location_card_expanded: bool, + /// Whether the emoji picker popup is currently expanded. + #[rust] is_emoji_picker_expanded: bool, } impl Widget for RoomInputBar { @@ -230,9 +325,58 @@ impl RoomInputBar { self.redraw(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..."); + // Handle the more actions button being clicked. + if self.button(cx, ids!(more_actions_button)).clicked(actions) { + self.is_location_card_expanded = !self.is_location_card_expanded; + self.button(cx, ids!(location_card_button)).set_visible(cx, self.is_location_card_expanded); + self.redraw(cx); + } + + // Handle the emoji picker button being clicked. + if self.button(cx, ids!(emoji_picker_button)).clicked(actions) { + self.is_emoji_picker_expanded = !self.is_emoji_picker_expanded; + self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, self.is_emoji_picker_expanded); + self.redraw(cx); + } + + let picked_emoji = if self.button(cx, ids!(emoji_smile_button)).clicked(actions) { + Some("😀") + } else if self.button(cx, ids!(emoji_joy_button)).clicked(actions) { + Some("😂") + } else if self.button(cx, ids!(emoji_thumbsup_button)).clicked(actions) { + Some("👍") + } else if self.button(cx, ids!(emoji_heart_button)).clicked(actions) { + Some("❤️") + } else if self.button(cx, ids!(emoji_fire_button)).clicked(actions) { + Some("🔥") + } else if self.button(cx, ids!(emoji_party_button)).clicked(actions) { + Some("🎉") + } else if self.button(cx, ids!(emoji_think_button)).clicked(actions) { + Some("🤔") + } else if self.button(cx, ids!(emoji_clap_button)).clicked(actions) { + Some("👏") + } else { + None + }; + + if let Some(emoji) = picked_emoji { + let mut text = mentionable_text_input.text(); + text.push_str(emoji); + mentionable_text_input.set_text(cx, &text); + self.enable_send_message_button(cx, !text.trim().is_empty()); + submit_async_request(MatrixRequest::SendTypingNotice { + room_id: room_screen_props.timeline_kind.room_id().clone(), + typing: !text.is_empty(), + }); + self.is_emoji_picker_expanded = false; + self.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); + self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); + self.redraw(cx); + } + + // Handle the location card being clicked. + if self.button(cx, ids!(location_card_button)).clicked(actions) { + log!("Location card clicked; requesting current location..."); if let Err(_e) = init_location_subscriber(cx) { error!("Failed to initialize location subscriber"); enqueue_popup_notification( @@ -425,7 +569,7 @@ impl RoomInputBar { // so that the user can immediately start typing their reply // without having to manually click on the message input box. if grab_key_focus { - self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)).set_key_focus(cx); + self.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)).set_key_focus(cx); } self.button(cx, ids!(cancel_reply_button)).reset_hover(cx); self.redraw(cx); @@ -506,7 +650,7 @@ impl RoomInputBar { } } - /// Sets the send_message_button to be enabled and green, or disabled and gray. + /// Sets the send_message_button to be shown/enabled and green, or hidden/disabled and gray. /// /// This should be called to update the button state when the message TextInput content changes. fn enable_send_message_button(&mut self, cx: &mut Cx, enable: bool) { @@ -517,6 +661,7 @@ impl RoomInputBar { (COLOR_FG_DISABLED, COLOR_BG_DISABLED) }; script_apply_eval!(cx, send_message_button, { + visible: #(enable), enabled: #(enable), draw_icon.color: #(fg_color), draw_bg.color: #(bg_color), @@ -665,7 +810,7 @@ impl RoomInputBarRef { was_replying_preview_visible: inner.was_replying_preview_visible, replying_to: inner.replying_to.clone(), editing_pane_state: inner.child_by_path(ids!(editing_pane)).as_editing_pane().save_state(), - text_input_state: inner.child_by_path(ids!(input_bar.mentionable_text_input.text_input)).as_text_input().save_state(), + text_input_state: inner.child_by_path(ids!(input_bar.input_row.mentionable_text_input.text_input)).as_text_input().save_state(), } } @@ -694,8 +839,16 @@ impl RoomInputBarRef { inner.update_user_power_levels(cx, user_power_levels); // 1. Restore the state of the TextInput within the MentionableTextInput. - inner.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + inner.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)) .restore_state(cx, text_input_state); + let is_text_input_empty = inner.text_input(cx, ids!(input_bar.input_row.mentionable_text_input.text_input)) + .text() + .is_empty(); + inner.enable_send_message_button(cx, !is_text_input_empty); + inner.is_location_card_expanded = false; + inner.button(cx, ids!(location_card_button)).set_visible(cx, false); + inner.is_emoji_picker_expanded = false; + inner.view.view(cx, ids!(emoji_picker_popup)).set_visible(cx, false); // 2. Restore the state of the replying-to preview. if let Some(replying_to) = replying_to { From 3c69b1218a85d0ef8df9f2c612a12347dd82437e Mon Sep 17 00:00:00 2001 From: alanpoon Date: Wed, 1 Apr 2026 22:13:05 +0800 Subject: [PATCH 052/283] reduce code change --- src/app.rs | 132 ++++- src/login/login_screen.rs | 208 +++++--- src/settings/account_settings.rs | 25 +- src/sliding_sync.rs | 841 +++++++++++++++---------------- 4 files changed, 675 insertions(+), 531 deletions(-) diff --git a/src/app.rs b/src/app.rs index 5c1ca4148..f00f7f771 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,7 +8,7 @@ use matrix_sdk::{RoomState, ruma::{OwnedEventId, OwnedRoomId, OwnedUserId, RoomI 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, clear_timeline_states, RoomScreenWidgetRefExt}, rooms_list::{RoomsListAction, RoomsListRef, RoomsListUpdate, clear_all_invited_rooms, enqueue_rooms_list_update}, invite_screen::InviteScreenWidgetRefExt, space_lobby::SpaceLobbyScreenWidgetRefExt, }, 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::{AccountSwitchAction, DirectMessageRoomAction, MatrixRequest, current_user_id, submit_async_request}, utils::RoomNameId, verification::VerificationAction, verification_modal::{ @@ -171,6 +171,9 @@ 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 { @@ -419,6 +422,33 @@ 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)) => { @@ -796,7 +826,7 @@ impl AppMain for App { } #[cfg(feature = "tsp")] { // Save the TSP wallet state, if it exists, with a 3-second timeout. - let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap_or_else(|e| e.into_inner())); + let tsp_state = std::mem::take(&mut *crate::tsp::tsp_state_ref().lock().unwrap()); let res = crate::sliding_sync::block_on_async_with_timeout( Some(std::time::Duration::from_secs(3)), async move { @@ -937,6 +967,104 @@ 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/login/login_screen.rs b/src/login/login_screen.rs index 097d2bb18..5a4c2fd1b 100644 --- a/src/login/login_screen.rs +++ b/src/login/login_screen.rs @@ -3,7 +3,7 @@ use std::ops::Not; use makepad_widgets::*; use url::Url; -use crate::sliding_sync::{submit_async_request, AccountSwitchAction, LoginByPassword, LoginRequest, MatrixRequest}; +use crate::sliding_sync::{submit_async_request, AccountSwitchAction, LoginByPassword, LoginRequest, MatrixRequest, RegisterAccount}; use super::login_status_modal::{LoginStatusModalAction, LoginStatusModalWidgetExt}; @@ -69,19 +69,13 @@ script_mod! { } } - RoundedView { + View { margin: Inset{top: 40, bottom: 40} width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit align: Align{x: 0.5, y: 0.5} flow: Overlay, - show_bg: true, - draw_bg +: { - color: (COLOR_SECONDARY) - border_radius: 6.0 - } - View { width: Fill // TODO: once Makepad supports it, use `Fill {max: 375}` height: Fit @@ -184,54 +178,61 @@ script_mod! { text: "Login" } - LineH { - width: 275 - margin: Inset{bottom: -5} - draw_bg.color: #C8C8C8 - } + login_only_view := View { + width: Fit, height: Fit, + flow: Down, + align: Align{x: 0.5, y: 0.5} + spacing: 15.0 - Label { - width: Fit, height: Fit - padding: 0, - draw_text +: { - color: (COLOR_TEXT) - text_style: TITLE_TEXT {font_size: 11.0} + LineH { + width: 275 + margin: Inset{bottom: -5} + draw_bg.color: #C8C8C8 } - text: "Or, login with an SSO provider:" - } - sso_view := View { - width: 275, height: Fit, - margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide - flow: Flow.Right{wrap: true}, - apple_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/apple.png") + Label { + width: Fit, height: Fit + padding: 0, + draw_text +: { + color: (COLOR_TEXT) + text_style: TITLE_TEXT {font_size: 11.0} } + text: "Or, login with an SSO provider:" } - facebook_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/facebook.png") + + sso_view := View { + width: 275, height: Fit, + margin: Inset{left: 30, right: 5} // make the inner view 240 pixels wide + flow: Flow.Right{wrap: true}, + apple_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/apple.png") + } } - } - github_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/github.png") + facebook_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/facebook.png") + } } - } - gitlab_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/gitlab.png") + github_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/github.png") + } } - } - google_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/google.png") + gitlab_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/gitlab.png") + } } - } - twitter_button := mod.widgets.SsoButton { - image := mod.widgets.SsoImage { - src: crate_resource("self://resources/img/x.png") + google_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/google.png") + } + } + twitter_button := mod.widgets.SsoButton { + image := mod.widgets.SsoImage { + src: crate_resource("self://resources/img/x.png") + } } } } @@ -246,7 +247,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } - Label { + account_prompt_label := Label { width: Fit, height: Fit padding: Inset{left: 1, right: 1, top: 0, bottom: 0} draw_text +: { @@ -259,7 +260,7 @@ script_mod! { LineH { draw_bg.color: #C8C8C8 } } - signup_button := RobrixIconButton { + mode_toggle_button := RobrixIconButton { width: Fit, height: Fit padding: Inset{left: 15, right: 15, top: 10, bottom: 10} margin: Inset{bottom: 5} @@ -293,20 +294,48 @@ script_mod! { } } -static MATRIX_SIGN_UP_URL: &str = "https://matrix.org/docs/chat_basics/matrix-for-im/#creating-a-matrix-account"; - #[derive(Script, ScriptHook, Widget)] pub struct LoginScreen { #[source] source: ScriptObjectRef, #[deref] view: View, + /// Whether the screen is showing the in-app sign-up flow. + #[rust] signup_mode: bool, /// Boolean to indicate if the SSO login process is still in flight #[rust] sso_pending: bool, /// The URL to redirect to after logging in with SSO. #[rust] sso_redirect_url: Option, + /// The most recent login failure message shown to the user. + #[rust] last_failure_message_shown: Option, /// Boolean to indicate if we're in "add account" mode (adding another Matrix account). #[rust] adding_account: bool, } +impl LoginScreen { + fn set_signup_mode(&mut self, cx: &mut Cx, signup_mode: bool) { + self.signup_mode = signup_mode; + self.view.view(cx, ids!(confirm_password_wrapper)).set_visible(cx, signup_mode); + self.view.view(cx, ids!(login_only_view)).set_visible(cx, !signup_mode); + self.view.label(cx, ids!(title)).set_text(cx, + if signup_mode { "Create your Robrix account" } else { "Login to Robrix" } + ); + self.view.button(cx, ids!(login_button)).set_text(cx, + if signup_mode { "Create account" } else { "Login" } + ); + self.view.label(cx, ids!(account_prompt_label)).set_text(cx, + if signup_mode { "Already have an account?" } else { "Don't have an account?" } + ); + self.view.button(cx, ids!(mode_toggle_button)).set_text(cx, + if signup_mode { "Back to login" } else { "Sign up here" } + ); + + if !signup_mode { + self.view.text_input(cx, ids!(confirm_password_input)).set_text(cx, ""); + } + + self.redraw(cx); + } +} + impl Widget for LoginScreen { fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { @@ -322,10 +351,11 @@ impl Widget for LoginScreen { impl MatchEvent for LoginScreen { fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { let login_button = self.view.button(cx, ids!(login_button)); - let signup_button = self.view.button(cx, ids!(signup_button)); + let mode_toggle_button = self.view.button(cx, ids!(mode_toggle_button)); let cancel_button = self.view.button(cx, ids!(cancel_button)); let user_id_input = self.view.text_input(cx, ids!(user_id_input)); let password_input = self.view.text_input(cx, ids!(password_input)); + let confirm_password_input = self.view.text_input(cx, ids!(confirm_password_input)); let homeserver_input = self.view.text_input(cx, ids!(homeserver_input)); let login_status_modal = self.view.modal(cx, ids!(login_status_modal)); @@ -338,24 +368,25 @@ impl MatchEvent for LoginScreen { self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); cancel_button.set_visible(cx, false); self.view.view(cx, ids!(sso_view)).set_visible(cx, true); - signup_button.set_visible(cx, true); + mode_toggle_button.set_visible(cx, true); cx.action(LoginAction::CancelAddAccount); self.redraw(cx); } - if signup_button.clicked(actions) { - log!("Opening URL \"{}\"", MATRIX_SIGN_UP_URL); - let _ = robius_open::Uri::new(MATRIX_SIGN_UP_URL).open(); + if mode_toggle_button.clicked(actions) { + self.set_signup_mode(cx, !self.signup_mode); } if login_button.clicked(actions) || user_id_input.returned(actions).is_some() || password_input.returned(actions).is_some() + || (self.signup_mode && confirm_password_input.returned(actions).is_some()) || homeserver_input.returned(actions).is_some() { - let user_id = user_id_input.text(); + let user_id = user_id_input.text().trim().to_owned(); let password = password_input.text(); - let homeserver = homeserver_input.text(); + let confirm_password = confirm_password_input.text(); + let homeserver = homeserver_input.text().trim().to_owned(); if user_id.is_empty() { login_status_modal_inner.set_title(cx, "Missing User ID"); login_status_modal_inner.set_status(cx, "Please enter a valid User ID."); @@ -364,16 +395,40 @@ impl MatchEvent for LoginScreen { login_status_modal_inner.set_title(cx, "Missing Password"); login_status_modal_inner.set_status(cx, "Please enter a valid password."); login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); + } else if self.signup_mode && password != confirm_password { + login_status_modal_inner.set_title(cx, "Passwords do not match"); + login_status_modal_inner.set_status(cx, "Please enter the same password in both password fields."); + login_status_modal_inner.button_ref(cx).set_text(cx, "Okay"); } else { - login_status_modal_inner.set_title(cx, "Logging in..."); - login_status_modal_inner.set_status(cx, "Waiting for a login response..."); + self.last_failure_message_shown = None; + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Creating account..." + } else { + "Logging in..." + }); + login_status_modal_inner.set_status( + cx, + if self.signup_mode { + "Waiting for the homeserver to create your account..." + } else { + "Waiting for a login response..." + }, + ); login_status_modal_inner.button_ref(cx).set_text(cx, "Cancel"); - submit_async_request(MatrixRequest::Login(LoginRequest::LoginByPassword(LoginByPassword { - user_id, - password, - homeserver: homeserver.is_empty().not().then_some(homeserver), - is_add_account: self.adding_account, - }))); + submit_async_request(MatrixRequest::Login(if self.signup_mode { + LoginRequest::Register(RegisterAccount { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + }) + } else { + LoginRequest::LoginByPassword(LoginByPassword { + user_id, + password, + homeserver: homeserver.is_empty().not().then_some(homeserver), + is_add_account: self.adding_account, + }) + })); } login_status_modal.open(cx); self.redraw(cx); @@ -396,6 +451,7 @@ impl MatchEvent for LoginScreen { // Handle login-related actions received from background async tasks. match action.downcast_ref() { Some(LoginAction::CliAutoLogin { user_id, homeserver }) => { + self.last_failure_message_shown = None; user_id_input.set_text(cx, user_id); password_input.set_text(cx, ""); homeserver_input.set_text(cx, homeserver.as_deref().unwrap_or_default()); @@ -410,6 +466,7 @@ impl MatchEvent for LoginScreen { login_status_modal.open(cx); } Some(LoginAction::Status { title, status }) => { + self.last_failure_message_shown = None; login_status_modal_inner.set_title(cx, title); login_status_modal_inner.set_status(cx, status); let login_status_modal_button = login_status_modal_inner.button_ref(cx); @@ -421,19 +478,30 @@ impl MatchEvent for LoginScreen { Some(LoginAction::LoginSuccess) => { // The main `App` component handles showing the main screen // and hiding the login screen & login status modal. + self.last_failure_message_shown = None; + self.set_signup_mode(cx, false); self.adding_account = false; user_id_input.set_text(cx, ""); password_input.set_text(cx, ""); + confirm_password_input.set_text(cx, ""); homeserver_input.set_text(cx, ""); // Reset title and buttons in case we were in add-account mode self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); cancel_button.set_visible(cx, false); - signup_button.set_visible(cx, true); + mode_toggle_button.set_visible(cx, true); login_status_modal.close(cx); self.redraw(cx); } Some(LoginAction::LoginFailure(error)) => { - login_status_modal_inner.set_title(cx, "Login Failed."); + if self.last_failure_message_shown.as_deref() == Some(error.as_str()) { + continue; + } + self.last_failure_message_shown = Some(error.clone()); + login_status_modal_inner.set_title(cx, if self.signup_mode { + "Account Creation Failed." + } else { + "Login Failed." + }); login_status_modal_inner.set_status(cx, error); let login_status_modal_button = login_status_modal_inner.button_ref(cx); login_status_modal_button.set_text(cx, "Okay"); @@ -464,7 +532,7 @@ impl MatchEvent for LoginScreen { self.view.label(cx, ids!(title)).set_text(cx, "Add Another Account"); cancel_button.set_visible(cx, true); // Hide signup button in add-account mode (user already has an account) - signup_button.set_visible(cx, false); + mode_toggle_button.set_visible(cx, false); self.redraw(cx); } Some(LoginAction::AddAccountSuccess) => { @@ -476,7 +544,7 @@ impl MatchEvent for LoginScreen { // Reset title and buttons self.view.label(cx, ids!(title)).set_text(cx, "Login to Robrix"); cancel_button.set_visible(cx, false); - signup_button.set_visible(cx, true); + mode_toggle_button.set_visible(cx, true); login_status_modal.close(cx); self.redraw(cx); } diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index f003808d5..46ad09e1a 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -120,8 +120,7 @@ script_mod! { // their styles to RobrixNeutralIconButton / RobrixPositiveIconButton. cancel_display_name_button := RobrixNeutralIconButton { enabled: false, - width: Fit, - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + width: Fit, height: Fit, padding: 10, margin: Inset{left: 5}, draw_icon.svg: (ICON_FORBIDDEN) @@ -131,10 +130,10 @@ script_mod! { accept_display_name_button := RobrixPositiveIconButton { enabled: false, - width: Fit, - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, + width: Fit, height: Fit, 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" @@ -311,7 +310,6 @@ script_mod! { spacing: 10 manage_account_button := RobrixIconButton { - height: mod.widgets.SETTINGS_BUTTON_HEIGHT, padding: Inset{top: 10, bottom: 10, left: 12, right: 15} margin: Inset{left: 5} draw_icon.svg: (ICON_EXTERNAL_LINK) @@ -320,7 +318,6 @@ 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) @@ -350,7 +347,7 @@ impl Widget for AccountSettings { match event.hits(cx, copy_user_id_button_area) { Hit::FingerHoverIn(_) | Hit::FingerLongPress(_) => { cx.widget_action( - copy_user_id_button.widget_uid(), + copy_user_id_button.widget_uid(), TooltipAction::HoverIn { text: "Copy User ID".to_string(), widget_rect: copy_user_id_button_area.rect(cx), @@ -363,7 +360,7 @@ impl Widget for AccountSettings { } Hit::FingerHoverOut(_) => { cx.widget_action( - copy_user_id_button.widget_uid(), + copy_user_id_button.widget_uid(), TooltipAction::HoverOut, ); } @@ -380,9 +377,6 @@ impl Widget for AccountSettings { impl MatchEvent for AccountSettings { fn handle_signal(&mut self, cx: &mut Cx) { - // Process avatar updates from the cache - avatar_cache::process_avatar_updates(cx); - // If we don't have a profile yet, try to get it if self.own_profile.is_none() { user_profile_cache::process_user_profile_updates(cx); @@ -398,6 +392,8 @@ impl MatchEvent for AccountSettings { } return; } + // Process avatar updates from the cache + avatar_cache::process_avatar_updates(cx); // Update avatar from cache if we have a profile if let Some(profile) = self.own_profile.as_mut() { @@ -521,10 +517,13 @@ impl MatchEvent for AccountSettings { let Some(own_profile) = &self.own_profile else { return }; if upload_avatar_button.clicked(actions) { + // TODO: uncomment the below once avatar uploading is implemented + // Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); + // Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); enqueue_popup_notification( "Avatar upload is not yet implemented.", - PopupKind::Info, - Some(3.0), + PopupKind::Warning, + Some(4.0), ); } diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 395e73338..9ad399e0e 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -8,7 +8,7 @@ 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::{MediaFormat, MediaRequestParameters}, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ + config::RequestConfig, encryption::EncryptionSettings, event_handler::EventHandlerDropGuard, media::MediaRequestParameters, room::{edit::EditedContent, reply::Reply, IncludeRelations, RelationsOptions, RoomMember}, ruma::{ api::{Direction, client::{ account::register::v3::Request as RegistrationRequest, error::ErrorKind, @@ -48,7 +48,7 @@ use crate::{ }, 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, Clone)] +#[derive(Parser, Default)] struct Cli { /// The user ID to login with. #[clap(value_parser)] @@ -846,15 +846,6 @@ pub enum MatrixRequest { destination: MediaCacheEntryRef, update_sender: Option>, }, - /// Request to download a file from Matrix and save it to disk. - DownloadFile { - /// The media source of the file to download. - media_source: ruma::events::room::MediaSource, - /// The suggested filename for the downloaded file. - filename: String, - /// The destination path to save the file to. - destination_path: std::path::PathBuf, - }, /// Request to send a message to the given room. SendMessage { timeline_kind: TimelineKind, @@ -962,12 +953,9 @@ pub enum MatrixRequest { /// 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_or_else(|e| e.into_inner()).as_ref() { - if let Err(_e) = sender.send(req) { - // The receiver has been dropped, likely due to account switching or logout. - // This is expected during transitions, so we silently ignore the error. - log!("Note: matrix worker task receiver unavailable, request dropped (likely during account switch)"); - } + if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() { + sender.send(req) + .expect("BUG: matrix worker task receiver has died!"); } } @@ -1283,7 +1271,7 @@ async fn matrix_worker_task( MatrixRequest::CreateThreadTimeline { room_id, thread_root_event_id } => { let main_room_timeline = { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); + 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; @@ -1311,7 +1299,7 @@ async fn matrix_worker_task( match build_result { Ok(thread_timeline) => { - let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()); + let mut all_joined_rooms = ALL_JOINED_ROOMS.lock().unwrap(); let Some(room_info) = all_joined_rooms.get_mut(&room_id) else { return; }; @@ -1347,7 +1335,7 @@ async fn matrix_worker_task( } 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_or_else(|e| e.into_inner()); + 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 @@ -1572,7 +1560,7 @@ async fn matrix_worker_task( 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_or_else(|e| e.into_inner()); + 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; @@ -1901,7 +1889,7 @@ async fn matrix_worker_task( 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_or_else(|e| e.into_inner()); + 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; @@ -2062,6 +2050,7 @@ async fn matrix_worker_task( 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; @@ -2069,48 +2058,6 @@ async fn matrix_worker_task( }); } - MatrixRequest::DownloadFile { media_source, filename, destination_path } => { - let Some(client) = get_client() else { continue }; - - let _download_task = Handle::current().spawn(async move { - log!("Downloading file {filename} to {:?}...", destination_path); - let media_request = MediaRequestParameters { - source: media_source, - format: MediaFormat::File, - }; - match client.media().get_media_content(&media_request, true).await { - Ok(data) => { - match std::fs::write(&destination_path, &data) { - Ok(_) => { - log!("Successfully downloaded file to {:?}", destination_path); - enqueue_popup_notification( - format!("Downloaded: {filename}"), - PopupKind::Success, - None, - ); - } - Err(e) => { - error!("Failed to write file to {:?}: {e}", destination_path); - enqueue_popup_notification( - format!("Failed to save file: {e}"), - PopupKind::Error, - None, - ); - } - } - } - Err(e) => { - error!("Failed to download file {filename}: {e}"); - enqueue_popup_notification( - format!("Failed to download: {e}"), - PopupKind::Error, - None, - ); - } - } - }); - } - MatrixRequest::SendMessage { timeline_kind, message, @@ -2479,7 +2426,7 @@ pub fn block_on_async_with_timeout( timeout: Option, async_future: impl Future, ) -> Result { - let rt = TOKIO_RUNTIME.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(|| + let rt = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") ).handle().clone(); @@ -2499,7 +2446,7 @@ pub fn block_on_async_with_timeout( /// 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_or_else(|e| e.into_inner()).get_or_insert_with(|| { + let rt_handle = TOKIO_RUNTIME.lock().unwrap().get_or_insert_with(|| { tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime") }).handle().clone(); @@ -2508,7 +2455,7 @@ pub fn start_matrix_tokio() -> Result { rt_handle.spawn(async move { match build_client(&Cli::default(), app_data_dir()).await { Ok(client_and_session) => { - DEFAULT_SSO_CLIENT.lock().unwrap_or_else(|e| e.into_inner()) + DEFAULT_SSO_CLIENT.lock().unwrap() .get_or_insert(client_and_session); } Err(e) => error!("Error: could not create DEFAULT_SSO_CLIENT object: {e}"), @@ -2525,6 +2472,7 @@ pub fn start_matrix_tokio() -> Result { 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>; @@ -2613,19 +2561,19 @@ fn get_per_timeline_details<'a>( /// 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_or_else(|e| e.into_inner()).deref_mut(), kind) + 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_or_else(|e| e.into_inner()).deref_mut(), kind) + 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_or_else(|e| e.into_inner()) + ALL_JOINED_ROOMS.lock().unwrap() .get(room_id) .map(|jrd| jrd.main_timeline.timeline.clone()) } @@ -2634,12 +2582,12 @@ fn get_room_timeline(room_id: &RoomId) -> Option> { static CLIENT: Mutex> = Mutex::new(None); pub fn get_client() -> Option { - CLIENT.lock().unwrap_or_else(|e| e.into_inner()).clone() + 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_or_else(|e| e.into_inner()).as_ref().and_then(|c| + CLIENT.lock().unwrap().as_ref().and_then(|c| c.session_meta().map(|m| m.user_id.clone()) ) } @@ -2689,12 +2637,12 @@ static IGNORED_USERS: Mutex> = Mutex::new(Hash /// Returns a deep clone of the current list of ignored users. pub fn get_ignored_users() -> HashSet { - IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).clone() + 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_or_else(|e| e.into_inner()).contains(user_id) + IGNORED_USERS.lock().unwrap().contains(user_id) } @@ -2707,7 +2655,7 @@ pub fn is_user_ignored(user_id: &UserId) -> bool { /// 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_or_else(|e| e.into_inner()); + 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, @@ -2811,7 +2759,7 @@ impl RoomListServiceRoomInfo { 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_or_else(|e| e.into_inner()).replace(sender); + REQUEST_SENDER.lock().unwrap().replace(sender); let (login_sender, mut login_receiver) = tokio::sync::mpsc::channel(1); @@ -2890,418 +2838,420 @@ async fn start_matrix_client_login_and_sync(rt: Handle) { // 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, validate_session, session) = 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, _is_add_account, session)) => break (client, sync_token, false, session), - Err(e) => { - error!("Login failed: {e:?}"); - Cx::post_action(LoginAction::LoginFailure(format!("{e}"))); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: format!("Login failed: {e}"), - }); + + loop { + let (client, sync_service, logged_in_user_id) = 'login_loop: loop { + let (client, _sync_token, validate_session, session) = 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, _is_add_account, session)) => break (client, sync_token, false, session), + 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; } - }, - 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; } } } - } - }; + }; - if validate_session { - match client.whoami().await { - Ok(_) => {} - Err(e) if is_invalid_token_http_error(&e) => { - clear_persisted_session(client.user_id()).await; - let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; - Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: err_msg.to_string(), - }); - continue 'login_loop; - } - Err(e) => { - warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); + if validate_session { + match client.whoami().await { + Ok(_) => {} + Err(e) if is_invalid_token_http_error(&e) => { + clear_persisted_session(client.user_id()).await; + let err_msg = "Your login token is no longer valid.\n\nPlease log in again."; + Cx::post_action(LoginAction::LoginFailure(err_msg.to_string())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: err_msg.to_string(), + }); + continue 'login_loop; + } + Err(e) => { + warning!("Session validation via whoami failed, but the error was not an invalid token; continuing startup: {e}"); + } } } - } - - // 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 }); + // Deallocate the default SSO client after a successful login. + if let Ok(mut client_opt) = DEFAULT_SSO_CLIENT.lock() { + let _ = client_opt.take(); + } - // Add the account to the AccountManager - let account = account_manager::Account { - client: client.clone(), - user_id: logged_in_user_id.clone(), - session, - display_name: None, - avatar_url: None, - }; - let is_new = account_manager::add_account(account); - log!("Added account {} to AccountManager. New account: {}", logged_in_user_id, is_new); + 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 }); + + // Add the account to the AccountManager + let account = account_manager::Account { + client: client.clone(), + user_id: logged_in_user_id.clone(), + session, + display_name: None, + avatar_url: None, + }; + let is_new = account_manager::add_account(account); + log!("Added account {} to AccountManager. New account: {}", logged_in_user_id, is_new); - // Store this active client in our global Client state so that other tasks can access it. - if let Some(_existing) = CLIENT.lock().unwrap_or_else(|e| e.into_inner()).replace(client.clone()) { - error!("BUG: unexpectedly replaced an existing client when initializing the matrix client."); - } + // 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 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 updates to the ignored user list. + handle_ignore_user_list_subscriber(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}.") - }; - if is_invalid_token_error(&e) { - clear_persisted_session(client.user_id()).await; + 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}.") + }; + if is_invalid_token_error(&e) { + clear_persisted_session(client.user_id()).await; + } + 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; } - 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_or_else(|e| e.into_inner()).take(); - continue 'login_loop; - } - }; + }; - break 'login_loop (client, sync_service, logged_in_user_id); - }; + break 'login_loop (client, sync_service, logged_in_user_id); + }; - let (session_reset_sender, mut session_reset_receiver) = - tokio::sync::mpsc::unbounded_channel::(); - // Listen for session changes, e.g., when the access token becomes invalid. - let session_change_handler_task = - handle_session_changes(client.clone(), session_reset_sender); + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + // Listen for session changes, e.g., when the access token becomes invalid. + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); - // 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); + // 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; + // 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(); + let room_list_service = sync_service.room_list_service(); - if let Some(_existing) = SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).replace(Arc::new(sync_service)) { - error!("BUG: unexpectedly replaced an existing sync service when initializing the matrix client."); - } + 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. - let reauth_message: Option = loop { - tokio::select! { - session_reset = session_reset_receiver.recv() => { - match session_reset { - Some(SessionResetAction::Reauthenticate { message }) => { - break Some(message); - } - None => { - warning!("Session reset receiver closed unexpectedly."); - continue; + 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 + // matrix/background tasks for the currently-authenticated session. + #[allow(clippy::never_loop)] // unsure if needed, just following tokio's examples. + let reauth_message: Option = loop { + tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + break Some(message); + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; + } } } - } - result = &mut matrix_worker_task_handle => { - session_change_handler_task.abort(); - match result { - Ok(Ok(())) => { - // Check if this is due to logout or account switch - if is_logout_in_progress() { - log!("matrix worker task ended due to logout"); - } else if is_account_switch_pending() { - log!("matrix worker task ended due to account switch"); - } else { - error!("BUG: matrix worker task ended unexpectedly!"); + result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); + match result { + Ok(Ok(())) => { + // Check if this is due to logout or account switch + if is_logout_in_progress() { + log!("matrix worker task ended due to logout"); + } else if is_account_switch_pending() { + log!("matrix worker task ended due to account switch"); + } else { + error!("BUG: matrix worker task ended unexpectedly!"); + } } - } - Ok(Err(e)) => { - // Check if this is due to logout or account switch - if is_logout_in_progress() { - log!("matrix worker task ended with error due to logout: {e:?}"); - } else if is_account_switch_pending() { - log!("matrix worker task ended with error due to account switch: {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, - ); + Ok(Err(e)) => { + // Check if this is due to logout or account switch + if is_logout_in_progress() { + log!("matrix worker task ended with error due to logout: {e:?}"); + } else if is_account_switch_pending() { + log!("matrix worker task ended with error due to account switch: {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:?}"); } - }, - Err(e) => { - error!("BUG: failed to join matrix worker task: {e:?}"); } + break None; } - break None; - } - result = &mut room_list_service_task => { - session_change_handler_task.abort(); - match result { - Ok(Ok(())) => { - if is_logout_in_progress() || is_account_switch_pending() { - log!("room list service loop task ended due to logout/account switch"); - } else { - error!("BUG: room list service loop task ended unexpectedly!"); + result = &mut room_list_service_task => { + session_change_handler_task.abort(); + match result { + Ok(Ok(())) => { + if is_logout_in_progress() || is_account_switch_pending() { + log!("room list service loop task ended due to logout/account switch"); + } else { + error!("BUG: room list service loop task ended unexpectedly!"); + } } - } - Ok(Err(e)) => { - if !is_logout_in_progress() && !is_account_switch_pending() { - 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, - ); + Ok(Err(e)) => { + if !is_logout_in_progress() && !is_account_switch_pending() { + 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:?}"); } - }, - Err(e) => { - error!("BUG: failed to join room list service loop task: {e:?}"); } + break None; } - break None; - } - result = &mut space_service_task => { - session_change_handler_task.abort(); - match result { - Ok(Ok(())) => { - if is_logout_in_progress() || is_account_switch_pending() { - log!("space service loop task ended due to logout/account switch"); - } else { - error!("BUG: space service loop task ended unexpectedly!"); + result = &mut space_service_task => { + session_change_handler_task.abort(); + match result { + Ok(Ok(())) => { + if is_logout_in_progress() || is_account_switch_pending() { + log!("space service loop task ended due to logout/account switch"); + } else { + error!("BUG: space service loop task ended unexpectedly!"); + } } - } - Ok(Err(e)) => { - if !is_logout_in_progress() && !is_account_switch_pending() { - 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, - ); + Ok(Err(e)) => { + if !is_logout_in_progress() && !is_account_switch_pending() { + 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:?}"); } - }, - Err(e) => { - error!("BUG: failed to join space service loop task: {e:?}"); } + break None; } - break None; } - } - }; + }; - // Check if we need to restart for an account switch (loop to handle consecutive switches) - while let Some(switch_user_id) = take_account_switch_target() { - // Clear all backend state - CLIENT.lock().unwrap_or_else(|e| e.into_inner()).take(); - SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).take(); - ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); - IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).clear(); - - // Clear the rooms list UI - enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); - enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); - - // Post action to clear UI state - Cx::post_action(AccountSwitchAction::Starting(switch_user_id.clone())); - - // Update active account - account_manager::set_active_account(&switch_user_id); - - // Restore session for the switched account - match persistence::restore_session(Some(switch_user_id.clone())).await { - Ok((client, _sync_token, _session)) => { - // Store the client - CLIENT.lock().unwrap_or_else(|e| e.into_inner()).replace(client.clone()); - - // Set up the new client - add_verification_event_handlers_and_sync_client(client.clone()); - handle_ignore_user_list_subscriber(client.clone()); - - // Create new sync service - let sync_service = match SyncService::builder(client.clone()) - .with_offline_mode() - .build() - .await - { - Ok(ss) => ss, - Err(e) => { - error!("Failed to create SyncService: {e:?}"); - Cx::post_action(AccountSwitchAction::Failed(format!("Failed to create sync service: {e}"))); - return; - } - }; + // Check if we need to restart for an account switch (loop to handle consecutive switches) + while let Some(switch_user_id) = take_account_switch_target() { + // Clear all backend state + CLIENT.lock().unwrap().take(); + SYNC_SERVICE.lock().unwrap().take(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); + IGNORED_USERS.lock().unwrap().clear(); + + // Clear the rooms list UI + enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); + enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); + + // Post action to clear UI state + Cx::post_action(AccountSwitchAction::Starting(switch_user_id.clone())); + + // Update active account + account_manager::set_active_account(&switch_user_id); + // Recreate worker task and service loops + let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); + REQUEST_SENDER.lock().unwrap().replace(sender); + // Restore session for the switched account + match persistence::restore_session(Some(switch_user_id.clone())).await { + Ok((client, _sync_token, _session)) => { + // Store the client + CLIENT.lock().unwrap().replace(client.clone()); + + // Set up the new client + add_verification_event_handlers_and_sync_client(client.clone()); + handle_ignore_user_list_subscriber(client.clone()); + + // Create new sync service + let sync_service = match SyncService::builder(client.clone()) + .with_offline_mode() + .build() + .await + { + Ok(ss) => ss, + Err(e) => { + error!("Failed to create SyncService: {e:?}"); + Cx::post_action(AccountSwitchAction::Failed(format!("Failed to create sync service: {e}"))); + return; + } + }; - // Load app state for the new user - handle_load_app_state(switch_user_id.clone()); - 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(); - - SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).replace(Arc::new(sync_service)); - - // Recreate worker task and service loops - let (sender, receiver) = tokio::sync::mpsc::unbounded_channel::(); - REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).replace(sender); - let (login_sender, _login_receiver) = tokio::sync::mpsc::channel(1); - - // Set up session change handler for the switched account - let (session_reset_sender, mut session_reset_receiver) = - tokio::sync::mpsc::unbounded_channel::(); - let session_change_handler_task = - handle_session_changes(client.clone(), session_reset_sender); - - let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); - 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.clone())); - - // Notify UI that switch is complete (app.rs handles the popup notification) - Cx::post_action(AccountSwitchAction::Switched(switch_user_id.clone())); - - // Re-enter the main monitoring loop - loop { - tokio::select! { - session_reset = session_reset_receiver.recv() => { - match session_reset { - Some(SessionResetAction::Reauthenticate { message }) => { - error!("Session reset during account switch: {}", message); - session_change_handler_task.abort(); - room_list_service_task.abort(); - space_service_task.abort(); - Cx::post_action(AccountSwitchAction::Failed(message)); - break; - } - None => { - warning!("Session reset receiver closed unexpectedly."); - continue; + // Load app state for the new user + handle_load_app_state(switch_user_id.clone()); + 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(); + + SYNC_SERVICE.lock().unwrap().replace(Arc::new(sync_service)); + + let (login_sender, _login_receiver) = tokio::sync::mpsc::channel(1); + + // Set up session change handler for the switched account + let (session_reset_sender, mut session_reset_receiver) = + tokio::sync::mpsc::unbounded_channel::(); + let session_change_handler_task = + handle_session_changes(client.clone(), session_reset_sender); + + let mut matrix_worker_task_handle = rt.spawn(matrix_worker_task(receiver, login_sender)); + 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.clone())); + + // Notify UI that switch is complete (app.rs handles the popup notification) + Cx::post_action(AccountSwitchAction::Switched(switch_user_id.clone())); + + // Re-enter the main monitoring loop + loop { + tokio::select! { + session_reset = session_reset_receiver.recv() => { + match session_reset { + Some(SessionResetAction::Reauthenticate { message }) => { + error!("Session reset during account switch: {}", message); + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); + Cx::post_action(AccountSwitchAction::Failed(message)); + break; + } + None => { + warning!("Session reset receiver closed unexpectedly."); + continue; + } } } - } - result = &mut matrix_worker_task_handle => { - session_change_handler_task.abort(); - match result { - Ok(Ok(())) => { - if !is_logout_in_progress() && !is_account_switch_pending() { - error!("BUG: matrix worker task ended unexpectedly!"); + result = &mut matrix_worker_task_handle => { + session_change_handler_task.abort(); + match result { + Ok(Ok(())) => { + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("BUG: matrix worker task ended unexpectedly!"); + } } - } - Ok(Err(e)) => { - if !is_logout_in_progress() && !is_account_switch_pending() { - error!("Error: matrix worker task ended:\n\t{e:?}"); + Ok(Err(e)) => { + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Error: matrix worker task ended:\n\t{e:?}"); + } + } + Err(e) => { + error!("BUG: failed to join matrix worker task: {e:?}"); } } - Err(e) => { - error!("BUG: failed to join matrix worker task: {e:?}"); - } + break; } - break; - } - result = &mut room_list_service_task => { - session_change_handler_task.abort(); - if let Err(e) = result { - if !is_logout_in_progress() && !is_account_switch_pending() { - error!("Room list service task error: {e:?}"); + result = &mut room_list_service_task => { + session_change_handler_task.abort(); + if let Err(e) = result { + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Room list service task error: {e:?}"); + } } + break; } - break; - } - result = &mut space_service_task => { - session_change_handler_task.abort(); - if let Err(e) = result { - if !is_logout_in_progress() && !is_account_switch_pending() { - error!("Space service task error: {e:?}"); + result = &mut space_service_task => { + session_change_handler_task.abort(); + if let Err(e) = result { + if !is_logout_in_progress() && !is_account_switch_pending() { + error!("Space service task error: {e:?}"); + } } + break; } - break; } } + // After inner loop breaks, outer while loop will check for another pending account switch + } + Err(e) => { + error!("Failed to restore session for account switch: {e:?}"); + Cx::post_action(AccountSwitchAction::Failed(format!("Failed to restore session: {e}"))); + enqueue_popup_notification( + format!("Account switch failed: {e}"), + PopupKind::Error, + None, + ); + // Don't loop back - a failed switch shouldn't keep trying + break; } - // After inner loop breaks, outer while loop will check for another pending account switch - } - Err(e) => { - error!("Failed to restore session for account switch: {e:?}"); - Cx::post_action(AccountSwitchAction::Failed(format!("Failed to restore session: {e}"))); - enqueue_popup_notification( - format!("Account switch failed: {e}"), - PopupKind::Error, - None, - ); - // Don't loop back - a failed switch shouldn't keep trying - break; } } - } - // Only run reauth cleanup if we got a reauth message (not account switch or logout) - if let Some(reauth_msg) = reauth_message { - session_change_handler_task.abort(); - room_list_service_task.abort(); - space_service_task.abort(); + // Only run reauth cleanup if we got a reauth message (not account switch or logout) + if let Some(reauth_msg) = reauth_message { + session_change_handler_task.abort(); + room_list_service_task.abort(); + space_service_task.abort(); - reset_runtime_state_for_relogin().await; - Cx::post_action(LoginAction::LoginFailure(reauth_msg.clone())); - enqueue_rooms_list_update(RoomsListUpdate::Status { - status: reauth_msg, - }); + reset_runtime_state_for_relogin().await; + Cx::post_action(LoginAction::LoginFailure(reauth_msg.clone())); + enqueue_rooms_list_update(RoomsListUpdate::Status { + status: reauth_msg, + }); + } } } @@ -3348,7 +3298,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu } // 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_or_else(|e| e.into_inner()).clear(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); } else { @@ -3388,7 +3338,7 @@ async fn room_list_service_loop(room_list_service: Arc) -> Resu VectorDiff::Clear => { if LOG_ROOM_LIST_DIFFS { log!("room_list: diff Clear"); } all_known_rooms.clear(); - ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); + ALL_JOINED_ROOMS.lock().unwrap().clear(); enqueue_rooms_list_update(RoomsListUpdate::RoomOrderUpdate(VecDiff::Clear)); enqueue_rooms_list_update(RoomsListUpdate::ClearRooms); } @@ -3696,7 +3646,7 @@ async fn update_room( 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_or_else(|e| e.into_inner()).get(room_id) { + if let Some(jrd) = ALL_JOINED_ROOMS.lock().unwrap().get(room_id) { __timeline_update_sender_opt = Some(jrd.main_timeline.timeline_update_sender.clone()); } } @@ -3747,7 +3697,7 @@ async fn update_room( /// 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_or_else(|e| e.into_inner()).remove(&room.room_id); + ALL_JOINED_ROOMS.lock().unwrap().remove(&room.room_id); enqueue_rooms_list_update( RoomsListUpdate::RemoveRoom { room_id: room.room_id.clone(), @@ -3855,7 +3805,7 @@ async fn add_new_room( // 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_or_else(|e| e.into_inner()).insert( + ALL_JOINED_ROOMS.lock().unwrap().insert( new_room.room_id.clone(), JoinedRoomDetails { room_id: new_room.room_id.clone(), @@ -3932,7 +3882,7 @@ fn handle_ignore_user_list_subscriber(client: Client) { .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_or_else(|e| e.into_inner()); + let mut ignored_users_old = IGNORED_USERS.lock().unwrap(); let has_changed = *ignored_users_old != ignored_users_new; *ignored_users_old = ignored_users_new; @@ -4766,7 +4716,7 @@ async fn spawn_sso_server( // 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_or_else(|e| e.into_inner()).take(); + 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 @@ -4843,7 +4793,6 @@ async fn spawn_sso_server( }) { Ok(identity_provider_res) => { if !is_logged_in { - // SSO login doesn't support add-account mode yet, so pass false if let Err(e) = login_sender.send(LoginRequest::LoginBySSOSuccess(client, client_session, false)).await { error!("Error sending login request to login_sender: {e:?}"); Cx::post_action(LoginAction::LoginFailure(String::from( @@ -5025,7 +4974,7 @@ impl UserPowerLevels { /// 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_or_else(|e| e.into_inner()).take() { + if let Some(runtime) = TOKIO_RUNTIME.lock().unwrap().take() { runtime.shutdown_background(); } } @@ -5033,11 +4982,11 @@ pub fn shutdown_background_tasks() { 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_or_else(|e| e.into_inner()).take(); - SYNC_SERVICE.lock().unwrap_or_else(|e| e.into_inner()).take(); - REQUEST_SENDER.lock().unwrap_or_else(|e| e.into_inner()).take(); - IGNORED_USERS.lock().unwrap_or_else(|e| e.into_inner()).clear(); - ALL_JOINED_ROOMS.lock().unwrap_or_else(|e| e.into_inner()).clear(); + 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() }); From 6526855a87ab89134a6a2cce260059d7dfdd90e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Wed, 1 Apr 2026 22:17:06 +0800 Subject: [PATCH 053/283] feat(settings): support avatar upload and robust avatar deletion - add desktop avatar file picker upload flow in account settings\n- add MatrixRequest::UploadAvatar worker path with PNG/JPEG validation\n- add fallback delete-avatar request for homeservers returning M_UNRECOGNIZED\n- show not-supported notices for avatar actions on mobile platforms --- Cargo.lock | 496 ++++++++++++++++++++++++++++++- Cargo.toml | 4 + src/settings/account_settings.rs | 44 ++- src/sliding_sync.rs | 83 +++++- 4 files changed, 603 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e87a8380..73e88fd6d 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.15", + "wayland-client 0.31.14", + "wayland-protocols 0.32.12", + "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" @@ -1459,7 +1613,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -1469,6 +1623,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 +1639,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 +1757,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 +1797,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]] @@ -2800,7 +2992,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 +3348,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", @@ -3680,6 +3872,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" @@ -3949,6 +4150,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 +4279,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 +4458,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 +4496,26 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[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 +4678,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[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 +4832,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 +4980,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" @@ -4847,11 +5129,13 @@ dependencies = [ "matrix-sdk", "matrix-sdk-base", "matrix-sdk-ui", + "mime", "percent-encoding", "quinn", "rand 0.8.5", "rangemap", "reqwest", + "rfd", "robius-directories", "robius-location", "robius-open", @@ -5101,7 +5385,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -5448,6 +5732,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" @@ -5949,7 +6244,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -6405,6 +6700,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 +7084,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.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-sys 0.31.11", ] [[package]] @@ -6788,7 +7108,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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustix", + "wayland-backend 0.3.15", + "wayland-scanner", ] [[package]] @@ -6796,8 +7128,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 +7138,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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-backend 0.3.15", + "wayland-client 0.31.14", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", ] [[package]] @@ -6819,6 +7174,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.84" @@ -6883,7 +7249,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 +7850,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 +8010,44 @@ name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[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 8bd24357a..28eeb092f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ hashbrown = { version = "0.16", features = ["raw-entry"] } htmlize = "1.0.5" indexmap = "2.6.0" imghdr = "0.7.0" +mime = "0.3" 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 = [ @@ -78,6 +79,9 @@ tracing-subscriber = "0.3.17" unicode-segmentation = "1.11.0" url = "2.5.0" +[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] +rfd = "0.15" + ## Dependencies for TSP support. ## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219. diff --git a/src/settings/account_settings.rs b/src/settings/account_settings.rs index 4669039d4..b3226e7ec 100644 --- a/src/settings/account_settings.rs +++ b/src/settings/account_settings.rs @@ -1,6 +1,8 @@ use std::cell::RefCell; use makepad_widgets::{text::selection::Cursor, *}; +#[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] +use rfd::FileDialog; use crate::{app::ConfirmDeleteAction, avatar_cache::{self}, logout::logout_confirm_modal::{LogoutAction, LogoutConfirmModalAction}, profile::user_profile::UserProfile, shared::{avatar::{AvatarState, AvatarWidgetExt}, confirmation_modal::ConfirmationModalContent, popup_list::{PopupKind, enqueue_popup_notification}, styles::*}, sliding_sync::{AccountDataAction, MatrixRequest, submit_async_request}, utils}; @@ -376,17 +378,34 @@ impl MatchEvent for AccountSettings { let Some(own_profile) = &self.own_profile else { return }; if upload_avatar_button.clicked(actions) { - // TODO: uncomment the below once avatar uploading is implemented - // Self::enable_upload_avatar_button(cx, false, &upload_avatar_button); - // Self::enable_delete_avatar_button(cx, false, &delete_avatar_button); - enqueue_popup_notification( - "Avatar uploading is not yet implemented.", - PopupKind::Warning, - Some(4.0), - ); + #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] + { + if let Some(avatar_path) = FileDialog::new() + .add_filter("Image", &["png", "jpg", "jpeg"]) + .pick_file() + { + submit_async_request(MatrixRequest::UploadAvatar { avatar_path }); + cx.action(AccountSettingsAction::AvatarUploadStarted); + enqueue_popup_notification( + "Uploading avatar...", + PopupKind::Info, + Some(5.0), + ); + } + } + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + enqueue_popup_notification( + "Avatar uploading is not yet supported on this platform.", + PopupKind::Warning, + Some(4.0), + ); + } } if delete_avatar_button.clicked(actions) { + #[cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))] + { // Don't immediately disable the buttons. Instead, we wait for the user // to confirm the action in the confirmation modal, // and then we disable the buttons in the AvatarDeleteStarted action handler. @@ -406,6 +425,15 @@ impl MatchEvent for AccountSettings { ..Default::default() }; cx.action(ConfirmDeleteAction::Show(RefCell::new(Some(content)))); + } + #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] + { + enqueue_popup_notification( + "Deleting avatar is not yet supported on this platform.", + PopupKind::Warning, + Some(4.0), + ); + } } // Enable the name change buttons if the user modified the display name to be different. diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 29649c3fb..a89c121d5 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -6,6 +6,7 @@ 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 mime::{IMAGE_JPEG, IMAGE_PNG}; 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::{ @@ -14,7 +15,7 @@ use matrix_sdk::{ room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, directory::get_public_rooms_filtered, error::ErrorKind, - profile::{AvatarUrl, DisplayName}, + profile::{AvatarUrl, DisplayName, set_avatar_url}, receipt::create_receipt::v3::ReceiptType, uiaa::{AuthData, AuthType, Dummy}, }}, directory::{Filter as PublicRoomsFilter, RoomTypeFilter}, events::{ @@ -37,7 +38,7 @@ use tokio::{ 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::{borrow::Cow, cmp::{max, min}, future::Future, hash::{BuildHasherDefault, DefaultHasher}, iter::Peekable, ops::{Deref, DerefMut, Not}, path::{ Path, PathBuf }, sync::{Arc, LazyLock, Mutex}, time::Duration}; use std::io; use hashbrown::{HashMap, HashSet}; use crate::{ @@ -803,6 +804,11 @@ pub enum MatrixRequest { /// which is only needed because it isn't present in the `RoomMember` object. room_id: OwnedRoomId, }, + /// Request to upload and set the avatar of the current user's account. + UploadAvatar { + /// The path to a local PNG or JPEG image file. + avatar_path: PathBuf, + }, /// 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. @@ -1840,6 +1846,55 @@ async fn matrix_worker_task( }); } + MatrixRequest::UploadAvatar { avatar_path } => { + let Some(client) = get_client() else { continue }; + let _upload_avatar_task = Handle::current().spawn(async move { + let data = match std::fs::read(&avatar_path) { + Ok(data) => data, + Err(e) => { + Cx::post_action(AccountDataAction::AvatarChangeFailed( + format!("Failed to read selected avatar file {:?}: {e}", avatar_path) + )); + return; + } + }; + + let content_type = match imghdr::from_bytes(&data) { + Some(imghdr::Type::Png) => IMAGE_PNG, + Some(imghdr::Type::Jpeg) => IMAGE_JPEG, + _ => { + let ext = avatar_path + .extension() + .and_then(|e| e.to_str()) + .map(|e| e.to_ascii_lowercase()); + match ext.as_deref() { + Some("png") => IMAGE_PNG, + Some("jpg") | Some("jpeg") => IMAGE_JPEG, + _ => { + Cx::post_action(AccountDataAction::AvatarChangeFailed( + "Unsupported avatar format. Please choose a PNG or JPEG image.".to_string() + )); + return; + } + } + } + }; + + log!("Uploading avatar from file: {:?}", avatar_path); + match client.account().upload_avatar(&content_type, data).await { + Ok(new_avatar_uri) => { + log!("Successfully uploaded avatar."); + Cx::post_action(AccountDataAction::AvatarChanged(Some(new_avatar_uri))); + } + Err(e) => { + Cx::post_action(AccountDataAction::AvatarChangeFailed( + format!("Failed to upload avatar: {e}") + )); + } + } + }); + } + MatrixRequest::SetAvatar { avatar_url } => { let Some(client) = get_client() else { continue }; let _set_avatar_task = Handle::current().spawn(async move { @@ -1852,6 +1907,30 @@ async fn matrix_worker_task( Cx::post_action(AccountDataAction::AvatarChanged(avatar_url)); } Err(e) => { + if is_removing && e.client_api_error_kind() == Some(&ErrorKind::Unrecognized) { + log!("Avatar delete endpoint not recognized by homeserver, retrying fallback request..."); + let Some(user_id) = client.user_id() else { + Cx::post_action(AccountDataAction::AvatarChangeFailed( + "Failed to remove avatar: not authenticated.".to_string() + )); + return; + }; + #[allow(deprecated)] + let fallback_result = client.send( + set_avatar_url::v3::Request::new(user_id.to_owned(), None) + ).await; + match fallback_result { + Ok(_) => { + log!("Successfully removed avatar via fallback endpoint."); + Cx::post_action(AccountDataAction::AvatarChanged(None)); + } + Err(fallback_err) => { + let err_msg = format!("Failed to remove avatar: {fallback_err}"); + Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); + } + } + return; + } let err_msg = format!("Failed to {} avatar: {e}", if is_removing { "remove" } else { "set" }); Cx::post_action(AccountDataAction::AvatarChangeFailed(err_msg)); } From 0df3aca6900f0c772221d5506198c42b43ad51ee Mon Sep 17 00:00:00 2001 From: alanpoon Date: Wed, 1 Apr 2026 22:34:49 +0800 Subject: [PATCH 054/283] fix clippy --- src/app.rs | 1 + src/sliding_sync.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 1ec79ceda..a23a4ac8f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -473,6 +473,7 @@ fn init_file_logging() -> Option<()> { /// Writes a log message to the log file (if file logging is enabled). #[cfg(not(any(target_os = "android", target_os = "ios")))] +#[allow(dead_code)] fn write_to_log_file(message: &str) { if let Some(Some(file_mutex)) = LOG_FILE.get() { if let Ok(mut file) = file_mutex.lock() { diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 02a5cfaf9..e5f6d55ab 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -385,7 +385,7 @@ async fn login( } else { (cli, false) }; - let (client, client_session) = build_client(&cli, app_data_dir()).await?; + 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), From c2323655d5d340d8f4bdc8befbc7310160a842ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tyrese=20Luo=20=28=E7=BE=85=E5=81=A5=E5=B3=AF=29?= Date: Thu, 2 Apr 2026 01:17:04 +0800 Subject: [PATCH 055/283] feat(i18n): localize major UI copy across app screens - add English and Simplified Chinese translation resources - introduce i18n module and wire AppLanguage-driven text updates - replace hardcoded UI strings in home/login/settings/room/tsp flows --- resources/i18n/en.json | 420 ++++++++++++++++++++++++++ resources/i18n/zh-CN.json | 420 ++++++++++++++++++++++++++ src/app.rs | 53 +++- src/home/add_room.rs | 372 +++++++++++++++++------ src/home/home_screen.rs | 2 +- src/home/invite_modal.rs | 73 ++++- src/home/invite_screen.rs | 54 +++- src/home/loading_pane.rs | 81 +++-- src/home/room_context_menu.rs | 61 ++-- src/home/room_screen.rs | 446 ++++++++++++++++++++-------- src/home/rooms_list.rs | 37 +-- src/home/rooms_list_entry.rs | 29 +- src/home/rooms_list_header.rs | 42 ++- src/home/search_messages.rs | 21 ++ src/home/space_lobby.rs | 144 ++++++--- src/home/spaces_bar.rs | 54 +++- src/home/welcome_screen.rs | 64 +++- src/i18n.rs | 115 +++++++ src/lib.rs | 2 + src/login/login_screen.rs | 125 +++++--- src/room/room_input_bar.rs | 3 +- src/settings/account_settings.rs | 120 ++++++-- src/settings/bot_settings.rs | 60 +++- src/settings/settings_screen.rs | 299 ++++++++++++++++--- src/shared/collapsible_header.rs | 28 +- src/shared/room_filter_input_bar.rs | 24 ++ src/tsp/tsp_settings_screen.rs | 120 +++++--- src/tsp/wallet_entry/mod.rs | 55 ++-- src/tsp_dummy/mod.rs | 54 +++- 29 files changed, 2827 insertions(+), 551 deletions(-) create mode 100644 resources/i18n/en.json create mode 100644 resources/i18n/zh-CN.json create mode 100644 src/i18n.rs diff --git a/resources/i18n/en.json b/resources/i18n/en.json new file mode 100644 index 000000000..acdca7351 --- /dev/null +++ b/resources/i18n/en.json @@ -0,0 +1,420 @@ +{ + "settings.all_settings_title": "All Settings", + "settings.category.account": "Account", + "settings.category.preferences": "Preferences", + "settings.category.labs": "Labs", + "settings.preferences.language.title": "Language", + "settings.preferences.language.application_label": "Application language", + "settings.preferences.language.reload_hint": "The app will reload after selecting another language", + "language.option.english": "English", + "language.option.chinese_simplified": "Simplified Chinese", + + "login.title.login_to_robrix": "Login to Robrix", + "login.title.create_account": "Create your Robrix account", + "login.input.user_id": "User ID", + "login.input.password": "Password", + "login.input.confirm_password": "Confirm password", + "login.input.homeserver": "matrix.org", + "login.label.homeserver_optional": "Homeserver URL (optional)", + "login.button.login": "Login", + "login.button.create_account": "Create account", + "login.sso.prompt": "Or, login with an SSO provider:", + "login.account_prompt.no_account": "Don't have an account?", + "login.account_prompt.already_have": "Already have an account?", + "login.mode_toggle.sign_up_here": "Sign up here", + "login.mode_toggle.back_to_login": "Back to login", + "login.status.missing_user_id.title": "Missing User ID", + "login.status.missing_user_id.body": "Please enter a valid User ID.", + "login.status.missing_password.title": "Missing Password", + "login.status.missing_password.body": "Please enter a valid password.", + "login.status.password_mismatch.title": "Passwords do not match", + "login.status.password_mismatch.body": "Please enter the same password in both password fields.", + "login.status.creating_account.title": "Creating account...", + "login.status.creating_account.body": "Waiting for the homeserver to create your account...", + "login.status.logging_in.title": "Logging in...", + "login.status.logging_in.body": "Waiting for a login response...", + "login.status.logging_in_cli.title": "Logging in via CLI...", + "login.status.auto_logging_in_as_user": "Auto-logging in as user {user_id}...", + "login.status.account_creation_failed": "Account Creation Failed.", + "login.status.login_failed": "Login Failed.", + "login.status.okay": "Okay", + "login.status.cancel": "Cancel", + "login_status_modal.title": "Login Status", + "login_status_modal.button.cancel": "Cancel", + + "room_context_menu.button.mark_unread": "Mark as Unread", + "room_context_menu.button.mark_read": "Mark as Read", + "room_context_menu.button.favorite": "Favorite", + "room_context_menu.button.unfavorite": "Un-favorite", + "room_context_menu.button.set_low_priority": "Set Low Priority", + "room_context_menu.button.unset_low_priority": "Un-set Low Priority", + "room_context_menu.button.copy_link_to_room": "Copy Link to Room", + "room_context_menu.button.settings": "Settings", + "room_context_menu.button.notifications": "Notifications", + "room_context_menu.button.invite": "Invite", + "room_context_menu.button.bind_botfather": "Bind BotFather", + "room_context_menu.button.unbind_botfather": "Unbind BotFather", + "room_context_menu.button.leave_room": "Leave Room", + "room_context_menu.popup.settings_not_implemented": "The room settings page is not yet implemented.", + "room_context_menu.popup.notifications_not_implemented": "The room notifications page is not yet implemented.", + "room_context_menu.popup.removing_botfather": "Removing BotFather {bot_user_id} from this room...", + "room_context_menu.popup.inviting_botfather": "Inviting BotFather {bot_user_id} into this room...", + "room_context_menu.popup.bot_settings_unavailable": "Bot settings are unavailable right now.", + + "add_room.title": "Add/Explore Rooms and Spaces", + "add_room.section.create_new_room": "Create a new room:", + "add_room.section.add_friend": "Add a friend:", + "add_room.section.join_existing": "Join an existing room or space:", + "add_room.create_room.help.default": "Create a standalone room, or attach it under a space where you can create child rooms.", + "add_room.create_room.help.fixed_parent": "Enter a room name. It will be created directly in this space.", + "add_room.create_room.dropdown.no_space": "Create without a space", + "add_room.create_room.dropdown.hint.choose_space": "Choose a space where you have permission to create child rooms.", + "add_room.create_room.dropdown.hint.no_creatable_spaces": "No joined space currently allows you to create child rooms.", + "add_room.create_room.dropdown.hint.new_room_under": "New room will be added under: {selected_name}", + "add_room.create_room.dropdown.hint.default": "Create a standalone room, or choose a space from the dropdown.", + "add_room.create_room.input.placeholder": "Enter the new room name...", + "add_room.create_room.button.create": "Create room", + "add_room.create_room.button.syncing": "Syncing...", + "add_room.create_room.modal.title": "Create New Room", + "add_room.create_room.modal.subtitle": "Create a new room directly inside the selected space.", + "add_room.button.cancel": "Cancel", + "add_room.add_friend.help": "Enter a Matrix user ID to open or create a direct message room.", + "add_room.add_friend.input.placeholder": "Enter a Matrix user ID, like @alice:matrix.org...", + "add_room.add_friend.button": "Add friend", + "add_room.join.input.placeholder": "Enter alias, ID, or Matrix link...", + "add_room.join.button.go": "Go", + "add_room.join.help_html": "