use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::Mutex as AsyncMutex; use tracing::warn; use matrix_sdk::{ encryption::verification::SasVerification, ruma::{OwnedRoomId, RoomId}, }; use modalkit::{ editing::{ action::{Action, UIError, UIResult}, application::{ ApplicationAction, ApplicationContentId, ApplicationError, ApplicationInfo, ApplicationStore, ApplicationWindowId, }, context::EditContext, store::Store, }, env::vim::{ command::{VimCommand, VimCommandMachine}, keybindings::VimMachine, VimContext, }, input::bindings::SequenceStatus, input::key::TerminalKey, }; use crate::{ message::{Message, Messages}, worker::Requester, ApplicationSettings, }; const ROOM_FETCH_DEBOUNCE: Duration = Duration::from_secs(3); #[derive(Clone, Debug, Eq, PartialEq)] pub enum IambInfo {} #[derive(Clone, Debug, Eq, PartialEq)] pub enum VerifyAction { Accept, Cancel, Confirm, Mismatch, } #[derive(Clone, Debug, Eq, PartialEq)] pub enum IambAction { Verify(VerifyAction, String), VerifyRequest(String), SendMessage(OwnedRoomId, String), ToggleScrollbackFocus, } impl ApplicationAction for IambAction { fn is_edit_sequence(&self, _: &C) -> SequenceStatus { match self { IambAction::SendMessage(..) => SequenceStatus::Break, IambAction::ToggleScrollbackFocus => SequenceStatus::Break, IambAction::Verify(..) => SequenceStatus::Break, IambAction::VerifyRequest(..) => SequenceStatus::Break, } } fn is_last_action(&self, _: &C) -> SequenceStatus { match self { IambAction::SendMessage(..) => SequenceStatus::Atom, IambAction::ToggleScrollbackFocus => SequenceStatus::Atom, IambAction::Verify(..) => SequenceStatus::Atom, IambAction::VerifyRequest(..) => SequenceStatus::Atom, } } fn is_last_selection(&self, _: &C) -> SequenceStatus { match self { IambAction::SendMessage(..) => SequenceStatus::Ignore, IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore, IambAction::Verify(..) => SequenceStatus::Ignore, IambAction::VerifyRequest(..) => SequenceStatus::Ignore, } } fn is_switchable(&self, _: &C) -> bool { match self { IambAction::SendMessage(..) => false, IambAction::ToggleScrollbackFocus => false, IambAction::Verify(..) => false, IambAction::VerifyRequest(..) => false, } } } impl From for ProgramAction { fn from(act: IambAction) -> Self { Action::Application(act) } } pub type ProgramAction = Action; pub type ProgramContext = VimContext; pub type Keybindings = VimMachine; pub type ProgramCommand = VimCommand; pub type ProgramCommands = VimCommandMachine; pub type ProgramStore = Store; pub type AsyncProgramStore = Arc>; pub type IambResult = UIResult; #[derive(thiserror::Error, Debug)] pub enum IambError { #[error("Unknown room identifier: {0}")] InvalidUserId(String), #[error("Invalid verification user/device pair: {0}")] InvalidVerificationId(String), #[error("Cryptographic storage error: {0}")] CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError), #[error("HTTP client error: {0}")] Http(#[from] matrix_sdk::HttpError), #[error("Matrix client error: {0}")] Matrix(#[from] matrix_sdk::Error), #[error("Matrix client storage error: {0}")] Store(#[from] matrix_sdk::StoreError), #[error("Serialization/deserialization error: {0}")] Serde(#[from] serde_json::Error), #[error("Unknown room identifier: {0}")] UnknownRoom(OwnedRoomId), #[error("Verification request error: {0}")] VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError), } impl From for UIError { fn from(err: IambError) -> Self { UIError::Application(err) } } impl ApplicationError for IambError {} #[derive(Default)] pub enum RoomFetchStatus { Done, HaveMore(String), #[default] NotStarted, } #[derive(Default)] pub struct RoomInfo { pub name: Option, pub messages: Messages, pub fetch_id: RoomFetchStatus, pub fetch_last: Option, } impl RoomInfo { fn recently_fetched(&self) -> bool { self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE) } } pub struct ChatStore { pub worker: Requester, pub rooms: HashMap, pub names: HashMap, pub verifications: HashMap, pub settings: ApplicationSettings, pub need_load: HashSet, } impl ChatStore { pub fn new(worker: Requester, settings: ApplicationSettings) -> Self { ChatStore { worker, settings, names: Default::default(), rooms: Default::default(), verifications: Default::default(), need_load: Default::default(), } } pub fn mark_for_load(&mut self, room_id: OwnedRoomId) { self.need_load.insert(room_id); } pub fn load_older(&mut self, limit: u32) { let ChatStore { need_load, rooms, worker, .. } = self; for room_id in std::mem::take(need_load).into_iter() { let info = rooms.entry(room_id.clone()).or_default(); if info.recently_fetched() { need_load.insert(room_id); continue; } else { info.fetch_last = Instant::now().into(); } let fetch_id = match &info.fetch_id { RoomFetchStatus::Done => continue, RoomFetchStatus::HaveMore(fetch_id) => Some(fetch_id.clone()), RoomFetchStatus::NotStarted => None, }; let res = worker.load_older(room_id.clone(), fetch_id, limit); match res { Ok((fetch_id, msgs)) => { for msg in msgs.into_iter() { let key = (msg.origin_server_ts().into(), msg.event_id().to_owned()); info.messages.insert(key, Message::from(msg)); } info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore); }, Err(e) => { warn!( room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages" ); // Wait and try again. need_load.insert(room_id); }, } } } pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo { self.rooms.entry(room_id).or_default() } pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) { self.rooms.entry(room_id.to_owned()).or_default().name = name.to_string().into(); } pub fn insert_sas(&mut self, sas: SasVerification) { let key = format!("{}/{}", sas.other_user_id(), sas.other_device().device_id()); self.verifications.insert(key, sas); } } impl ApplicationStore for ChatStore {} #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum IambId { Room(OwnedRoomId), DirectList, RoomList, SpaceList, VerifyList, Welcome, } impl ApplicationWindowId for IambId {} #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum RoomFocus { Scrollback, MessageBar, } impl RoomFocus { pub fn is_scrollback(&self) -> bool { matches!(self, RoomFocus::Scrollback) } pub fn is_msgbar(&self) -> bool { matches!(self, RoomFocus::MessageBar) } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum IambBufferId { Command, Room(OwnedRoomId, RoomFocus), DirectList, RoomList, SpaceList, VerifyList, Welcome, } impl IambBufferId { pub fn to_window(&self) -> Option { match self { IambBufferId::Command => None, IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())), IambBufferId::DirectList => Some(IambId::DirectList), IambBufferId::RoomList => Some(IambId::RoomList), IambBufferId::SpaceList => Some(IambId::SpaceList), IambBufferId::VerifyList => Some(IambId::VerifyList), IambBufferId::Welcome => Some(IambId::Welcome), } } } impl ApplicationContentId for IambBufferId {} impl ApplicationInfo for IambInfo { type Error = IambError; type Store = ChatStore; type Action = IambAction; type WindowId = IambId; type ContentId = IambBufferId; }