atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: labs/multiverse/src/main.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
#![allow(clippy::large_enum_variant)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
use std::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
    collections::{HashMap, HashSet},
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
    io::{self, Write, stdout},
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
    path::{Path, PathBuf},
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
    sync::Arc,
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
    time::{Duration, Instant},
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
use clap::Parser;
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
use color_eyre::Result;
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
use crossterm::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
    event::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
        self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
    execute,
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
use futures_util::{StreamExt as _, pin_mut};
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
use imbl::Vector;
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
use layout::Flex;
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
use matrix_sdk::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
    AuthSession, Client, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore,
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
    ThreadingSupport,
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
    authentication::matrix::MatrixSession,
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
    config::StoreConfig,
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
    encryption::{BackupDownloadStrategy, EncryptionSettings},
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
    reqwest::Url,
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
    ruma::{OwnedRoomId, api::client::room::create_room::v3::Request as CreateRoomRequest},
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
    search_index::{SearchIndexGuard, SearchIndexStoreKind},
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
use matrix_sdk_base::{RoomStateFilter, event_cache::store::EventCacheStoreLockGuard};
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, locks::Mutex};
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
use matrix_sdk_ui::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
    Timeline as SdkTimeline,
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
    room_list_service::{self, State, filters::new_filter_non_left},
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
    sync_service::SyncService,
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
    timeline::{RoomExt as _, TimelineFocus, TimelineItem},
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
use ratatui::{DefaultTerminal, prelude::*, style::palette::tailwind, widgets::*};
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
use throbber_widgets_tui::{Throbber, ThrobberState};
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
use tokio::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
    spawn,
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
    sync::mpsc::{Receiver, Sender, channel, error::TryRecvError},
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
    task::JoinHandle,
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
    time::timeout,
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
use tracing::{debug, error, warn};
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
use tracing_subscriber::EnvFilter;
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
use widgets::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
    recovery::create_centered_throbber_area, room_view::RoomView, settings::SettingsView,
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
use crate::widgets::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
    create_room::CreateRoomView,
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
    help::HelpView,
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
    room_list::{ExtraRoomInfo, RoomInfos, RoomList, Rooms},
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
    search::{
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
        indexing::{IndexingMessage, IndexingView},
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
        searching::SearchingView,
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
    },
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
    status::Status,
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
};
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
mod widgets;
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
const HEADER_BG: Color = tailwind::BLUE.c950;
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
const TEXT_COLOR: Color = tailwind::SLATE.c200;
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
type Timelines = Arc<Mutex<HashMap<OwnedRoomId, Timeline>>>;
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
#[derive(Debug, Parser)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
struct Cli {
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
    /// The homeserver the client should connect to.
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
    server_name: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
    /// The path where session specific data should be stored.
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
    #[clap(default_value = "/tmp/")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
    session_path: PathBuf,
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
    /// Set the proxy that should be used for the connection.
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
    #[clap(short, long, env = "PROXY")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
    proxy: Option<Url>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
    /// Whether to *not* reload the `pos`ition sliding sync token from disk at
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
    /// start or not, for the room list sliding sync.
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
    /// Set to false by default (i.e. reload the position from disk).
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
    #[clap(short, long, default_value_t = false)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
    dont_share_pos: bool,
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
#[derive(Default)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
pub enum GlobalMode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
    /// The default mode, no popout screen is opened.
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
    #[default]
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
    Default,
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
    /// Mode where we have opened the help screen.
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
    Help,
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
    /// Mode where we have opened the settings screen.
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
    Settings { view: SettingsView },
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
    /// Mode where we are shutting our tasks down and exiting multiverse.
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
    Exiting { shutdown_task: JoinHandle<()> },
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
    /// Mode where we have opened the create room screen
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
    CreateRoom { view: CreateRoomView },
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
    /// Mode where we have opened the search screen
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
    Searching { view: SearchingView, is_global: bool },
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
    /// Mode where we have opened the indexing screen
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
    Indexing { view: IndexingView },
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
/// Helper function to create a centered rect using up certain percentage of the
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
/// available rect `r`
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
    let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
    let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
    let [area] = vertical.areas(area);
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
    let [area] = horizontal.areas(area);
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
    area
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
#[tokio::main]
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
async fn main() -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
    let cli = Cli::parse();
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
    let file_writer = tracing_appender::rolling::hourly(&cli.session_path, "logs-");
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
    tracing_subscriber::fmt()
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
        .with_env_filter(EnvFilter::from_default_env())
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
        .with_ansi(false)
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
        .with_writer(file_writer)
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
        .init();
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
    color_eyre::install()?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
    let share_pos = !cli.dont_share_pos;
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
    let client = configure_client(cli).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
    // Watch for errors in background jobs.
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
    spawn({
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
        let client = client.clone();
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
        async move {
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
            let mut recv = client.task_monitor().subscribe();
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
            while let Ok(report) = recv.recv().await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
                error!(name = report.task.name, ?report.reason, "A background task has crashed!");
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
    });
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
    let event_cache = client.event_cache();
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
    event_cache.config_mut().experimental_auto_backpagination = true;
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
    event_cache.subscribe()?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
    let terminal = ratatui::init();
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
    execute!(stdout(), EnableMouseCapture)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
    let mut app = App::new(client, share_pos).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
    app.run(terminal).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
pub struct Timeline {
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
    timeline: Arc<SdkTimeline>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC165
    items: Arc<Mutex<Vector<Arc<TimelineItem>>>>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC166
    task: JoinHandle<()>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC167
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC168
7ff8adafe9e4atagen2026-10-22 12:30 UTC169
#[derive(Default)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC170
pub struct AppState {
7ff8adafe9e4atagen2026-10-22 12:30 UTC171
    /// What popup are we showing that is covering the majority of the screen,
7ff8adafe9e4atagen2026-10-22 12:30 UTC172
    /// mainly used for help and settings screens.
7ff8adafe9e4atagen2026-10-22 12:30 UTC173
    global_mode: GlobalMode,
7ff8adafe9e4atagen2026-10-22 12:30 UTC174
7ff8adafe9e4atagen2026-10-22 12:30 UTC175
    /// State for a global throbber.
7ff8adafe9e4atagen2026-10-22 12:30 UTC176
    throbber_state: ThrobberState,
7ff8adafe9e4atagen2026-10-22 12:30 UTC177
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC178
7ff8adafe9e4atagen2026-10-22 12:30 UTC179
struct App {
7ff8adafe9e4atagen2026-10-22 12:30 UTC180
    /// Reference to the main SDK client.
7ff8adafe9e4atagen2026-10-22 12:30 UTC181
    client: Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC182
7ff8adafe9e4atagen2026-10-22 12:30 UTC183
    /// The sync service used for synchronizing events.
7ff8adafe9e4atagen2026-10-22 12:30 UTC184
    sync_service: Arc<SyncService>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC185
7ff8adafe9e4atagen2026-10-22 12:30 UTC186
    /// Timelines data structures for each room.
7ff8adafe9e4atagen2026-10-22 12:30 UTC187
    timelines: Timelines,
7ff8adafe9e4atagen2026-10-22 12:30 UTC188
7ff8adafe9e4atagen2026-10-22 12:30 UTC189
    /// The room list widget on the left-hand side of the screen.
7ff8adafe9e4atagen2026-10-22 12:30 UTC190
    room_list: RoomList,
7ff8adafe9e4atagen2026-10-22 12:30 UTC191
7ff8adafe9e4atagen2026-10-22 12:30 UTC192
    /// A view displaying the contents of the selected room, the widget on the
7ff8adafe9e4atagen2026-10-22 12:30 UTC193
    /// right-hand side of the screen.
7ff8adafe9e4atagen2026-10-22 12:30 UTC194
    room_view: RoomView,
7ff8adafe9e4atagen2026-10-22 12:30 UTC195
7ff8adafe9e4atagen2026-10-22 12:30 UTC196
    /// Task listening to room list service changes, and spawning timelines.
7ff8adafe9e4atagen2026-10-22 12:30 UTC197
    listen_task: JoinHandle<()>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC198
7ff8adafe9e4atagen2026-10-22 12:30 UTC199
    /// Task that is indexing events for search.
7ff8adafe9e4atagen2026-10-22 12:30 UTC200
    indexing_task: JoinHandle<()>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC201
7ff8adafe9e4atagen2026-10-22 12:30 UTC202
    /// Receiver that notifies when indexing is complete.
7ff8adafe9e4atagen2026-10-22 12:30 UTC203
    indexing_receiver: Receiver<(bool, IndexingMessage)>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC204
7ff8adafe9e4atagen2026-10-22 12:30 UTC205
    /// The status widget at the bottom of the screen.
7ff8adafe9e4atagen2026-10-22 12:30 UTC206
    status: Status,
7ff8adafe9e4atagen2026-10-22 12:30 UTC207
7ff8adafe9e4atagen2026-10-22 12:30 UTC208
    state: AppState,
7ff8adafe9e4atagen2026-10-22 12:30 UTC209
7ff8adafe9e4atagen2026-10-22 12:30 UTC210
    last_tick: Instant,
7ff8adafe9e4atagen2026-10-22 12:30 UTC211
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC212
7ff8adafe9e4atagen2026-10-22 12:30 UTC213
impl App {
7ff8adafe9e4atagen2026-10-22 12:30 UTC214
    const TICK_RATE: Duration = Duration::from_millis(250);
7ff8adafe9e4atagen2026-10-22 12:30 UTC215
7ff8adafe9e4atagen2026-10-22 12:30 UTC216
    async fn new(client: Client, share_pos: bool) -> Result<Self> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC217
        let sync_service =
7ff8adafe9e4atagen2026-10-22 12:30 UTC218
            Arc::new(SyncService::builder(client.clone()).with_share_pos(share_pos).build().await?);
7ff8adafe9e4atagen2026-10-22 12:30 UTC219
7ff8adafe9e4atagen2026-10-22 12:30 UTC220
        let rooms = Rooms::default();
7ff8adafe9e4atagen2026-10-22 12:30 UTC221
        let room_infos = RoomInfos::default();
7ff8adafe9e4atagen2026-10-22 12:30 UTC222
        let timelines = Timelines::default();
7ff8adafe9e4atagen2026-10-22 12:30 UTC223
7ff8adafe9e4atagen2026-10-22 12:30 UTC224
        let room_list_service = sync_service.room_list_service();
7ff8adafe9e4atagen2026-10-22 12:30 UTC225
        let all_rooms = room_list_service.all_rooms().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC226
7ff8adafe9e4atagen2026-10-22 12:30 UTC227
        let listen_task = spawn(Self::listen_task(
7ff8adafe9e4atagen2026-10-22 12:30 UTC228
            rooms.clone(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC229
            room_infos.clone(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC230
            timelines.clone(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC231
            all_rooms,
7ff8adafe9e4atagen2026-10-22 12:30 UTC232
        ));
7ff8adafe9e4atagen2026-10-22 12:30 UTC233
7ff8adafe9e4atagen2026-10-22 12:30 UTC234
        // This will sync (with encryption) until an error happens or the program is
7ff8adafe9e4atagen2026-10-22 12:30 UTC235
        // stopped.
7ff8adafe9e4atagen2026-10-22 12:30 UTC236
        sync_service.start().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC237
7ff8adafe9e4atagen2026-10-22 12:30 UTC238
        let status = Status::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC239
        let room_list =
7ff8adafe9e4atagen2026-10-22 12:30 UTC240
            RoomList::new(client.clone(), rooms, room_infos, sync_service.clone(), status.handle());
7ff8adafe9e4atagen2026-10-22 12:30 UTC241
7ff8adafe9e4atagen2026-10-22 12:30 UTC242
        let room_view = RoomView::new(client.clone(), timelines.clone(), status.handle());
7ff8adafe9e4atagen2026-10-22 12:30 UTC243
7ff8adafe9e4atagen2026-10-22 12:30 UTC244
        let (indexing_sender, indexing_receiver) = channel::<(bool, IndexingMessage)>(1024);
7ff8adafe9e4atagen2026-10-22 12:30 UTC245
        let indexing_task =
7ff8adafe9e4atagen2026-10-22 12:30 UTC246
            spawn(App::indexing_task(client.clone(), indexing_sender, sync_service.clone()));
7ff8adafe9e4atagen2026-10-22 12:30 UTC247
7ff8adafe9e4atagen2026-10-22 12:30 UTC248
        let indexing_view = IndexingView::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC249
7ff8adafe9e4atagen2026-10-22 12:30 UTC250
        Ok(Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC251
            sync_service,
7ff8adafe9e4atagen2026-10-22 12:30 UTC252
            timelines,
7ff8adafe9e4atagen2026-10-22 12:30 UTC253
            room_list,
7ff8adafe9e4atagen2026-10-22 12:30 UTC254
            room_view,
7ff8adafe9e4atagen2026-10-22 12:30 UTC255
            client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC256
            listen_task,
7ff8adafe9e4atagen2026-10-22 12:30 UTC257
            indexing_task,
7ff8adafe9e4atagen2026-10-22 12:30 UTC258
            indexing_receiver,
7ff8adafe9e4atagen2026-10-22 12:30 UTC259
            status,
7ff8adafe9e4atagen2026-10-22 12:30 UTC260
            state: AppState {
7ff8adafe9e4atagen2026-10-22 12:30 UTC261
                global_mode: GlobalMode::Indexing { view: indexing_view },
7ff8adafe9e4atagen2026-10-22 12:30 UTC262
                ..Default::default()
7ff8adafe9e4atagen2026-10-22 12:30 UTC263
            },
7ff8adafe9e4atagen2026-10-22 12:30 UTC264
            last_tick: Instant::now(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC265
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC266
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC267
7ff8adafe9e4atagen2026-10-22 12:30 UTC268
    async fn listen_task(
7ff8adafe9e4atagen2026-10-22 12:30 UTC269
        rooms: Rooms,
7ff8adafe9e4atagen2026-10-22 12:30 UTC270
        room_infos: RoomInfos,
7ff8adafe9e4atagen2026-10-22 12:30 UTC271
        timelines: Timelines,
7ff8adafe9e4atagen2026-10-22 12:30 UTC272
        all_rooms: room_list_service::RoomList,
7ff8adafe9e4atagen2026-10-22 12:30 UTC273
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC274
        let (stream, entries_controller) = all_rooms.entries_with_dynamic_adapters(50_000);
7ff8adafe9e4atagen2026-10-22 12:30 UTC275
        entries_controller.set_filter(Box::new(new_filter_non_left()));
7ff8adafe9e4atagen2026-10-22 12:30 UTC276
7ff8adafe9e4atagen2026-10-22 12:30 UTC277
        pin_mut!(stream);
7ff8adafe9e4atagen2026-10-22 12:30 UTC278
7ff8adafe9e4atagen2026-10-22 12:30 UTC279
        let mut previous_rooms = HashSet::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC280
7ff8adafe9e4atagen2026-10-22 12:30 UTC281
        while let Some(diffs) = stream.next().await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC282
            let all_rooms = {
7ff8adafe9e4atagen2026-10-22 12:30 UTC283
                // Apply the diffs to the list of room entries.
7ff8adafe9e4atagen2026-10-22 12:30 UTC284
                let mut rooms = rooms.lock();
7ff8adafe9e4atagen2026-10-22 12:30 UTC285
7ff8adafe9e4atagen2026-10-22 12:30 UTC286
                for diff in diffs {
7ff8adafe9e4atagen2026-10-22 12:30 UTC287
                    diff.apply(&mut rooms);
7ff8adafe9e4atagen2026-10-22 12:30 UTC288
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC289
7ff8adafe9e4atagen2026-10-22 12:30 UTC290
                // Collect rooms early to release the room entries list lock.
7ff8adafe9e4atagen2026-10-22 12:30 UTC291
                (*rooms).clone()
7ff8adafe9e4atagen2026-10-22 12:30 UTC292
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC293
7ff8adafe9e4atagen2026-10-22 12:30 UTC294
            let mut new_rooms = HashMap::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC295
            let mut new_timelines = Vec::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC296
7ff8adafe9e4atagen2026-10-22 12:30 UTC297
            // Update all the room info for all rooms.
7ff8adafe9e4atagen2026-10-22 12:30 UTC298
            for room in all_rooms.iter() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC299
                let raw_name = room.name();
7ff8adafe9e4atagen2026-10-22 12:30 UTC300
                let display_name =
7ff8adafe9e4atagen2026-10-22 12:30 UTC301
                    room.cached_display_name().map(|display_name| display_name.to_string());
7ff8adafe9e4atagen2026-10-22 12:30 UTC302
                let is_dm = room
7ff8adafe9e4atagen2026-10-22 12:30 UTC303
                    .is_direct()
7ff8adafe9e4atagen2026-10-22 12:30 UTC304
                    .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC305
                    .map_err(|err| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC306
                        warn!("couldn't figure whether a room is a DM or not: {err}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC307
                    })
7ff8adafe9e4atagen2026-10-22 12:30 UTC308
                    .ok();
7ff8adafe9e4atagen2026-10-22 12:30 UTC309
                room_infos.lock().insert(
7ff8adafe9e4atagen2026-10-22 12:30 UTC310
                    room.room_id().to_owned(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC311
                    ExtraRoomInfo { raw_name, display_name, is_dm },
7ff8adafe9e4atagen2026-10-22 12:30 UTC312
                );
7ff8adafe9e4atagen2026-10-22 12:30 UTC313
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC314
7ff8adafe9e4atagen2026-10-22 12:30 UTC315
            // Initialize all the new rooms.
7ff8adafe9e4atagen2026-10-22 12:30 UTC316
            for room in
7ff8adafe9e4atagen2026-10-22 12:30 UTC317
                all_rooms.into_iter().filter(|room| !previous_rooms.contains(room.room_id()))
7ff8adafe9e4atagen2026-10-22 12:30 UTC318
            {
7ff8adafe9e4atagen2026-10-22 12:30 UTC319
                // Initialize the timeline.
7ff8adafe9e4atagen2026-10-22 12:30 UTC320
                let Ok(timeline) = room
7ff8adafe9e4atagen2026-10-22 12:30 UTC321
                    .timeline_builder()
7ff8adafe9e4atagen2026-10-22 12:30 UTC322
                    .with_focus(TimelineFocus::Live { hide_threaded_events: true })
7ff8adafe9e4atagen2026-10-22 12:30 UTC323
                    .build()
7ff8adafe9e4atagen2026-10-22 12:30 UTC324
                    .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC325
                else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC326
                    error!("error when creating default timeline");
7ff8adafe9e4atagen2026-10-22 12:30 UTC327
                    continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC328
                };
7ff8adafe9e4atagen2026-10-22 12:30 UTC329
7ff8adafe9e4atagen2026-10-22 12:30 UTC330
                // Save the timeline in the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC331
                let (items, stream) = timeline.subscribe().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC332
                let items = Arc::new(Mutex::new(items));
7ff8adafe9e4atagen2026-10-22 12:30 UTC333
7ff8adafe9e4atagen2026-10-22 12:30 UTC334
                // Spawn a timeline task that will listen to all the timeline item changes.
7ff8adafe9e4atagen2026-10-22 12:30 UTC335
                let i = items.clone();
7ff8adafe9e4atagen2026-10-22 12:30 UTC336
                let timeline_task = spawn(async move {
7ff8adafe9e4atagen2026-10-22 12:30 UTC337
                    pin_mut!(stream);
7ff8adafe9e4atagen2026-10-22 12:30 UTC338
                    let items = i;
7ff8adafe9e4atagen2026-10-22 12:30 UTC339
                    while let Some(diffs) = stream.next().await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC340
                        let mut items = items.lock();
7ff8adafe9e4atagen2026-10-22 12:30 UTC341
7ff8adafe9e4atagen2026-10-22 12:30 UTC342
                        for diff in diffs {
7ff8adafe9e4atagen2026-10-22 12:30 UTC343
                            diff.apply(&mut items);
7ff8adafe9e4atagen2026-10-22 12:30 UTC344
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC345
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC346
                });
7ff8adafe9e4atagen2026-10-22 12:30 UTC347
7ff8adafe9e4atagen2026-10-22 12:30 UTC348
                new_timelines.push((
7ff8adafe9e4atagen2026-10-22 12:30 UTC349
                    room.room_id().to_owned(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC350
                    Timeline { timeline: Arc::new(timeline), items, task: timeline_task },
7ff8adafe9e4atagen2026-10-22 12:30 UTC351
                ));
7ff8adafe9e4atagen2026-10-22 12:30 UTC352
7ff8adafe9e4atagen2026-10-22 12:30 UTC353
                // Save the room list service room in the cache.
7ff8adafe9e4atagen2026-10-22 12:30 UTC354
                new_rooms.insert(room.room_id().to_owned(), room);
7ff8adafe9e4atagen2026-10-22 12:30 UTC355
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC356
7ff8adafe9e4atagen2026-10-22 12:30 UTC357
            previous_rooms.extend(new_rooms.into_keys());
7ff8adafe9e4atagen2026-10-22 12:30 UTC358
7ff8adafe9e4atagen2026-10-22 12:30 UTC359
            timelines.lock().extend(new_timelines);
7ff8adafe9e4atagen2026-10-22 12:30 UTC360
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC361
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC362
7ff8adafe9e4atagen2026-10-22 12:30 UTC363
    async fn wait_for_room_sync(
7ff8adafe9e4atagen2026-10-22 12:30 UTC364
        update_sender: &Sender<(bool, IndexingMessage)>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC365
        sync_service: Arc<SyncService>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC366
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC367
        let mut sync_subscriber = sync_service.room_list_service().state();
7ff8adafe9e4atagen2026-10-22 12:30 UTC368
7ff8adafe9e4atagen2026-10-22 12:30 UTC369
        // Spin until there are rooms to index
7ff8adafe9e4atagen2026-10-22 12:30 UTC370
        while let Some(state) = sync_subscriber.next().await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC371
            match state {
7ff8adafe9e4atagen2026-10-22 12:30 UTC372
                State::Running => return,
7ff8adafe9e4atagen2026-10-22 12:30 UTC373
                State::Terminated { from: _prev } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC374
                    while let Err(e) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC375
                        update_sender.send((true, IndexingMessage::Progress(0))).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC376
                    {
7ff8adafe9e4atagen2026-10-22 12:30 UTC377
                        debug!("Failed to send final message, trying again: {e:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC378
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC379
                    return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC380
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC381
                _ => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC382
                    debug!("Sync service not running. Waiting to start indexing. {state:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC383
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC384
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC385
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC386
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC387
7ff8adafe9e4atagen2026-10-22 12:30 UTC388
    async fn index_event_cache(
7ff8adafe9e4atagen2026-10-22 12:30 UTC389
        client: &Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC390
        update_sender: &Sender<(bool, IndexingMessage)>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC391
        store: &EventCacheStoreLockGuard,
7ff8adafe9e4atagen2026-10-22 12:30 UTC392
        search_index_guard: &mut SearchIndexGuard<'_>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC393
        mut count: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC394
    ) -> Result<usize, ()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC395
        for room in client.rooms_filtered(RoomStateFilter::JOINED.union(RoomStateFilter::LEFT)) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC396
            let room_id = room.room_id();
7ff8adafe9e4atagen2026-10-22 12:30 UTC397
7ff8adafe9e4atagen2026-10-22 12:30 UTC398
            let maybe_room_cache = room.event_cache().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC399
            let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC400
                warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC401
                continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC402
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC403
7ff8adafe9e4atagen2026-10-22 12:30 UTC404
            let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
7ff8adafe9e4atagen2026-10-22 12:30 UTC405
7ff8adafe9e4atagen2026-10-22 12:30 UTC406
            let maybe_timeline_events = store.get_room_events(room_id, None, None).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC407
            let Ok(timeline_events) = maybe_timeline_events else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC408
                warn!("Failed to get room's events: {maybe_timeline_events:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC409
                continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC410
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC411
7ff8adafe9e4atagen2026-10-22 12:30 UTC412
            let no_of_events = timeline_events.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC413
7ff8adafe9e4atagen2026-10-22 12:30 UTC414
            if let Err(err) = search_index_guard
7ff8adafe9e4atagen2026-10-22 12:30 UTC415
                .bulk_handle_timeline_event(
7ff8adafe9e4atagen2026-10-22 12:30 UTC416
                    timeline_events.clone().into_iter(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC417
                    &room_cache,
7ff8adafe9e4atagen2026-10-22 12:30 UTC418
                    room_id,
7ff8adafe9e4atagen2026-10-22 12:30 UTC419
                    &redaction_rules,
7ff8adafe9e4atagen2026-10-22 12:30 UTC420
                )
7ff8adafe9e4atagen2026-10-22 12:30 UTC421
                .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC422
            {
7ff8adafe9e4atagen2026-10-22 12:30 UTC423
                error!("Failed to handle event for indexing: {err}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC424
                let mut error = Some(err);
7ff8adafe9e4atagen2026-10-22 12:30 UTC425
                while let Some(err) = error.take() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC426
                    if let Err(e) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC427
                        update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC428
                    {
7ff8adafe9e4atagen2026-10-22 12:30 UTC429
                        debug!("Failed to send final error message, trying again: {e:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC430
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC431
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC432
                return Err(());
7ff8adafe9e4atagen2026-10-22 12:30 UTC433
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC434
7ff8adafe9e4atagen2026-10-22 12:30 UTC435
            count += no_of_events;
7ff8adafe9e4atagen2026-10-22 12:30 UTC436
            let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC437
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC438
        Ok(count)
7ff8adafe9e4atagen2026-10-22 12:30 UTC439
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC440
7ff8adafe9e4atagen2026-10-22 12:30 UTC441
    async fn index_from_server(
7ff8adafe9e4atagen2026-10-22 12:30 UTC442
        client: &Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC443
        update_sender: &Sender<(bool, IndexingMessage)>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC444
        search_index_guard: &mut SearchIndexGuard<'_>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC445
        mut count: usize,
7ff8adafe9e4atagen2026-10-22 12:30 UTC446
    ) -> Result<usize, ()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC447
        let batch_size = 25;
7ff8adafe9e4atagen2026-10-22 12:30 UTC448
7ff8adafe9e4atagen2026-10-22 12:30 UTC449
        let mut rooms = client.rooms_filtered(RoomStateFilter::JOINED);
7ff8adafe9e4atagen2026-10-22 12:30 UTC450
        let mut idx = 0;
7ff8adafe9e4atagen2026-10-22 12:30 UTC451
7ff8adafe9e4atagen2026-10-22 12:30 UTC452
        while !rooms.is_empty() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC453
            let room = &rooms[idx];
7ff8adafe9e4atagen2026-10-22 12:30 UTC454
7ff8adafe9e4atagen2026-10-22 12:30 UTC455
            let room_id = room.room_id();
7ff8adafe9e4atagen2026-10-22 12:30 UTC456
7ff8adafe9e4atagen2026-10-22 12:30 UTC457
            let maybe_room_cache = room.event_cache().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC458
            let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC459
                warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC460
                idx = (idx + 1) % rooms.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC461
                continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC462
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC463
7ff8adafe9e4atagen2026-10-22 12:30 UTC464
            let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
7ff8adafe9e4atagen2026-10-22 12:30 UTC465
7ff8adafe9e4atagen2026-10-22 12:30 UTC466
            let Ok(pagination) = room_cache.pagination().run_backwards_until(batch_size).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC467
            else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC468
                error!("Failed to backpaginate {room_id}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC469
                idx = (idx + 1) % rooms.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC470
                continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC471
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC472
7ff8adafe9e4atagen2026-10-22 12:30 UTC473
            let no_of_events = pagination.events.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC474
7ff8adafe9e4atagen2026-10-22 12:30 UTC475
            if let Err(err) = search_index_guard
7ff8adafe9e4atagen2026-10-22 12:30 UTC476
                .bulk_handle_timeline_event(
7ff8adafe9e4atagen2026-10-22 12:30 UTC477
                    pagination.events.clone().into_iter(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC478
                    &room_cache,
7ff8adafe9e4atagen2026-10-22 12:30 UTC479
                    room_id,
7ff8adafe9e4atagen2026-10-22 12:30 UTC480
                    &redaction_rules,
7ff8adafe9e4atagen2026-10-22 12:30 UTC481
                )
7ff8adafe9e4atagen2026-10-22 12:30 UTC482
                .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC483
            {
7ff8adafe9e4atagen2026-10-22 12:30 UTC484
                warn!("Failed to handle event for indexing: {err}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC485
                let mut error = Some(err);
7ff8adafe9e4atagen2026-10-22 12:30 UTC486
                while let Some(err) = error.take() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC487
                    if let Err(e) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC488
                        update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC489
                    {
7ff8adafe9e4atagen2026-10-22 12:30 UTC490
                        debug!("Failed to send final error message, trying again: {e:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC491
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC492
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC493
                return Err(());
7ff8adafe9e4atagen2026-10-22 12:30 UTC494
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC495
7ff8adafe9e4atagen2026-10-22 12:30 UTC496
            count += no_of_events;
7ff8adafe9e4atagen2026-10-22 12:30 UTC497
            let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC498
7ff8adafe9e4atagen2026-10-22 12:30 UTC499
            if pagination.reached_start {
7ff8adafe9e4atagen2026-10-22 12:30 UTC500
                rooms.remove(idx);
7ff8adafe9e4atagen2026-10-22 12:30 UTC501
                let len = rooms.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC502
                if len > 0 {
7ff8adafe9e4atagen2026-10-22 12:30 UTC503
                    idx %= len;
7ff8adafe9e4atagen2026-10-22 12:30 UTC504
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC505
            } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC506
                idx = (idx + 1) % rooms.len();
7ff8adafe9e4atagen2026-10-22 12:30 UTC507
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC508
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC509
        Ok(count)
7ff8adafe9e4atagen2026-10-22 12:30 UTC510
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC511
7ff8adafe9e4atagen2026-10-22 12:30 UTC512
    /// The sender sends (progress, done?, error?).
7ff8adafe9e4atagen2026-10-22 12:30 UTC513
    async fn indexing_task(
7ff8adafe9e4atagen2026-10-22 12:30 UTC514
        client: Client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC515
        update_sender: Sender<(bool, IndexingMessage)>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC516
        sync_service: Arc<SyncService>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC517
    ) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC518
        if timeout(Duration::from_secs(30), App::wait_for_room_sync(&update_sender, sync_service))
7ff8adafe9e4atagen2026-10-22 12:30 UTC519
            .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC520
            .is_err()
7ff8adafe9e4atagen2026-10-22 12:30 UTC521
        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC522
            debug!("Waiting for sync to run timed out. Quitting indexing task.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC523
            return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC524
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC525
7ff8adafe9e4atagen2026-10-22 12:30 UTC526
        let Ok(store) = client.event_cache_store().lock().await else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC527
            error!("Failed to get EventCacheStore");
7ff8adafe9e4atagen2026-10-22 12:30 UTC528
            return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC529
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC530
7ff8adafe9e4atagen2026-10-22 12:30 UTC531
        let mut search_index_guard = client.search_index().lock().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC532
        let count = 0;
7ff8adafe9e4atagen2026-10-22 12:30 UTC533
7ff8adafe9e4atagen2026-10-22 12:30 UTC534
        debug!("Start indexing from the event cache.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC535
7ff8adafe9e4atagen2026-10-22 12:30 UTC536
        // First index everything in the cache
7ff8adafe9e4atagen2026-10-22 12:30 UTC537
        let Ok(count) = App::index_event_cache(
7ff8adafe9e4atagen2026-10-22 12:30 UTC538
            &client,
7ff8adafe9e4atagen2026-10-22 12:30 UTC539
            &update_sender,
7ff8adafe9e4atagen2026-10-22 12:30 UTC540
            store.as_clean().expect("Only one process should access the event cache store"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC541
            &mut search_index_guard,
7ff8adafe9e4atagen2026-10-22 12:30 UTC542
            count,
7ff8adafe9e4atagen2026-10-22 12:30 UTC543
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC544
        .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC545
        else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC546
            debug!("Quitting index task.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC547
            return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC548
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC549
7ff8adafe9e4atagen2026-10-22 12:30 UTC550
        // Now index from the server
7ff8adafe9e4atagen2026-10-22 12:30 UTC551
        debug!("Start indexing from the server.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC552
7ff8adafe9e4atagen2026-10-22 12:30 UTC553
        let Ok(count) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC554
            App::index_from_server(&client, &update_sender, &mut search_index_guard, count).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC555
        else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC556
            debug!("Quitting index task.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC557
            return;
7ff8adafe9e4atagen2026-10-22 12:30 UTC558
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC559
7ff8adafe9e4atagen2026-10-22 12:30 UTC560
        while let Err(err) = update_sender.send((true, IndexingMessage::Progress(count))).await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC561
            debug!("couldn't send final update {err}, trying again.");
7ff8adafe9e4atagen2026-10-22 12:30 UTC562
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC563
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC564
7ff8adafe9e4atagen2026-10-22 12:30 UTC565
    fn set_global_mode(&mut self, mode: GlobalMode) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC566
        self.state.global_mode = mode;
7ff8adafe9e4atagen2026-10-22 12:30 UTC567
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC568
7ff8adafe9e4atagen2026-10-22 12:30 UTC569
    async fn handle_global_event(&mut self, event: Event) -> Result<bool> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC570
        use KeyCode::*;
7ff8adafe9e4atagen2026-10-22 12:30 UTC571
7ff8adafe9e4atagen2026-10-22 12:30 UTC572
        match event {
7ff8adafe9e4atagen2026-10-22 12:30 UTC573
            Event::Key(KeyEvent { code: F(1), modifiers: KeyModifiers::NONE, .. }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC574
                self.set_global_mode(GlobalMode::Help)
7ff8adafe9e4atagen2026-10-22 12:30 UTC575
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC576
7ff8adafe9e4atagen2026-10-22 12:30 UTC577
            Event::Key(KeyEvent { code: F(10), modifiers: KeyModifiers::NONE, .. }) => self
7ff8adafe9e4atagen2026-10-22 12:30 UTC578
                .set_global_mode(GlobalMode::Settings {
7ff8adafe9e4atagen2026-10-22 12:30 UTC579
                    view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
7ff8adafe9e4atagen2026-10-22 12:30 UTC580
                }),
7ff8adafe9e4atagen2026-10-22 12:30 UTC581
7ff8adafe9e4atagen2026-10-22 12:30 UTC582
            Event::Key(KeyEvent {
7ff8adafe9e4atagen2026-10-22 12:30 UTC583
                code: Char('j') | Down,
7ff8adafe9e4atagen2026-10-22 12:30 UTC584
                modifiers: KeyModifiers::CONTROL,
7ff8adafe9e4atagen2026-10-22 12:30 UTC585
                ..
7ff8adafe9e4atagen2026-10-22 12:30 UTC586
            }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC587
                self.room_list.next_room().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC588
                let room_id = self.room_list.get_selected_room_id();
7ff8adafe9e4atagen2026-10-22 12:30 UTC589
                self.room_view.set_selected_room(room_id);
7ff8adafe9e4atagen2026-10-22 12:30 UTC590
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC591
7ff8adafe9e4atagen2026-10-22 12:30 UTC592
            Event::Key(KeyEvent {
7ff8adafe9e4atagen2026-10-22 12:30 UTC593
                code: Char('k') | Up, modifiers: KeyModifiers::CONTROL, ..
7ff8adafe9e4atagen2026-10-22 12:30 UTC594
            }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC595
                self.room_list.previous_room().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC596
                let room_id = self.room_list.get_selected_room_id();
7ff8adafe9e4atagen2026-10-22 12:30 UTC597
                self.room_view.set_selected_room(room_id);
7ff8adafe9e4atagen2026-10-22 12:30 UTC598
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC599
7ff8adafe9e4atagen2026-10-22 12:30 UTC600
            Event::Key(KeyEvent { code: Char('m'), modifiers: KeyModifiers::ALT, .. }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC601
                self.room_view.mark_as_read().await
7ff8adafe9e4atagen2026-10-22 12:30 UTC602
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC603
7ff8adafe9e4atagen2026-10-22 12:30 UTC604
            Event::Key(KeyEvent { code: Char('q'), modifiers: KeyModifiers::CONTROL, .. }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC605
                if !matches!(self.state.global_mode, GlobalMode::Default) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC606
                    self.set_global_mode(GlobalMode::Default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC607
                } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC608
                    return Ok(true);
7ff8adafe9e4atagen2026-10-22 12:30 UTC609
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC610
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC611
7ff8adafe9e4atagen2026-10-22 12:30 UTC612
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('r'), .. }) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC613
                self.set_global_mode(GlobalMode::CreateRoom { view: CreateRoomView::new() })
7ff8adafe9e4atagen2026-10-22 12:30 UTC614
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC615
7ff8adafe9e4atagen2026-10-22 12:30 UTC616
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('s'), .. }) => self
7ff8adafe9e4atagen2026-10-22 12:30 UTC617
                .set_global_mode(GlobalMode::Searching {
7ff8adafe9e4atagen2026-10-22 12:30 UTC618
                    view: SearchingView::new(false),
7ff8adafe9e4atagen2026-10-22 12:30 UTC619
                    is_global: false,
7ff8adafe9e4atagen2026-10-22 12:30 UTC620
                }),
7ff8adafe9e4atagen2026-10-22 12:30 UTC621
7ff8adafe9e4atagen2026-10-22 12:30 UTC622
            Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('g'), .. }) => self
7ff8adafe9e4atagen2026-10-22 12:30 UTC623
                .set_global_mode(GlobalMode::Searching {
7ff8adafe9e4atagen2026-10-22 12:30 UTC624
                    view: SearchingView::new(true),
7ff8adafe9e4atagen2026-10-22 12:30 UTC625
                    is_global: true,
7ff8adafe9e4atagen2026-10-22 12:30 UTC626
                }),
7ff8adafe9e4atagen2026-10-22 12:30 UTC627
7ff8adafe9e4atagen2026-10-22 12:30 UTC628
            _ => self.room_view.handle_event(event).await,
7ff8adafe9e4atagen2026-10-22 12:30 UTC629
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC630
7ff8adafe9e4atagen2026-10-22 12:30 UTC631
        Ok(false)
7ff8adafe9e4atagen2026-10-22 12:30 UTC632
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC633
7ff8adafe9e4atagen2026-10-22 12:30 UTC634
    fn on_tick(&mut self) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC635
        self.state.throbber_state.calc_next();
7ff8adafe9e4atagen2026-10-22 12:30 UTC636
7ff8adafe9e4atagen2026-10-22 12:30 UTC637
        match &mut self.state.global_mode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC638
            GlobalMode::Help
7ff8adafe9e4atagen2026-10-22 12:30 UTC639
            | GlobalMode::Default
7ff8adafe9e4atagen2026-10-22 12:30 UTC640
            | GlobalMode::CreateRoom { .. }
7ff8adafe9e4atagen2026-10-22 12:30 UTC641
            | GlobalMode::Searching { .. }
7ff8adafe9e4atagen2026-10-22 12:30 UTC642
            | GlobalMode::Exiting { .. } => {}
7ff8adafe9e4atagen2026-10-22 12:30 UTC643
            GlobalMode::Settings { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC644
                view.on_tick();
7ff8adafe9e4atagen2026-10-22 12:30 UTC645
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC646
            GlobalMode::Indexing { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC647
                view.on_tick();
7ff8adafe9e4atagen2026-10-22 12:30 UTC648
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC649
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC650
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC651
7ff8adafe9e4atagen2026-10-22 12:30 UTC652
    async fn render_loop(&mut self, mut terminal: DefaultTerminal) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC653
        use KeyCode::*;
7ff8adafe9e4atagen2026-10-22 12:30 UTC654
7ff8adafe9e4atagen2026-10-22 12:30 UTC655
        let mut check_channel = true;
7ff8adafe9e4atagen2026-10-22 12:30 UTC656
7ff8adafe9e4atagen2026-10-22 12:30 UTC657
        loop {
7ff8adafe9e4atagen2026-10-22 12:30 UTC658
            if check_channel {
7ff8adafe9e4atagen2026-10-22 12:30 UTC659
                match self.indexing_receiver.try_recv() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC660
                    Ok((done, message)) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC661
                        if !matches!(message, IndexingMessage::Error(_)) && done {
7ff8adafe9e4atagen2026-10-22 12:30 UTC662
                            self.set_global_mode(GlobalMode::Default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC663
                        } else if let GlobalMode::Indexing { view } = &mut self.state.global_mode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC664
                            view.set_message(message);
7ff8adafe9e4atagen2026-10-22 12:30 UTC665
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC666
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC667
                    Err(TryRecvError::Disconnected) => check_channel = false,
7ff8adafe9e4atagen2026-10-22 12:30 UTC668
                    Err(TryRecvError::Empty) => {}
7ff8adafe9e4atagen2026-10-22 12:30 UTC669
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC670
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC671
7ff8adafe9e4atagen2026-10-22 12:30 UTC672
            terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC673
7ff8adafe9e4atagen2026-10-22 12:30 UTC674
            if event::poll(Duration::from_millis(100))? {
7ff8adafe9e4atagen2026-10-22 12:30 UTC675
                let event = event::read()?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC676
7ff8adafe9e4atagen2026-10-22 12:30 UTC677
                match &mut self.state.global_mode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC678
                    GlobalMode::Default => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC679
                        if self.handle_global_event(event).await? {
7ff8adafe9e4atagen2026-10-22 12:30 UTC680
                            let sync_service = self.sync_service.clone();
7ff8adafe9e4atagen2026-10-22 12:30 UTC681
                            let timelines = self.timelines.clone();
7ff8adafe9e4atagen2026-10-22 12:30 UTC682
                            let listen_task = self.listen_task.abort_handle();
7ff8adafe9e4atagen2026-10-22 12:30 UTC683
                            let indexing_task = self.indexing_task.abort_handle();
7ff8adafe9e4atagen2026-10-22 12:30 UTC684
7ff8adafe9e4atagen2026-10-22 12:30 UTC685
                            let shutdown_task = spawn(async move {
7ff8adafe9e4atagen2026-10-22 12:30 UTC686
                                sync_service.stop().await;
7ff8adafe9e4atagen2026-10-22 12:30 UTC687
7ff8adafe9e4atagen2026-10-22 12:30 UTC688
                                listen_task.abort();
7ff8adafe9e4atagen2026-10-22 12:30 UTC689
                                indexing_task.abort();
7ff8adafe9e4atagen2026-10-22 12:30 UTC690
7ff8adafe9e4atagen2026-10-22 12:30 UTC691
                                for timeline in timelines.lock().values() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC692
                                    timeline.task.abort();
7ff8adafe9e4atagen2026-10-22 12:30 UTC693
                                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC694
                            });
7ff8adafe9e4atagen2026-10-22 12:30 UTC695
7ff8adafe9e4atagen2026-10-22 12:30 UTC696
                            self.set_global_mode(GlobalMode::Exiting { shutdown_task });
7ff8adafe9e4atagen2026-10-22 12:30 UTC697
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC698
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC699
                    GlobalMode::Help => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC700
                        if let Event::Key(key) = event
7ff8adafe9e4atagen2026-10-22 12:30 UTC701
                            && let KeyModifiers::NONE = key.modifiers
7ff8adafe9e4atagen2026-10-22 12:30 UTC702
                            && let Char('q') | Esc = key.code
7ff8adafe9e4atagen2026-10-22 12:30 UTC703
                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC704
                            self.set_global_mode(GlobalMode::Default)
7ff8adafe9e4atagen2026-10-22 12:30 UTC705
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC706
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC707
                    GlobalMode::Settings { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC708
                        if let Event::Key(key) = event
7ff8adafe9e4atagen2026-10-22 12:30 UTC709
                            && view.handle_key_press(key).await
7ff8adafe9e4atagen2026-10-22 12:30 UTC710
                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC711
                            self.set_global_mode(GlobalMode::Default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC712
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC713
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC714
                    GlobalMode::CreateRoom { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC715
                        if let Event::Key(key) = event
7ff8adafe9e4atagen2026-10-22 12:30 UTC716
                            && let KeyModifiers::NONE = key.modifiers
7ff8adafe9e4atagen2026-10-22 12:30 UTC717
                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC718
                            match key.code {
7ff8adafe9e4atagen2026-10-22 12:30 UTC719
                                Enter => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC720
                                    if let Some(room_name) = view.get_text() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC721
                                        let mut request = CreateRoomRequest::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC722
                                        request.name = Some(room_name);
7ff8adafe9e4atagen2026-10-22 12:30 UTC723
                                        if let Err(err) = self
7ff8adafe9e4atagen2026-10-22 12:30 UTC724
                                            .sync_service
7ff8adafe9e4atagen2026-10-22 12:30 UTC725
                                            .room_list_service()
7ff8adafe9e4atagen2026-10-22 12:30 UTC726
                                            .client()
7ff8adafe9e4atagen2026-10-22 12:30 UTC727
                                            .create_room(request)
7ff8adafe9e4atagen2026-10-22 12:30 UTC728
                                            .await
7ff8adafe9e4atagen2026-10-22 12:30 UTC729
                                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC730
                                            error!("error while creating room: {err:?}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC731
                                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC732
                                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC733
                                    self.set_global_mode(GlobalMode::Default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC734
                                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC735
                                Esc => self.set_global_mode(GlobalMode::Default),
7ff8adafe9e4atagen2026-10-22 12:30 UTC736
                                _ => view.handle_key_press(key),
7ff8adafe9e4atagen2026-10-22 12:30 UTC737
                            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC738
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC739
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC740
                    GlobalMode::Searching { view, is_global } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC741
                        if let Event::Key(key) = event {
7ff8adafe9e4atagen2026-10-22 12:30 UTC742
                            match key.code {
7ff8adafe9e4atagen2026-10-22 12:30 UTC743
                                Enter => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC744
                                    if let Some(query) = view.get_text() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC745
                                        if *is_global {
7ff8adafe9e4atagen2026-10-22 12:30 UTC746
                                            let mut search =
7ff8adafe9e4atagen2026-10-22 12:30 UTC747
                                                self.client.search_messages(query, 5).build();
7ff8adafe9e4atagen2026-10-22 12:30 UTC748
7ff8adafe9e4atagen2026-10-22 12:30 UTC749
                                            let mut all_results = HashMap::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC750
                                            loop {
7ff8adafe9e4atagen2026-10-22 12:30 UTC751
                                                let Ok(results) = search.next_events().await else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC752
                                                    continue;
7ff8adafe9e4atagen2026-10-22 12:30 UTC753
                                                };
7ff8adafe9e4atagen2026-10-22 12:30 UTC754
                                                let Some(results) = results else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC755
                                                    break;
7ff8adafe9e4atagen2026-10-22 12:30 UTC756
                                                };
7ff8adafe9e4atagen2026-10-22 12:30 UTC757
                                                for (room_id, event_id) in results {
7ff8adafe9e4atagen2026-10-22 12:30 UTC758
                                                    all_results
7ff8adafe9e4atagen2026-10-22 12:30 UTC759
                                                        .entry(room_id)
7ff8adafe9e4atagen2026-10-22 12:30 UTC760
                                                        .or_insert_with(Vec::new)
7ff8adafe9e4atagen2026-10-22 12:30 UTC761
                                                        .push(event_id);
7ff8adafe9e4atagen2026-10-22 12:30 UTC762
                                                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC763
                                            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC764
7ff8adafe9e4atagen2026-10-22 12:30 UTC765
                                            view.set_results(
7ff8adafe9e4atagen2026-10-22 12:30 UTC766
                                                all_results
7ff8adafe9e4atagen2026-10-22 12:30 UTC767
                                                    .into_iter()
7ff8adafe9e4atagen2026-10-22 12:30 UTC768
                                                    .map(|(room_id, events)| {
7ff8adafe9e4atagen2026-10-22 12:30 UTC769
                                                        (Some(room_id), events)
7ff8adafe9e4atagen2026-10-22 12:30 UTC770
                                                    })
7ff8adafe9e4atagen2026-10-22 12:30 UTC771
                                                    .collect(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC772
                                            );
7ff8adafe9e4atagen2026-10-22 12:30 UTC773
                                        } else if let Some((query, room)) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC774
                                            view.get_text().zip(self.room_view.room())
7ff8adafe9e4atagen2026-10-22 12:30 UTC775
                                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC776
                                            let mut room_search = room.search_messages(query, 5);
7ff8adafe9e4atagen2026-10-22 12:30 UTC777
7ff8adafe9e4atagen2026-10-22 12:30 UTC778
                                            let mut all_results = Vec::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC779
                                            while let Some(results) =
7ff8adafe9e4atagen2026-10-22 12:30 UTC780
                                                room_search.next_events().await?
7ff8adafe9e4atagen2026-10-22 12:30 UTC781
                                            {
7ff8adafe9e4atagen2026-10-22 12:30 UTC782
                                                all_results.extend(results);
7ff8adafe9e4atagen2026-10-22 12:30 UTC783
                                            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC784
                                            view.set_results(vec![(None, all_results)]);
7ff8adafe9e4atagen2026-10-22 12:30 UTC785
                                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC786
                                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC787
                                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC788
7ff8adafe9e4atagen2026-10-22 12:30 UTC789
                                Esc => self.set_global_mode(GlobalMode::Default),
7ff8adafe9e4atagen2026-10-22 12:30 UTC790
7ff8adafe9e4atagen2026-10-22 12:30 UTC791
                                Up => view.list_state.previous(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC792
7ff8adafe9e4atagen2026-10-22 12:30 UTC793
                                Down => view.list_state.next(),
7ff8adafe9e4atagen2026-10-22 12:30 UTC794
7ff8adafe9e4atagen2026-10-22 12:30 UTC795
                                _ => view.handle_key_press(key),
7ff8adafe9e4atagen2026-10-22 12:30 UTC796
                            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC797
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC798
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC799
                    GlobalMode::Indexing { .. } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC800
                        if let Event::Key(key) = event
7ff8adafe9e4atagen2026-10-22 12:30 UTC801
                            && let KeyModifiers::NONE = key.modifiers
7ff8adafe9e4atagen2026-10-22 12:30 UTC802
                            && let Esc = key.code
7ff8adafe9e4atagen2026-10-22 12:30 UTC803
                        {
7ff8adafe9e4atagen2026-10-22 12:30 UTC804
                            self.indexing_task.abort();
7ff8adafe9e4atagen2026-10-22 12:30 UTC805
                            self.set_global_mode(GlobalMode::Default);
7ff8adafe9e4atagen2026-10-22 12:30 UTC806
                        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC807
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC808
                    GlobalMode::Exiting { .. } => {}
7ff8adafe9e4atagen2026-10-22 12:30 UTC809
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC810
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC811
7ff8adafe9e4atagen2026-10-22 12:30 UTC812
            match &self.state.global_mode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC813
                GlobalMode::Default
7ff8adafe9e4atagen2026-10-22 12:30 UTC814
                | GlobalMode::Help
7ff8adafe9e4atagen2026-10-22 12:30 UTC815
                | GlobalMode::CreateRoom { .. }
7ff8adafe9e4atagen2026-10-22 12:30 UTC816
                | GlobalMode::Searching { .. }
7ff8adafe9e4atagen2026-10-22 12:30 UTC817
                | GlobalMode::Indexing { .. }
7ff8adafe9e4atagen2026-10-22 12:30 UTC818
                | GlobalMode::Settings { .. } => {}
7ff8adafe9e4atagen2026-10-22 12:30 UTC819
                GlobalMode::Exiting { shutdown_task } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC820
                    if shutdown_task.is_finished() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC821
                        break;
7ff8adafe9e4atagen2026-10-22 12:30 UTC822
                    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC823
                }
7ff8adafe9e4atagen2026-10-22 12:30 UTC824
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC825
7ff8adafe9e4atagen2026-10-22 12:30 UTC826
            if self.last_tick.elapsed() >= Self::TICK_RATE {
7ff8adafe9e4atagen2026-10-22 12:30 UTC827
                self.on_tick();
7ff8adafe9e4atagen2026-10-22 12:30 UTC828
                self.last_tick = Instant::now();
7ff8adafe9e4atagen2026-10-22 12:30 UTC829
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC830
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC831
7ff8adafe9e4atagen2026-10-22 12:30 UTC832
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC833
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC834
7ff8adafe9e4atagen2026-10-22 12:30 UTC835
    async fn run(&mut self, terminal: DefaultTerminal) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC836
        self.render_loop(terminal).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC837
7ff8adafe9e4atagen2026-10-22 12:30 UTC838
        // At this point the user has exited the loop, so shut down the application.
7ff8adafe9e4atagen2026-10-22 12:30 UTC839
        ratatui::restore();
7ff8adafe9e4atagen2026-10-22 12:30 UTC840
        execute!(stdout(), DisableMouseCapture)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC841
7ff8adafe9e4atagen2026-10-22 12:30 UTC842
        Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC843
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC844
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC845
7ff8adafe9e4atagen2026-10-22 12:30 UTC846
impl Widget for &mut App {
7ff8adafe9e4atagen2026-10-22 12:30 UTC847
    /// Render the whole app.
7ff8adafe9e4atagen2026-10-22 12:30 UTC848
    fn render(self, area: Rect, buf: &mut Buffer) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC849
        // Create a space for header, room list and timeline and the footer.
7ff8adafe9e4atagen2026-10-22 12:30 UTC850
        let vertical =
7ff8adafe9e4atagen2026-10-22 12:30 UTC851
            Layout::vertical([Constraint::Length(2), Constraint::Min(0), Constraint::Length(1)]);
7ff8adafe9e4atagen2026-10-22 12:30 UTC852
        let [header_area, rest_area, status_area] = vertical.areas(area);
7ff8adafe9e4atagen2026-10-22 12:30 UTC853
7ff8adafe9e4atagen2026-10-22 12:30 UTC854
        // Create two chunks with equal horizontal screen space. One for the list and
7ff8adafe9e4atagen2026-10-22 12:30 UTC855
        // the other for the info block.
7ff8adafe9e4atagen2026-10-22 12:30 UTC856
        let horizontal =
7ff8adafe9e4atagen2026-10-22 12:30 UTC857
            Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)]);
7ff8adafe9e4atagen2026-10-22 12:30 UTC858
        let [room_list_area, room_view_area] = horizontal.areas(rest_area);
7ff8adafe9e4atagen2026-10-22 12:30 UTC859
7ff8adafe9e4atagen2026-10-22 12:30 UTC860
        self.render_title(header_area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC861
        self.room_list.render(room_list_area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC862
        self.room_view.render(room_view_area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC863
        self.status.render(status_area, buf, &mut self.state);
7ff8adafe9e4atagen2026-10-22 12:30 UTC864
7ff8adafe9e4atagen2026-10-22 12:30 UTC865
        match &mut self.state.global_mode {
7ff8adafe9e4atagen2026-10-22 12:30 UTC866
            GlobalMode::Default => {}
7ff8adafe9e4atagen2026-10-22 12:30 UTC867
            GlobalMode::Exiting { .. } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC868
                Clear.render(rest_area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC869
                let centered = create_centered_throbber_area(area);
7ff8adafe9e4atagen2026-10-22 12:30 UTC870
                let throbber = Throbber::default()
7ff8adafe9e4atagen2026-10-22 12:30 UTC871
                    .label("Exiting")
7ff8adafe9e4atagen2026-10-22 12:30 UTC872
                    .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
7ff8adafe9e4atagen2026-10-22 12:30 UTC873
                StatefulWidget::render(throbber, centered, buf, &mut self.state.throbber_state);
7ff8adafe9e4atagen2026-10-22 12:30 UTC874
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC875
            GlobalMode::Settings { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC876
                view.render(area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC877
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC878
            GlobalMode::Help => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC879
                let mut help_view = HelpView::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC880
                help_view.render(area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC881
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC882
            GlobalMode::CreateRoom { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC883
                view.render(area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC884
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC885
            GlobalMode::Searching { view, .. } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC886
                view.render(room_view_area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC887
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC888
            GlobalMode::Indexing { view } => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC889
                view.render(area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC890
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC891
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC892
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC893
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC894
7ff8adafe9e4atagen2026-10-22 12:30 UTC895
impl App {
7ff8adafe9e4atagen2026-10-22 12:30 UTC896
    /// Render the top square (title of the program).
7ff8adafe9e4atagen2026-10-22 12:30 UTC897
    fn render_title(&self, area: Rect, buf: &mut Buffer) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC898
        Paragraph::new("Multiverse").bold().centered().render(area, buf);
7ff8adafe9e4atagen2026-10-22 12:30 UTC899
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC900
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC901
7ff8adafe9e4atagen2026-10-22 12:30 UTC902
/// Configure the client so it's ready for sync'ing.
7ff8adafe9e4atagen2026-10-22 12:30 UTC903
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC904
/// Will log in or reuse a previous session.
7ff8adafe9e4atagen2026-10-22 12:30 UTC905
async fn configure_client(cli: Cli) -> Result<Client> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC906
    let Cli { server_name, session_path, proxy, dont_share_pos: _ } = cli;
7ff8adafe9e4atagen2026-10-22 12:30 UTC907
7ff8adafe9e4atagen2026-10-22 12:30 UTC908
    let mut client_builder = Client::builder()
7ff8adafe9e4atagen2026-10-22 12:30 UTC909
        .store_config(
7ff8adafe9e4atagen2026-10-22 12:30 UTC910
            StoreConfig::new(CrossProcessLockConfig::multi_process("multiverse"))
7ff8adafe9e4atagen2026-10-22 12:30 UTC911
                .crypto_store(SqliteCryptoStore::open(session_path.join("crypto"), None).await?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC912
                .state_store(SqliteStateStore::open(session_path.join("state"), None).await?)
7ff8adafe9e4atagen2026-10-22 12:30 UTC913
                .event_cache_store(
7ff8adafe9e4atagen2026-10-22 12:30 UTC914
                    SqliteEventCacheStore::open(session_path.join("cache"), None).await?,
7ff8adafe9e4atagen2026-10-22 12:30 UTC915
                ),
7ff8adafe9e4atagen2026-10-22 12:30 UTC916
        )
7ff8adafe9e4atagen2026-10-22 12:30 UTC917
        .server_name_or_homeserver_url(&server_name)
7ff8adafe9e4atagen2026-10-22 12:30 UTC918
        .with_encryption_settings(EncryptionSettings {
7ff8adafe9e4atagen2026-10-22 12:30 UTC919
            auto_enable_cross_signing: true,
7ff8adafe9e4atagen2026-10-22 12:30 UTC920
            backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
7ff8adafe9e4atagen2026-10-22 12:30 UTC921
            auto_enable_backups: true,
7ff8adafe9e4atagen2026-10-22 12:30 UTC922
        })
7ff8adafe9e4atagen2026-10-22 12:30 UTC923
        .with_enable_share_history_on_invite(true)
7ff8adafe9e4atagen2026-10-22 12:30 UTC924
        .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
7ff8adafe9e4atagen2026-10-22 12:30 UTC925
        .search_index_store(SearchIndexStoreKind::UnencryptedDirectory(
7ff8adafe9e4atagen2026-10-22 12:30 UTC926
            session_path.join("indexData"),
7ff8adafe9e4atagen2026-10-22 12:30 UTC927
        ));
7ff8adafe9e4atagen2026-10-22 12:30 UTC928
7ff8adafe9e4atagen2026-10-22 12:30 UTC929
    if let Some(proxy_url) = proxy {
7ff8adafe9e4atagen2026-10-22 12:30 UTC930
        client_builder = client_builder.proxy(proxy_url).disable_ssl_verification();
7ff8adafe9e4atagen2026-10-22 12:30 UTC931
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC932
7ff8adafe9e4atagen2026-10-22 12:30 UTC933
    let client = client_builder.build().await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC934
7ff8adafe9e4atagen2026-10-22 12:30 UTC935
    // Try reading a session, otherwise create a new one.
7ff8adafe9e4atagen2026-10-22 12:30 UTC936
    log_in_or_restore_session(&client, &session_path).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC937
7ff8adafe9e4atagen2026-10-22 12:30 UTC938
    Ok(client)
7ff8adafe9e4atagen2026-10-22 12:30 UTC939
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC940
7ff8adafe9e4atagen2026-10-22 12:30 UTC941
async fn log_in_or_restore_session(client: &Client, session_path: &Path) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC942
    let session_path = session_path.join("session.json");
7ff8adafe9e4atagen2026-10-22 12:30 UTC943
7ff8adafe9e4atagen2026-10-22 12:30 UTC944
    if let Ok(serialized) = std::fs::read_to_string(&session_path) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC945
        let session: MatrixSession = serde_json::from_str(&serialized)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC946
        client.restore_session(session).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC947
    } else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC948
        login_with_password(client).await?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC949
7ff8adafe9e4atagen2026-10-22 12:30 UTC950
        // Immediately save the session to disk.
7ff8adafe9e4atagen2026-10-22 12:30 UTC951
        if let Some(session) = client.session() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC952
            let AuthSession::Matrix(session) = session else {
7ff8adafe9e4atagen2026-10-22 12:30 UTC953
                panic!("unexpected OAuth 2.0 session")
7ff8adafe9e4atagen2026-10-22 12:30 UTC954
            };
7ff8adafe9e4atagen2026-10-22 12:30 UTC955
            let serialized = serde_json::to_string(&session)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC956
            std::fs::write(session_path, serialized)?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC957
7ff8adafe9e4atagen2026-10-22 12:30 UTC958
            println!("saved session");
7ff8adafe9e4atagen2026-10-22 12:30 UTC959
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC960
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC961
7ff8adafe9e4atagen2026-10-22 12:30 UTC962
    Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC963
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC964
7ff8adafe9e4atagen2026-10-22 12:30 UTC965
/// Asks the user of a username and password, and try to login using the matrix
7ff8adafe9e4atagen2026-10-22 12:30 UTC966
/// auth with those.
7ff8adafe9e4atagen2026-10-22 12:30 UTC967
async fn login_with_password(client: &Client) -> Result<()> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC968
    println!("Logging in with username and password…");
7ff8adafe9e4atagen2026-10-22 12:30 UTC969
7ff8adafe9e4atagen2026-10-22 12:30 UTC970
    loop {
7ff8adafe9e4atagen2026-10-22 12:30 UTC971
        print!("\nUsername: ");
7ff8adafe9e4atagen2026-10-22 12:30 UTC972
        stdout().flush().expect("Unable to write to stdout");
7ff8adafe9e4atagen2026-10-22 12:30 UTC973
        let mut username = String::new();
7ff8adafe9e4atagen2026-10-22 12:30 UTC974
        io::stdin().read_line(&mut username).expect("Unable to read user input");
7ff8adafe9e4atagen2026-10-22 12:30 UTC975
        username = username.trim().to_owned();
7ff8adafe9e4atagen2026-10-22 12:30 UTC976
7ff8adafe9e4atagen2026-10-22 12:30 UTC977
        let password = rpassword::prompt_password("Password.")?;
7ff8adafe9e4atagen2026-10-22 12:30 UTC978
7ff8adafe9e4atagen2026-10-22 12:30 UTC979
        match client.matrix_auth().login_username(&username, password.trim()).await {
7ff8adafe9e4atagen2026-10-22 12:30 UTC980
            Ok(_) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC981
                println!("Logged in as {username}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC982
                break;
7ff8adafe9e4atagen2026-10-22 12:30 UTC983
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC984
            Err(error) => {
7ff8adafe9e4atagen2026-10-22 12:30 UTC985
                println!("Error logging in: {error}");
7ff8adafe9e4atagen2026-10-22 12:30 UTC986
                println!("Please try again\n");
7ff8adafe9e4atagen2026-10-22 12:30 UTC987
            }
7ff8adafe9e4atagen2026-10-22 12:30 UTC988
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC989
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC990
7ff8adafe9e4atagen2026-10-22 12:30 UTC991
    Ok(())
7ff8adafe9e4atagen2026-10-22 12:30 UTC992
}