7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 1 | #![allow(clippy::large_enum_variant)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 2 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 3 | use std::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 4 | collections::{HashMap, HashSet}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 5 | io::{self, Write, stdout}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 6 | path::{Path, PathBuf}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 7 | sync::Arc, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 8 | time::{Duration, Instant}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 9 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 10 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 11 | use clap::Parser; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 12 | use color_eyre::Result; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 13 | use crossterm::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 14 | event::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 15 | self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 16 | }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 17 | execute, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 18 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 19 | use futures_util::{StreamExt as _, pin_mut}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 20 | use imbl::Vector; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 21 | use layout::Flex; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 22 | use matrix_sdk::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 23 | AuthSession, Client, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 24 | ThreadingSupport, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 25 | authentication::matrix::MatrixSession, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 26 | config::StoreConfig, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 27 | encryption::{BackupDownloadStrategy, EncryptionSettings}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 28 | reqwest::Url, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 29 | ruma::{OwnedRoomId, api::client::room::create_room::v3::Request as CreateRoomRequest}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 30 | search_index::{SearchIndexGuard, SearchIndexStoreKind}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 31 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 32 | use matrix_sdk_base::{RoomStateFilter, event_cache::store::EventCacheStoreLockGuard}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 33 | use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, locks::Mutex}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 34 | use matrix_sdk_ui::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 35 | Timeline as SdkTimeline, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 36 | room_list_service::{self, State, filters::new_filter_non_left}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 37 | sync_service::SyncService, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 38 | timeline::{RoomExt as _, TimelineFocus, TimelineItem}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 39 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 40 | use ratatui::{DefaultTerminal, prelude::*, style::palette::tailwind, widgets::*}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 41 | use throbber_widgets_tui::{Throbber, ThrobberState}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 42 | use tokio::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 43 | spawn, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 44 | sync::mpsc::{Receiver, Sender, channel, error::TryRecvError}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 45 | task::JoinHandle, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 46 | time::timeout, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 47 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 48 | use tracing::{debug, error, warn}; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 49 | use tracing_subscriber::EnvFilter; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 50 | use widgets::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 51 | recovery::create_centered_throbber_area, room_view::RoomView, settings::SettingsView, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 52 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 53 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 54 | use crate::widgets::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 55 | create_room::CreateRoomView, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 56 | help::HelpView, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 57 | room_list::{ExtraRoomInfo, RoomInfos, RoomList, Rooms}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 58 | search::{ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 59 | indexing::{IndexingMessage, IndexingView}, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 60 | searching::SearchingView, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 61 | }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 62 | status::Status, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 63 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 64 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 65 | mod widgets; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 66 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 67 | const HEADER_BG: Color = tailwind::BLUE.c950; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 68 | const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 69 | const ALT_ROW_COLOR: Color = tailwind::SLATE.c900; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 70 | const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 71 | const TEXT_COLOR: Color = tailwind::SLATE.c200; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 72 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 73 | type Timelines = Arc<Mutex<HashMap<OwnedRoomId, Timeline>>>; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 74 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 75 | #[derive(Debug, Parser)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 76 | struct Cli { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 77 | /// The homeserver the client should connect to. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 78 | server_name: String, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 79 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 80 | /// The path where session specific data should be stored. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 81 | #[clap(default_value = "/tmp/")] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 82 | session_path: PathBuf, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 83 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 84 | /// Set the proxy that should be used for the connection. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 85 | #[clap(short, long, env = "PROXY")] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 86 | proxy: Option<Url>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 87 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 88 | /// Whether to *not* reload the `pos`ition sliding sync token from disk at |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 89 | /// start or not, for the room list sliding sync. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 90 | /// |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 91 | /// Set to false by default (i.e. reload the position from disk). |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 92 | #[clap(short, long, default_value_t = false)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 93 | dont_share_pos: bool, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 94 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 95 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 96 | #[derive(Default)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 97 | pub enum GlobalMode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 98 | /// The default mode, no popout screen is opened. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 99 | #[default] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 100 | Default, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 101 | /// Mode where we have opened the help screen. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 102 | Help, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 103 | /// Mode where we have opened the settings screen. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 104 | Settings { view: SettingsView }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 105 | /// Mode where we are shutting our tasks down and exiting multiverse. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 106 | Exiting { shutdown_task: JoinHandle<()> }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 107 | /// Mode where we have opened the create room screen |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 108 | CreateRoom { view: CreateRoomView }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 109 | /// Mode where we have opened the search screen |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 110 | Searching { view: SearchingView, is_global: bool }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 111 | /// Mode where we have opened the indexing screen |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 112 | Indexing { view: IndexingView }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 113 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 114 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 115 | /// Helper function to create a centered rect using up certain percentage of the |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 116 | /// available rect `r` |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 117 | fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 118 | let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 119 | let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 120 | let [area] = vertical.areas(area); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 121 | let [area] = horizontal.areas(area); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 122 | area |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 123 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 124 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 125 | #[tokio::main] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 126 | async fn main() -> Result<()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 127 | let cli = Cli::parse(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 128 | let file_writer = tracing_appender::rolling::hourly(&cli.session_path, "logs-"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 129 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 130 | tracing_subscriber::fmt() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 131 | .with_env_filter(EnvFilter::from_default_env()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 132 | .with_ansi(false) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 133 | .with_writer(file_writer) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 134 | .init(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 135 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 136 | color_eyre::install()?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 137 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 138 | let share_pos = !cli.dont_share_pos; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 139 | let client = configure_client(cli).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 140 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 141 | // Watch for errors in background jobs. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 142 | spawn({ |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 143 | let client = client.clone(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 144 | async move { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 145 | let mut recv = client.task_monitor().subscribe(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 146 | while let Ok(report) = recv.recv().await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 147 | error!(name = report.task.name, ?report.reason, "A background task has crashed!"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 148 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 149 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 150 | }); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 151 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 152 | let event_cache = client.event_cache(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 153 | event_cache.config_mut().experimental_auto_backpagination = true; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 154 | event_cache.subscribe()?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 155 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 156 | let terminal = ratatui::init(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 157 | execute!(stdout(), EnableMouseCapture)?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 158 | let mut app = App::new(client, share_pos).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 159 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 160 | app.run(terminal).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 161 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 162 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 163 | pub struct Timeline { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 164 | timeline: Arc<SdkTimeline>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 165 | items: Arc<Mutex<Vector<Arc<TimelineItem>>>>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 166 | task: JoinHandle<()>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 167 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 168 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 169 | #[derive(Default)] |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 170 | pub struct AppState { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 171 | /// What popup are we showing that is covering the majority of the screen, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 172 | /// mainly used for help and settings screens. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 173 | global_mode: GlobalMode, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 174 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 175 | /// State for a global throbber. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 176 | throbber_state: ThrobberState, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 177 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 178 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 179 | struct App { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 180 | /// Reference to the main SDK client. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 181 | client: Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 182 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 183 | /// The sync service used for synchronizing events. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 184 | sync_service: Arc<SyncService>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 185 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 186 | /// Timelines data structures for each room. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 187 | timelines: Timelines, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 188 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 189 | /// The room list widget on the left-hand side of the screen. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 190 | room_list: RoomList, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 191 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 192 | /// A view displaying the contents of the selected room, the widget on the |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 193 | /// right-hand side of the screen. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 194 | room_view: RoomView, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 195 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 196 | /// Task listening to room list service changes, and spawning timelines. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 197 | listen_task: JoinHandle<()>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 198 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 199 | /// Task that is indexing events for search. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 200 | indexing_task: JoinHandle<()>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 201 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 202 | /// Receiver that notifies when indexing is complete. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 203 | indexing_receiver: Receiver<(bool, IndexingMessage)>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 204 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 205 | /// The status widget at the bottom of the screen. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 206 | status: Status, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 207 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 208 | state: AppState, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 209 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 210 | last_tick: Instant, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 211 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 212 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 213 | impl App { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 214 | const TICK_RATE: Duration = Duration::from_millis(250); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 215 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 216 | async fn new(client: Client, share_pos: bool) -> Result<Self> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 217 | let sync_service = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 218 | Arc::new(SyncService::builder(client.clone()).with_share_pos(share_pos).build().await?); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 219 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 220 | let rooms = Rooms::default(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 221 | let room_infos = RoomInfos::default(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 222 | let timelines = Timelines::default(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 223 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 224 | let room_list_service = sync_service.room_list_service(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 225 | let all_rooms = room_list_service.all_rooms().await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 226 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 227 | let listen_task = spawn(Self::listen_task( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 228 | rooms.clone(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 229 | room_infos.clone(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 230 | timelines.clone(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 231 | all_rooms, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 232 | )); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 233 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 234 | // This will sync (with encryption) until an error happens or the program is |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 235 | // stopped. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 236 | sync_service.start().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 237 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 238 | let status = Status::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 239 | let room_list = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 240 | RoomList::new(client.clone(), rooms, room_infos, sync_service.clone(), status.handle()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 241 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 242 | let room_view = RoomView::new(client.clone(), timelines.clone(), status.handle()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 243 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 244 | let (indexing_sender, indexing_receiver) = channel::<(bool, IndexingMessage)>(1024); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 245 | let indexing_task = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 246 | spawn(App::indexing_task(client.clone(), indexing_sender, sync_service.clone())); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 247 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 248 | let indexing_view = IndexingView::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 249 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 250 | Ok(Self { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 251 | sync_service, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 252 | timelines, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 253 | room_list, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 254 | room_view, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 255 | client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 256 | listen_task, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 257 | indexing_task, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 258 | indexing_receiver, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 259 | status, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 260 | state: AppState { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 261 | global_mode: GlobalMode::Indexing { view: indexing_view }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 262 | ..Default::default() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 263 | }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 264 | last_tick: Instant::now(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 265 | }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 266 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 267 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 268 | async fn listen_task( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 269 | rooms: Rooms, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 270 | room_infos: RoomInfos, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 271 | timelines: Timelines, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 272 | all_rooms: room_list_service::RoomList, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 273 | ) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 274 | let (stream, entries_controller) = all_rooms.entries_with_dynamic_adapters(50_000); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 275 | entries_controller.set_filter(Box::new(new_filter_non_left())); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 276 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 277 | pin_mut!(stream); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 278 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 279 | let mut previous_rooms = HashSet::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 280 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 281 | while let Some(diffs) = stream.next().await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 282 | let all_rooms = { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 283 | // Apply the diffs to the list of room entries. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 284 | let mut rooms = rooms.lock(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 285 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 286 | for diff in diffs { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 287 | diff.apply(&mut rooms); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 288 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 289 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 290 | // Collect rooms early to release the room entries list lock. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 291 | (*rooms).clone() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 292 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 293 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 294 | let mut new_rooms = HashMap::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 295 | let mut new_timelines = Vec::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 296 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 297 | // Update all the room info for all rooms. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 298 | for room in all_rooms.iter() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 299 | let raw_name = room.name(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 300 | let display_name = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 301 | room.cached_display_name().map(|display_name| display_name.to_string()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 302 | let is_dm = room |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 303 | .is_direct() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 304 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 305 | .map_err(|err| { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 306 | warn!("couldn't figure whether a room is a DM or not: {err}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 307 | }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 308 | .ok(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 309 | room_infos.lock().insert( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 310 | room.room_id().to_owned(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 311 | ExtraRoomInfo { raw_name, display_name, is_dm }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 312 | ); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 313 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 314 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 315 | // Initialize all the new rooms. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 316 | for room in |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 317 | all_rooms.into_iter().filter(|room| !previous_rooms.contains(room.room_id())) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 318 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 319 | // Initialize the timeline. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 320 | let Ok(timeline) = room |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 321 | .timeline_builder() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 322 | .with_focus(TimelineFocus::Live { hide_threaded_events: true }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 323 | .build() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 324 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 325 | else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 326 | error!("error when creating default timeline"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 327 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 328 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 329 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 330 | // Save the timeline in the cache. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 331 | let (items, stream) = timeline.subscribe().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 332 | let items = Arc::new(Mutex::new(items)); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 333 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 334 | // Spawn a timeline task that will listen to all the timeline item changes. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 335 | let i = items.clone(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 336 | let timeline_task = spawn(async move { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 337 | pin_mut!(stream); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 338 | let items = i; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 339 | while let Some(diffs) = stream.next().await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 340 | let mut items = items.lock(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 341 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 342 | for diff in diffs { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 343 | diff.apply(&mut items); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 344 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 345 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 346 | }); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 347 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 348 | new_timelines.push(( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 349 | room.room_id().to_owned(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 350 | Timeline { timeline: Arc::new(timeline), items, task: timeline_task }, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 351 | )); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 352 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 353 | // Save the room list service room in the cache. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 354 | new_rooms.insert(room.room_id().to_owned(), room); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 355 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 356 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 357 | previous_rooms.extend(new_rooms.into_keys()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 358 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 359 | timelines.lock().extend(new_timelines); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 360 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 361 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 362 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 363 | async fn wait_for_room_sync( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 364 | update_sender: &Sender<(bool, IndexingMessage)>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 365 | sync_service: Arc<SyncService>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 366 | ) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 367 | let mut sync_subscriber = sync_service.room_list_service().state(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 368 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 369 | // Spin until there are rooms to index |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 370 | while let Some(state) = sync_subscriber.next().await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 371 | match state { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 372 | State::Running => return, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 373 | State::Terminated { from: _prev } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 374 | while let Err(e) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 375 | update_sender.send((true, IndexingMessage::Progress(0))).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 376 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 377 | debug!("Failed to send final message, trying again: {e:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 378 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 379 | return; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 380 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 381 | _ => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 382 | debug!("Sync service not running. Waiting to start indexing. {state:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 383 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 384 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 385 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 386 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 387 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 388 | async fn index_event_cache( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 389 | client: &Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 390 | update_sender: &Sender<(bool, IndexingMessage)>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 391 | store: &EventCacheStoreLockGuard, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 392 | search_index_guard: &mut SearchIndexGuard<'_>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 393 | mut count: usize, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 394 | ) -> Result<usize, ()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 395 | for room in client.rooms_filtered(RoomStateFilter::JOINED.union(RoomStateFilter::LEFT)) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 396 | let room_id = room.room_id(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 397 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 398 | let maybe_room_cache = room.event_cache().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 399 | let Ok((room_cache, _drop_handles)) = maybe_room_cache else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 400 | warn!("Failed to get RoomEventCache: {maybe_room_cache:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 401 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 402 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 403 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 404 | let redaction_rules = room.clone_info().room_version_rules_or_default().redaction; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 405 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 406 | let maybe_timeline_events = store.get_room_events(room_id, None, None).await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 407 | let Ok(timeline_events) = maybe_timeline_events else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 408 | warn!("Failed to get room's events: {maybe_timeline_events:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 409 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 410 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 411 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 412 | let no_of_events = timeline_events.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 413 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 414 | if let Err(err) = search_index_guard |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 415 | .bulk_handle_timeline_event( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 416 | timeline_events.clone().into_iter(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 417 | &room_cache, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 418 | room_id, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 419 | &redaction_rules, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 420 | ) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 421 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 422 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 423 | error!("Failed to handle event for indexing: {err}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 424 | let mut error = Some(err); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 425 | while let Some(err) = error.take() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 426 | if let Err(e) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 427 | update_sender.send((true, IndexingMessage::Error(err.to_string()))).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 428 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 429 | debug!("Failed to send final error message, trying again: {e:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 430 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 431 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 432 | return Err(()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 433 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 434 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 435 | count += no_of_events; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 436 | let _ = update_sender.send((false, IndexingMessage::Progress(count))).await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 437 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 438 | Ok(count) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 439 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 440 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 441 | async fn index_from_server( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 442 | client: &Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 443 | update_sender: &Sender<(bool, IndexingMessage)>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 444 | search_index_guard: &mut SearchIndexGuard<'_>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 445 | mut count: usize, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 446 | ) -> Result<usize, ()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 447 | let batch_size = 25; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 448 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 449 | let mut rooms = client.rooms_filtered(RoomStateFilter::JOINED); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 450 | let mut idx = 0; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 451 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 452 | while !rooms.is_empty() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 453 | let room = &rooms[idx]; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 454 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 455 | let room_id = room.room_id(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 456 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 457 | let maybe_room_cache = room.event_cache().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 458 | let Ok((room_cache, _drop_handles)) = maybe_room_cache else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 459 | warn!("Failed to get RoomEventCache: {maybe_room_cache:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 460 | idx = (idx + 1) % rooms.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 461 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 462 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 463 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 464 | let redaction_rules = room.clone_info().room_version_rules_or_default().redaction; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 465 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 466 | let Ok(pagination) = room_cache.pagination().run_backwards_until(batch_size).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 467 | else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 468 | error!("Failed to backpaginate {room_id}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 469 | idx = (idx + 1) % rooms.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 470 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 471 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 472 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 473 | let no_of_events = pagination.events.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 474 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 475 | if let Err(err) = search_index_guard |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 476 | .bulk_handle_timeline_event( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 477 | pagination.events.clone().into_iter(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 478 | &room_cache, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 479 | room_id, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 480 | &redaction_rules, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 481 | ) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 482 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 483 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 484 | warn!("Failed to handle event for indexing: {err}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 485 | let mut error = Some(err); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 486 | while let Some(err) = error.take() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 487 | if let Err(e) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 488 | update_sender.send((true, IndexingMessage::Error(err.to_string()))).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 489 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 490 | debug!("Failed to send final error message, trying again: {e:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 491 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 492 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 493 | return Err(()); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 494 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 495 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 496 | count += no_of_events; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 497 | let _ = update_sender.send((false, IndexingMessage::Progress(count))).await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 498 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 499 | if pagination.reached_start { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 500 | rooms.remove(idx); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 501 | let len = rooms.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 502 | if len > 0 { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 503 | idx %= len; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 504 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 505 | } else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 506 | idx = (idx + 1) % rooms.len(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 507 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 508 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 509 | Ok(count) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 510 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 511 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 512 | /// The sender sends (progress, done?, error?). |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 513 | async fn indexing_task( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 514 | client: Client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 515 | update_sender: Sender<(bool, IndexingMessage)>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 516 | sync_service: Arc<SyncService>, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 517 | ) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 518 | if timeout(Duration::from_secs(30), App::wait_for_room_sync(&update_sender, sync_service)) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 519 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 520 | .is_err() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 521 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 522 | debug!("Waiting for sync to run timed out. Quitting indexing task."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 523 | return; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 524 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 525 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 526 | let Ok(store) = client.event_cache_store().lock().await else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 527 | error!("Failed to get EventCacheStore"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 528 | return; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 529 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 530 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 531 | let mut search_index_guard = client.search_index().lock().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 532 | let count = 0; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 533 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 534 | debug!("Start indexing from the event cache."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 535 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 536 | // First index everything in the cache |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 537 | let Ok(count) = App::index_event_cache( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 538 | &client, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 539 | &update_sender, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 540 | store.as_clean().expect("Only one process should access the event cache store"), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 541 | &mut search_index_guard, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 542 | count, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 543 | ) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 544 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 545 | else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 546 | debug!("Quitting index task."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 547 | return; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 548 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 549 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 550 | // Now index from the server |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 551 | debug!("Start indexing from the server."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 552 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 553 | let Ok(count) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 554 | App::index_from_server(&client, &update_sender, &mut search_index_guard, count).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 555 | else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 556 | debug!("Quitting index task."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 557 | return; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 558 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 559 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 560 | while let Err(err) = update_sender.send((true, IndexingMessage::Progress(count))).await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 561 | debug!("couldn't send final update {err}, trying again."); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 562 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 563 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 564 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 565 | fn set_global_mode(&mut self, mode: GlobalMode) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 566 | self.state.global_mode = mode; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 567 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 568 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 569 | async fn handle_global_event(&mut self, event: Event) -> Result<bool> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 570 | use KeyCode::*; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 571 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 572 | match event { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 573 | Event::Key(KeyEvent { code: F(1), modifiers: KeyModifiers::NONE, .. }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 574 | self.set_global_mode(GlobalMode::Help) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 575 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 576 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 577 | Event::Key(KeyEvent { code: F(10), modifiers: KeyModifiers::NONE, .. }) => self |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 578 | .set_global_mode(GlobalMode::Settings { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 579 | view: SettingsView::new(self.client.clone(), self.sync_service.clone()), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 580 | }), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 581 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 582 | Event::Key(KeyEvent { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 583 | code: Char('j') | Down, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 584 | modifiers: KeyModifiers::CONTROL, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 585 | .. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 586 | }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 587 | self.room_list.next_room().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 588 | let room_id = self.room_list.get_selected_room_id(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 589 | self.room_view.set_selected_room(room_id); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 590 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 591 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 592 | Event::Key(KeyEvent { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 593 | code: Char('k') | Up, modifiers: KeyModifiers::CONTROL, .. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 594 | }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 595 | self.room_list.previous_room().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 596 | let room_id = self.room_list.get_selected_room_id(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 597 | self.room_view.set_selected_room(room_id); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 598 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 599 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 600 | Event::Key(KeyEvent { code: Char('m'), modifiers: KeyModifiers::ALT, .. }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 601 | self.room_view.mark_as_read().await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 602 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 603 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 604 | Event::Key(KeyEvent { code: Char('q'), modifiers: KeyModifiers::CONTROL, .. }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 605 | if !matches!(self.state.global_mode, GlobalMode::Default) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 606 | self.set_global_mode(GlobalMode::Default); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 607 | } else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 608 | return Ok(true); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 609 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 610 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 611 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 612 | Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('r'), .. }) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 613 | self.set_global_mode(GlobalMode::CreateRoom { view: CreateRoomView::new() }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 614 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 615 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 616 | Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('s'), .. }) => self |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 617 | .set_global_mode(GlobalMode::Searching { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 618 | view: SearchingView::new(false), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 619 | is_global: false, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 620 | }), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 621 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 622 | Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('g'), .. }) => self |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 623 | .set_global_mode(GlobalMode::Searching { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 624 | view: SearchingView::new(true), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 625 | is_global: true, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 626 | }), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 627 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 628 | _ => self.room_view.handle_event(event).await, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 629 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 630 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 631 | Ok(false) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 632 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 633 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 634 | fn on_tick(&mut self) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 635 | self.state.throbber_state.calc_next(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 636 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 637 | match &mut self.state.global_mode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 638 | GlobalMode::Help |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 639 | | GlobalMode::Default |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 640 | | GlobalMode::CreateRoom { .. } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 641 | | GlobalMode::Searching { .. } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 642 | | GlobalMode::Exiting { .. } => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 643 | GlobalMode::Settings { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 644 | view.on_tick(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 645 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 646 | GlobalMode::Indexing { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 647 | view.on_tick(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 648 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 649 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 650 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 651 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 652 | async fn render_loop(&mut self, mut terminal: DefaultTerminal) -> Result<()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 653 | use KeyCode::*; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 654 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 655 | let mut check_channel = true; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 656 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 657 | loop { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 658 | if check_channel { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 659 | match self.indexing_receiver.try_recv() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 660 | Ok((done, message)) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 661 | if !matches!(message, IndexingMessage::Error(_)) && done { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 662 | self.set_global_mode(GlobalMode::Default); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 663 | } else if let GlobalMode::Indexing { view } = &mut self.state.global_mode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 664 | view.set_message(message); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 665 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 666 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 667 | Err(TryRecvError::Disconnected) => check_channel = false, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 668 | Err(TryRecvError::Empty) => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 669 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 670 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 671 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 672 | terminal.draw(|f| f.render_widget(&mut *self, f.area()))?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 673 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 674 | if event::poll(Duration::from_millis(100))? { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 675 | let event = event::read()?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 676 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 677 | match &mut self.state.global_mode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 678 | GlobalMode::Default => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 679 | if self.handle_global_event(event).await? { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 680 | let sync_service = self.sync_service.clone(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 681 | let timelines = self.timelines.clone(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 682 | let listen_task = self.listen_task.abort_handle(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 683 | let indexing_task = self.indexing_task.abort_handle(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 684 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 685 | let shutdown_task = spawn(async move { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 686 | sync_service.stop().await; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 687 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 688 | listen_task.abort(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 689 | indexing_task.abort(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 690 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 691 | for timeline in timelines.lock().values() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 692 | timeline.task.abort(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 693 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 694 | }); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 695 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 696 | self.set_global_mode(GlobalMode::Exiting { shutdown_task }); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 697 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 698 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 699 | GlobalMode::Help => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 700 | if let Event::Key(key) = event |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 701 | && let KeyModifiers::NONE = key.modifiers |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 702 | && let Char('q') | Esc = key.code |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 703 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 704 | self.set_global_mode(GlobalMode::Default) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 705 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 706 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 707 | GlobalMode::Settings { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 708 | if let Event::Key(key) = event |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 709 | && view.handle_key_press(key).await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 710 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 711 | self.set_global_mode(GlobalMode::Default); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 712 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 713 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 714 | GlobalMode::CreateRoom { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 715 | if let Event::Key(key) = event |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 716 | && let KeyModifiers::NONE = key.modifiers |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 717 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 718 | match key.code { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 719 | Enter => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 720 | if let Some(room_name) = view.get_text() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 721 | let mut request = CreateRoomRequest::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 722 | request.name = Some(room_name); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 723 | if let Err(err) = self |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 724 | .sync_service |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 725 | .room_list_service() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 726 | .client() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 727 | .create_room(request) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 728 | .await |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 729 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 730 | error!("error while creating room: {err:?}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 731 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 732 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 733 | self.set_global_mode(GlobalMode::Default); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 734 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 735 | Esc => self.set_global_mode(GlobalMode::Default), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 736 | _ => view.handle_key_press(key), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 737 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 738 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 739 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 740 | GlobalMode::Searching { view, is_global } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 741 | if let Event::Key(key) = event { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 742 | match key.code { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 743 | Enter => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 744 | if let Some(query) = view.get_text() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 745 | if *is_global { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 746 | let mut search = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 747 | self.client.search_messages(query, 5).build(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 748 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 749 | let mut all_results = HashMap::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 750 | loop { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 751 | let Ok(results) = search.next_events().await else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 752 | continue; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 753 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 754 | let Some(results) = results else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 755 | break; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 756 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 757 | for (room_id, event_id) in results { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 758 | all_results |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 759 | .entry(room_id) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 760 | .or_insert_with(Vec::new) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 761 | .push(event_id); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 762 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 763 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 764 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 765 | view.set_results( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 766 | all_results |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 767 | .into_iter() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 768 | .map(|(room_id, events)| { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 769 | (Some(room_id), events) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 770 | }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 771 | .collect(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 772 | ); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 773 | } else if let Some((query, room)) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 774 | view.get_text().zip(self.room_view.room()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 775 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 776 | let mut room_search = room.search_messages(query, 5); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 777 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 778 | let mut all_results = Vec::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 779 | while let Some(results) = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 780 | room_search.next_events().await? |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 781 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 782 | all_results.extend(results); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 783 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 784 | view.set_results(vec![(None, all_results)]); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 785 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 786 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 787 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 788 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 789 | Esc => self.set_global_mode(GlobalMode::Default), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 790 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 791 | Up => view.list_state.previous(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 792 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 793 | Down => view.list_state.next(), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 794 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 795 | _ => view.handle_key_press(key), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 796 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 797 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 798 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 799 | GlobalMode::Indexing { .. } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 800 | if let Event::Key(key) = event |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 801 | && let KeyModifiers::NONE = key.modifiers |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 802 | && let Esc = key.code |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 803 | { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 804 | self.indexing_task.abort(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 805 | self.set_global_mode(GlobalMode::Default); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 806 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 807 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 808 | GlobalMode::Exiting { .. } => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 809 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 810 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 811 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 812 | match &self.state.global_mode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 813 | GlobalMode::Default |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 814 | | GlobalMode::Help |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 815 | | GlobalMode::CreateRoom { .. } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 816 | | GlobalMode::Searching { .. } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 817 | | GlobalMode::Indexing { .. } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 818 | | GlobalMode::Settings { .. } => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 819 | GlobalMode::Exiting { shutdown_task } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 820 | if shutdown_task.is_finished() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 821 | break; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 822 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 823 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 824 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 825 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 826 | if self.last_tick.elapsed() >= Self::TICK_RATE { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 827 | self.on_tick(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 828 | self.last_tick = Instant::now(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 829 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 830 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 831 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 832 | Ok(()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 833 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 834 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 835 | async fn run(&mut self, terminal: DefaultTerminal) -> Result<()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 836 | self.render_loop(terminal).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 837 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 838 | // At this point the user has exited the loop, so shut down the application. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 839 | ratatui::restore(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 840 | execute!(stdout(), DisableMouseCapture)?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 841 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 842 | Ok(()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 843 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 844 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 845 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 846 | impl Widget for &mut App { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 847 | /// Render the whole app. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 848 | fn render(self, area: Rect, buf: &mut Buffer) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 849 | // Create a space for header, room list and timeline and the footer. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 850 | let vertical = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 851 | Layout::vertical([Constraint::Length(2), Constraint::Min(0), Constraint::Length(1)]); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 852 | let [header_area, rest_area, status_area] = vertical.areas(area); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 853 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 854 | // Create two chunks with equal horizontal screen space. One for the list and |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 855 | // the other for the info block. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 856 | let horizontal = |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 857 | Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)]); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 858 | let [room_list_area, room_view_area] = horizontal.areas(rest_area); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 859 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 860 | self.render_title(header_area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 861 | self.room_list.render(room_list_area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 862 | self.room_view.render(room_view_area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 863 | self.status.render(status_area, buf, &mut self.state); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 864 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 865 | match &mut self.state.global_mode { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 866 | GlobalMode::Default => {} |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 867 | GlobalMode::Exiting { .. } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 868 | Clear.render(rest_area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 869 | let centered = create_centered_throbber_area(area); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 870 | let throbber = Throbber::default() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 871 | .label("Exiting") |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 872 | .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 873 | StatefulWidget::render(throbber, centered, buf, &mut self.state.throbber_state); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 874 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 875 | GlobalMode::Settings { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 876 | view.render(area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 877 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 878 | GlobalMode::Help => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 879 | let mut help_view = HelpView::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 880 | help_view.render(area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 881 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 882 | GlobalMode::CreateRoom { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 883 | view.render(area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 884 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 885 | GlobalMode::Searching { view, .. } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 886 | view.render(room_view_area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 887 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 888 | GlobalMode::Indexing { view } => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 889 | view.render(area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 890 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 891 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 892 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 893 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 894 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 895 | impl App { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 896 | /// Render the top square (title of the program). |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 897 | fn render_title(&self, area: Rect, buf: &mut Buffer) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 898 | Paragraph::new("Multiverse").bold().centered().render(area, buf); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 899 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 900 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 901 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 902 | /// Configure the client so it's ready for sync'ing. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 903 | /// |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 904 | /// Will log in or reuse a previous session. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 905 | async fn configure_client(cli: Cli) -> Result<Client> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 906 | let Cli { server_name, session_path, proxy, dont_share_pos: _ } = cli; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 907 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 908 | let mut client_builder = Client::builder() |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 909 | .store_config( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 910 | StoreConfig::new(CrossProcessLockConfig::multi_process("multiverse")) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 911 | .crypto_store(SqliteCryptoStore::open(session_path.join("crypto"), None).await?) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 912 | .state_store(SqliteStateStore::open(session_path.join("state"), None).await?) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 913 | .event_cache_store( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 914 | SqliteEventCacheStore::open(session_path.join("cache"), None).await?, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 915 | ), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 916 | ) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 917 | .server_name_or_homeserver_url(&server_name) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 918 | .with_encryption_settings(EncryptionSettings { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 919 | auto_enable_cross_signing: true, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 920 | backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 921 | auto_enable_backups: true, |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 922 | }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 923 | .with_enable_share_history_on_invite(true) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 924 | .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true }) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 925 | .search_index_store(SearchIndexStoreKind::UnencryptedDirectory( |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 926 | session_path.join("indexData"), |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 927 | )); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 928 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 929 | if let Some(proxy_url) = proxy { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 930 | client_builder = client_builder.proxy(proxy_url).disable_ssl_verification(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 931 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 932 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 933 | let client = client_builder.build().await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 934 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 935 | // Try reading a session, otherwise create a new one. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 936 | log_in_or_restore_session(&client, &session_path).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 937 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 938 | Ok(client) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 939 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 940 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 941 | async fn log_in_or_restore_session(client: &Client, session_path: &Path) -> Result<()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 942 | let session_path = session_path.join("session.json"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 943 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 944 | if let Ok(serialized) = std::fs::read_to_string(&session_path) { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 945 | let session: MatrixSession = serde_json::from_str(&serialized)?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 946 | client.restore_session(session).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 947 | } else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 948 | login_with_password(client).await?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 949 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 950 | // Immediately save the session to disk. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 951 | if let Some(session) = client.session() { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 952 | let AuthSession::Matrix(session) = session else { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 953 | panic!("unexpected OAuth 2.0 session") |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 954 | }; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 955 | let serialized = serde_json::to_string(&session)?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 956 | std::fs::write(session_path, serialized)?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 957 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 958 | println!("saved session"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 959 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 960 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 961 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 962 | Ok(()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 963 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 964 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 965 | /// Asks the user of a username and password, and try to login using the matrix |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 966 | /// auth with those. |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 967 | async fn login_with_password(client: &Client) -> Result<()> { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 968 | println!("Logging in with username and password…"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 969 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 970 | loop { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 971 | print!("\nUsername: "); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 972 | stdout().flush().expect("Unable to write to stdout"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 973 | let mut username = String::new(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 974 | io::stdin().read_line(&mut username).expect("Unable to read user input"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 975 | username = username.trim().to_owned(); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 976 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 977 | let password = rpassword::prompt_password("Password.")?; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 978 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 979 | match client.matrix_auth().login_username(&username, password.trim()).await { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 980 | Ok(_) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 981 | println!("Logged in as {username}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 982 | break; |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 983 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 984 | Err(error) => { |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 985 | println!("Error logging in: {error}"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 986 | println!("Please try again\n"); |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 987 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 988 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 989 | } |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 990 | |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 991 | Ok(()) |
7ff8adafe9e4 | atagen | 2026-10-22 12:30 UTC | 992 | } |