2023-03-01 18:46:33 -08:00
|
|
|
use std::borrow::Cow;
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2023-06-28 23:42:31 -07:00
|
|
|
use std::convert::TryFrom;
|
|
|
|
use std::fmt::{self, Display};
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::hash::Hash;
|
2023-03-01 18:46:33 -08:00
|
|
|
use std::str::FromStr;
|
2022-12-29 18:00:59 -08:00
|
|
|
use std::sync::Arc;
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
use emojis::Emoji;
|
2023-06-28 23:42:31 -07:00
|
|
|
use serde::{
|
|
|
|
de::Error as SerdeError,
|
|
|
|
de::Visitor,
|
|
|
|
Deserialize,
|
|
|
|
Deserializer,
|
|
|
|
Serialize,
|
|
|
|
Serializer,
|
|
|
|
};
|
2022-12-29 18:00:59 -08:00
|
|
|
use tokio::sync::Mutex as AsyncMutex;
|
2023-06-28 23:42:31 -07:00
|
|
|
use url::Url;
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
use matrix_sdk::{
|
|
|
|
encryption::verification::SasVerification,
|
2023-07-05 15:25:42 -07:00
|
|
|
room::{Joined, Room as MatrixRoom},
|
2023-01-19 16:05:02 -08:00
|
|
|
ruma::{
|
2023-02-09 17:53:33 -08:00
|
|
|
events::{
|
|
|
|
reaction::ReactionEvent,
|
2023-03-13 15:18:53 -07:00
|
|
|
room::encrypted::RoomEncryptedEvent,
|
2023-02-09 17:53:33 -08:00
|
|
|
room::message::{
|
|
|
|
OriginalRoomMessageEvent,
|
|
|
|
Relation,
|
|
|
|
Replacement,
|
|
|
|
RoomMessageEvent,
|
|
|
|
RoomMessageEventContent,
|
|
|
|
},
|
|
|
|
tag::{TagName, Tags},
|
|
|
|
MessageLikeEvent,
|
2023-01-19 16:05:02 -08:00
|
|
|
},
|
2023-03-01 18:46:33 -08:00
|
|
|
presence::PresenceState,
|
2023-01-19 16:05:02 -08:00
|
|
|
EventId,
|
|
|
|
OwnedEventId,
|
|
|
|
OwnedRoomId,
|
|
|
|
OwnedUserId,
|
|
|
|
RoomId,
|
|
|
|
},
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
use modalkit::{
|
|
|
|
editing::{
|
|
|
|
action::{Action, UIError, UIResult},
|
|
|
|
application::{
|
|
|
|
ApplicationAction,
|
|
|
|
ApplicationContentId,
|
|
|
|
ApplicationError,
|
|
|
|
ApplicationInfo,
|
|
|
|
ApplicationStore,
|
|
|
|
ApplicationWindowId,
|
|
|
|
},
|
2023-03-01 18:46:33 -08:00
|
|
|
base::{CommandType, WordStyle},
|
|
|
|
completion::{complete_path, CompletionMap},
|
2022-12-29 18:00:59 -08:00
|
|
|
context::EditContext,
|
2023-03-01 18:46:33 -08:00
|
|
|
cursor::Cursor,
|
|
|
|
rope::EditRope,
|
2022-12-29 18:00:59 -08:00
|
|
|
store::Store,
|
|
|
|
},
|
|
|
|
env::vim::{
|
2023-03-01 18:46:33 -08:00
|
|
|
command::{CommandContext, CommandDescription, VimCommand, VimCommandMachine},
|
2022-12-29 18:00:59 -08:00
|
|
|
keybindings::VimMachine,
|
|
|
|
VimContext,
|
|
|
|
},
|
|
|
|
input::bindings::SequenceStatus,
|
|
|
|
input::key::TerminalKey,
|
2023-01-03 13:57:28 -08:00
|
|
|
tui::{
|
|
|
|
buffer::Buffer,
|
|
|
|
layout::{Alignment, Rect},
|
|
|
|
text::{Span, Spans},
|
|
|
|
widgets::{Paragraph, Widget},
|
|
|
|
},
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{
|
2023-01-19 16:05:02 -08:00
|
|
|
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
|
2022-12-29 18:00:59 -08:00
|
|
|
worker::Requester,
|
|
|
|
ApplicationSettings,
|
|
|
|
};
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
pub const MATRIX_ID_WORD: WordStyle = WordStyle::CharSet(is_mxid_char);
|
|
|
|
|
|
|
|
/// Find the boundaries for a Matrix username, room alias, or room ID.
|
|
|
|
///
|
|
|
|
/// Technically "[" and "]" should be here since IPv6 addresses are allowed
|
|
|
|
/// in the server name, but in practice that should be uncommon, and people
|
|
|
|
/// can just use `gf` and friends in Visual mode instead.
|
|
|
|
fn is_mxid_char(c: char) -> bool {
|
|
|
|
return c >= 'a' && c <= 'z' ||
|
|
|
|
c >= 'A' && c <= 'Z' ||
|
|
|
|
c >= '0' && c <= '9' ||
|
|
|
|
":-./@_#!".contains(c);
|
|
|
|
}
|
|
|
|
|
2023-01-23 17:08:11 -08:00
|
|
|
const ROOM_FETCH_DEBOUNCE: Duration = Duration::from_secs(2);
|
2022-12-29 18:00:59 -08:00
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum IambInfo {}
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum VerifyAction {
|
|
|
|
Accept,
|
|
|
|
Cancel,
|
|
|
|
Confirm,
|
|
|
|
Mismatch,
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum MessageAction {
|
2023-01-19 16:05:02 -08:00
|
|
|
/// Cance the current reply or edit.
|
2023-05-01 22:14:08 -07:00
|
|
|
///
|
|
|
|
/// The [bool] argument indicates whether to skip confirmation for clearing the message bar.
|
|
|
|
Cancel(bool),
|
2023-01-19 16:05:02 -08:00
|
|
|
|
|
|
|
/// Download an attachment to the given path.
|
|
|
|
///
|
2023-01-28 12:29:06 +00:00
|
|
|
/// The second argument controls whether to overwrite any already existing file at the
|
|
|
|
/// destination path, or to open the attachment after downloading.
|
|
|
|
Download(Option<String>, DownloadFlags),
|
2023-01-19 16:05:02 -08:00
|
|
|
|
|
|
|
/// Edit a sent message.
|
|
|
|
Edit,
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// React to a message with an Emoji.
|
|
|
|
React(String),
|
|
|
|
|
|
|
|
/// Redact a message, with an optional reason.
|
2023-04-28 16:52:33 -07:00
|
|
|
///
|
|
|
|
/// The [bool] argument indicates whether to skip confirmation.
|
|
|
|
Redact(Option<String>, bool),
|
2023-01-19 16:05:02 -08:00
|
|
|
|
|
|
|
/// Reply to a message.
|
2023-01-12 21:20:32 -08:00
|
|
|
Reply,
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// Unreact to a message.
|
|
|
|
///
|
|
|
|
/// If no specific Emoji to remove to is specified, then all reactions from the user on the
|
|
|
|
/// message are removed.
|
|
|
|
Unreact(Option<String>),
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
|
2023-03-03 16:37:11 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum CreateRoomType {
|
|
|
|
/// A direct message room.
|
|
|
|
Direct(OwnedUserId),
|
2023-03-04 12:23:17 -08:00
|
|
|
|
|
|
|
/// A standard chat room.
|
|
|
|
Room,
|
|
|
|
|
|
|
|
/// A Matrix space.
|
|
|
|
Space,
|
2023-03-03 16:37:11 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
bitflags::bitflags! {
|
|
|
|
pub struct CreateRoomFlags: u32 {
|
|
|
|
const NONE = 0b00000000;
|
|
|
|
|
|
|
|
/// Make the room public.
|
|
|
|
const PUBLIC = 0b00000001;
|
|
|
|
|
|
|
|
/// Encrypt this room.
|
|
|
|
const ENCRYPTED = 0b00000010;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-28 12:29:06 +00:00
|
|
|
bitflags::bitflags! {
|
|
|
|
pub struct DownloadFlags: u32 {
|
|
|
|
const NONE = 0b00000000;
|
|
|
|
|
|
|
|
/// Overwrite file if it already exists.
|
|
|
|
const FORCE = 0b00000001;
|
|
|
|
|
|
|
|
/// Open file after downloading.
|
|
|
|
const OPEN = 0b00000010;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:12:25 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2023-01-25 17:54:16 -08:00
|
|
|
pub enum RoomField {
|
|
|
|
Name,
|
|
|
|
Tag(TagName),
|
|
|
|
Topic,
|
2023-01-05 18:12:25 -08:00
|
|
|
}
|
|
|
|
|
2023-01-04 12:51:33 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum RoomAction {
|
2023-01-11 17:54:49 -08:00
|
|
|
InviteAccept,
|
|
|
|
InviteReject,
|
|
|
|
InviteSend(OwnedUserId),
|
2023-04-28 16:52:33 -07:00
|
|
|
Leave(bool),
|
2023-01-04 12:51:33 -08:00
|
|
|
Members(Box<CommandContext<ProgramContext>>),
|
2023-01-25 17:54:16 -08:00
|
|
|
Set(RoomField, String),
|
|
|
|
Unset(RoomField),
|
2023-01-04 12:51:33 -08:00
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SendAction {
|
|
|
|
Submit,
|
|
|
|
Upload(String),
|
2023-05-06 20:56:02 +01:00
|
|
|
UploadImage(usize, usize, Cow<'static, [u8]>),
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
|
2023-03-04 12:23:17 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum HomeserverAction {
|
|
|
|
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum IambAction {
|
2023-03-04 12:23:17 -08:00
|
|
|
Homeserver(HomeserverAction),
|
2023-01-10 19:59:30 -08:00
|
|
|
Message(MessageAction),
|
2023-01-04 12:51:33 -08:00
|
|
|
Room(RoomAction),
|
2023-01-10 19:59:30 -08:00
|
|
|
Send(SendAction),
|
2022-12-29 18:00:59 -08:00
|
|
|
Verify(VerifyAction, String),
|
|
|
|
VerifyRequest(String),
|
|
|
|
ToggleScrollbackFocus,
|
|
|
|
}
|
|
|
|
|
2023-03-04 12:23:17 -08:00
|
|
|
impl From<HomeserverAction> for IambAction {
|
|
|
|
fn from(act: HomeserverAction) -> Self {
|
|
|
|
IambAction::Homeserver(act)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
impl From<MessageAction> for IambAction {
|
|
|
|
fn from(act: MessageAction) -> Self {
|
|
|
|
IambAction::Message(act)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-05 18:12:25 -08:00
|
|
|
impl From<RoomAction> for IambAction {
|
|
|
|
fn from(act: RoomAction) -> Self {
|
|
|
|
IambAction::Room(act)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
impl From<SendAction> for IambAction {
|
|
|
|
fn from(act: SendAction) -> Self {
|
|
|
|
IambAction::Send(act)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
impl ApplicationAction for IambAction {
|
|
|
|
fn is_edit_sequence<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
|
|
|
match self {
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Break,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Break,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => SequenceStatus::Break,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Send(..) => SequenceStatus::Break,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Break,
|
|
|
|
IambAction::Verify(..) => SequenceStatus::Break,
|
|
|
|
IambAction::VerifyRequest(..) => SequenceStatus::Break,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_last_action<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
|
|
|
match self {
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Atom,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Atom,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => SequenceStatus::Atom,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Send(..) => SequenceStatus::Atom,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Atom,
|
|
|
|
IambAction::Verify(..) => SequenceStatus::Atom,
|
|
|
|
IambAction::VerifyRequest(..) => SequenceStatus::Atom,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_last_selection<C: EditContext>(&self, _: &C) -> SequenceStatus {
|
|
|
|
match self {
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Ignore,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Ignore,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => SequenceStatus::Ignore,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Send(..) => SequenceStatus::Ignore,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore,
|
|
|
|
IambAction::Verify(..) => SequenceStatus::Ignore,
|
|
|
|
IambAction::VerifyRequest(..) => SequenceStatus::Ignore,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_switchable<C: EditContext>(&self, _: &C) -> bool {
|
|
|
|
match self {
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => false,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => false,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => false,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Send(..) => false,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambAction::ToggleScrollbackFocus => false,
|
|
|
|
IambAction::Verify(..) => false,
|
|
|
|
IambAction::VerifyRequest(..) => false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-25 17:54:16 -08:00
|
|
|
impl From<RoomAction> for ProgramAction {
|
|
|
|
fn from(act: RoomAction) -> Self {
|
|
|
|
IambAction::from(act).into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
impl From<IambAction> for ProgramAction {
|
|
|
|
fn from(act: IambAction) -> Self {
|
|
|
|
Action::Application(act)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type ProgramAction = Action<IambInfo>;
|
|
|
|
pub type ProgramContext = VimContext<IambInfo>;
|
|
|
|
pub type Keybindings = VimMachine<TerminalKey, IambInfo>;
|
|
|
|
pub type ProgramCommand = VimCommand<ProgramContext, IambInfo>;
|
|
|
|
pub type ProgramCommands = VimCommandMachine<ProgramContext, IambInfo>;
|
|
|
|
pub type ProgramStore = Store<IambInfo>;
|
|
|
|
pub type AsyncProgramStore = Arc<AsyncMutex<ProgramStore>>;
|
|
|
|
|
|
|
|
pub type IambResult<T> = UIResult<T, IambInfo>;
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// Reaction events for some message.
|
|
|
|
///
|
|
|
|
/// The event identifier used as a key here is the ID for the reaction, and not for the message
|
|
|
|
/// it's reacting to.
|
|
|
|
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId)>;
|
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
pub type Receipts = HashMap<OwnedEventId, Vec<OwnedUserId>>;
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum IambError {
|
2023-01-11 17:54:49 -08:00
|
|
|
#[error("Invalid user identifier: {0}")]
|
2022-12-29 18:00:59 -08:00
|
|
|
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),
|
|
|
|
|
2023-07-07 20:35:01 -07:00
|
|
|
#[error("No download directory configured")]
|
|
|
|
NoDownloadDir,
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("Selected message does not have any attachments")]
|
|
|
|
NoAttachment,
|
|
|
|
|
|
|
|
#[error("No message currently selected")]
|
|
|
|
NoSelectedMessage,
|
|
|
|
|
|
|
|
#[error("Current window is not a room or space")]
|
|
|
|
NoSelectedRoomOrSpace,
|
|
|
|
|
|
|
|
#[error("Current window is not a room")]
|
|
|
|
NoSelectedRoom,
|
|
|
|
|
2023-01-11 17:54:49 -08:00
|
|
|
#[error("You do not have a current invitation to this room")]
|
|
|
|
NotInvited,
|
|
|
|
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("You need to join the room before you can do that")]
|
|
|
|
NotJoined,
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Unknown room identifier: {0}")]
|
|
|
|
UnknownRoom(OwnedRoomId),
|
|
|
|
|
|
|
|
#[error("Verification request error: {0}")]
|
|
|
|
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
|
2023-05-06 20:56:02 +01:00
|
|
|
|
|
|
|
#[error("Image error: {0}")]
|
|
|
|
Image(#[from] image::ImageError),
|
|
|
|
|
|
|
|
#[error("Could not use system clipboard data")]
|
|
|
|
Clipboard,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<IambError> for UIError<IambInfo> {
|
|
|
|
fn from(err: IambError) -> Self {
|
|
|
|
UIError::Application(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ApplicationError for IambError {}
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub enum RoomFetchStatus {
|
|
|
|
Done,
|
|
|
|
HaveMore(String),
|
|
|
|
#[default]
|
|
|
|
NotStarted,
|
|
|
|
}
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
pub enum EventLocation {
|
|
|
|
Message(MessageKey),
|
|
|
|
Reaction(OwnedEventId),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EventLocation {
|
|
|
|
fn to_message_key(&self) -> Option<&MessageKey> {
|
|
|
|
if let EventLocation::Message(key) = self {
|
|
|
|
Some(key)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Default)]
|
|
|
|
pub struct RoomInfo {
|
2023-02-09 17:53:33 -08:00
|
|
|
/// The display name for this room.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub name: Option<String>,
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// The tags placed on this room.
|
2023-01-25 17:54:16 -08:00
|
|
|
pub tags: Option<Tags>,
|
2023-01-19 16:05:02 -08:00
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// A map of event IDs to where they are stored in this struct.
|
|
|
|
pub keys: HashMap<OwnedEventId, EventLocation>,
|
|
|
|
|
|
|
|
/// The messages loaded for this room.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub messages: Messages,
|
2023-01-19 16:05:02 -08:00
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// A map of read markers to display on different events.
|
2023-01-26 15:40:16 -08:00
|
|
|
pub receipts: HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// An event ID for where we should indicate we've read up to.
|
2023-01-26 15:40:16 -08:00
|
|
|
pub read_till: Option<OwnedEventId>,
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// A map of message identifiers to a map of reaction events.
|
|
|
|
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
|
|
|
|
2023-03-13 10:46:26 -07:00
|
|
|
/// Whether the scrollback for this room is currently being fetched.
|
|
|
|
pub fetching: bool,
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
/// Where to continue fetching from when we continue loading scrollback history.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fetch_id: RoomFetchStatus,
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// The time that we last fetched scrollback for this room.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fetch_last: Option<Instant>,
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// Users currently typing in this room, and when we received notification of them doing so.
|
2023-01-03 13:57:28 -08:00
|
|
|
pub users_typing: Option<(Instant, Vec<OwnedUserId>)>,
|
2023-07-06 23:15:58 -07:00
|
|
|
|
|
|
|
/// The display names for users in this room.
|
|
|
|
pub display_names: HashMap<OwnedUserId, String>,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl RoomInfo {
|
2023-02-09 17:53:33 -08:00
|
|
|
pub fn get_reactions(&self, event_id: &EventId) -> Vec<(&str, usize)> {
|
|
|
|
if let Some(reacts) = self.reactions.get(event_id) {
|
|
|
|
let mut counts = HashMap::new();
|
|
|
|
|
|
|
|
for (key, _) in reacts.values() {
|
|
|
|
let count = counts.entry(key.as_str()).or_default();
|
|
|
|
*count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut reactions = counts.into_iter().collect::<Vec<_>>();
|
|
|
|
reactions.sort();
|
|
|
|
|
|
|
|
reactions
|
|
|
|
} else {
|
|
|
|
vec![]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-01 22:14:08 -07:00
|
|
|
pub fn get_message_key(&self, event_id: &EventId) -> Option<&MessageKey> {
|
|
|
|
self.keys.get(event_id)?.to_message_key()
|
|
|
|
}
|
|
|
|
|
2023-01-19 16:05:02 -08:00
|
|
|
pub fn get_event(&self, event_id: &EventId) -> Option<&Message> {
|
2023-05-01 22:14:08 -07:00
|
|
|
self.messages.get(self.get_message_key(event_id)?)
|
2023-02-09 17:53:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert_reaction(&mut self, react: ReactionEvent) {
|
|
|
|
match react {
|
|
|
|
MessageLikeEvent::Original(react) => {
|
|
|
|
let rel_id = react.content.relates_to.event_id;
|
|
|
|
let key = react.content.relates_to.key;
|
|
|
|
|
|
|
|
let message = self.reactions.entry(rel_id.clone()).or_default();
|
|
|
|
let event_id = react.event_id;
|
|
|
|
let user_id = react.sender;
|
|
|
|
|
|
|
|
message.insert(event_id.clone(), (key, user_id));
|
|
|
|
|
|
|
|
let loc = EventLocation::Reaction(rel_id);
|
|
|
|
self.keys.insert(event_id, loc);
|
|
|
|
},
|
|
|
|
MessageLikeEvent::Redacted(_) => {
|
|
|
|
return;
|
|
|
|
},
|
|
|
|
}
|
2023-01-19 16:05:02 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert_edit(&mut self, msg: Replacement) {
|
|
|
|
let event_id = msg.event_id;
|
|
|
|
let new_content = msg.new_content;
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
let key = if let Some(EventLocation::Message(k)) = self.keys.get(&event_id) {
|
2023-01-19 16:05:02 -08:00
|
|
|
k
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
let msg = if let Some(msg) = self.messages.get_mut(key) {
|
|
|
|
msg
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
match &mut msg.event {
|
|
|
|
MessageEvent::Original(orig) => {
|
2023-05-01 22:14:08 -07:00
|
|
|
orig.content.msgtype = new_content.msgtype;
|
2023-01-19 16:05:02 -08:00
|
|
|
},
|
2023-01-26 15:40:16 -08:00
|
|
|
MessageEvent::Local(_, content) => {
|
2023-05-01 22:14:08 -07:00
|
|
|
content.msgtype = new_content.msgtype;
|
2023-01-19 16:05:02 -08:00
|
|
|
},
|
2023-03-13 16:43:04 -07:00
|
|
|
MessageEvent::Redacted(_) |
|
|
|
|
MessageEvent::EncryptedOriginal(_) |
|
|
|
|
MessageEvent::EncryptedRedacted(_) => {
|
2023-01-19 16:05:02 -08:00
|
|
|
return;
|
|
|
|
},
|
|
|
|
}
|
2023-03-05 12:48:31 -08:00
|
|
|
|
|
|
|
msg.html = msg.event.html();
|
2023-01-19 16:05:02 -08:00
|
|
|
}
|
|
|
|
|
2023-03-13 15:18:53 -07:00
|
|
|
/// Inserts events that couldn't be decrypted into the scrollback.
|
|
|
|
pub fn insert_encrypted(&mut self, msg: RoomEncryptedEvent) {
|
|
|
|
let event_id = msg.event_id().to_owned();
|
|
|
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
|
|
|
|
|
|
|
self.keys.insert(event_id, EventLocation::Message(key.clone()));
|
|
|
|
self.messages.insert(key, msg.into());
|
|
|
|
}
|
|
|
|
|
2023-01-19 16:05:02 -08:00
|
|
|
pub fn insert_message(&mut self, msg: RoomMessageEvent) {
|
|
|
|
let event_id = msg.event_id().to_owned();
|
|
|
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
|
|
|
|
2023-02-09 17:53:33 -08:00
|
|
|
self.keys.insert(event_id.clone(), EventLocation::Message(key.clone()));
|
2023-01-19 16:05:02 -08:00
|
|
|
self.messages.insert(key, msg.into());
|
|
|
|
|
|
|
|
// Remove any echo.
|
|
|
|
let key = (MessageTimeStamp::LocalEcho, event_id);
|
|
|
|
let _ = self.messages.remove(&key);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert(&mut self, msg: RoomMessageEvent) {
|
|
|
|
match msg {
|
|
|
|
RoomMessageEvent::Original(OriginalRoomMessageEvent {
|
|
|
|
content:
|
|
|
|
RoomMessageEventContent { relates_to: Some(Relation::Replacement(repl)), .. },
|
|
|
|
..
|
|
|
|
}) => self.insert_edit(repl),
|
|
|
|
_ => self.insert_message(msg),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-13 10:46:26 -07:00
|
|
|
pub fn recently_fetched(&self) -> bool {
|
2022-12-29 18:00:59 -08:00
|
|
|
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
|
|
|
}
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
fn get_typers(&self) -> &[OwnedUserId] {
|
|
|
|
if let Some((t, users)) = &self.users_typing {
|
|
|
|
if t.elapsed() < Duration::from_secs(4) {
|
|
|
|
return users.as_ref();
|
|
|
|
} else {
|
|
|
|
return &[];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return &[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-26 15:40:16 -08:00
|
|
|
fn get_typing_spans<'a>(&'a self, settings: &'a ApplicationSettings) -> Spans<'a> {
|
2023-01-03 13:57:28 -08:00
|
|
|
let typers = self.get_typers();
|
|
|
|
let n = typers.len();
|
|
|
|
|
|
|
|
match n {
|
|
|
|
0 => Spans(vec![]),
|
|
|
|
1 => {
|
2023-07-06 23:15:58 -07:00
|
|
|
let user = settings.get_user_span(typers[0].as_ref(), self);
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
Spans(vec![user, Span::from(" is typing...")])
|
|
|
|
},
|
|
|
|
2 => {
|
2023-07-06 23:15:58 -07:00
|
|
|
let user1 = settings.get_user_span(typers[0].as_ref(), self);
|
|
|
|
let user2 = settings.get_user_span(typers[1].as_ref(), self);
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
Spans(vec![
|
|
|
|
user1,
|
|
|
|
Span::raw(" and "),
|
|
|
|
user2,
|
|
|
|
Span::from(" are typing..."),
|
|
|
|
])
|
|
|
|
},
|
|
|
|
n if n < 5 => Spans::from("Several people are typing..."),
|
|
|
|
_ => Spans::from("Many people are typing..."),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_typing(&mut self, user_ids: Vec<OwnedUserId>) {
|
|
|
|
self.users_typing = (Instant::now(), user_ids).into();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render_typing(
|
|
|
|
&mut self,
|
|
|
|
area: Rect,
|
|
|
|
buf: &mut Buffer,
|
|
|
|
settings: &ApplicationSettings,
|
|
|
|
) -> Rect {
|
|
|
|
if area.height <= 2 || area.width <= 20 {
|
|
|
|
return area;
|
|
|
|
}
|
|
|
|
|
|
|
|
if !settings.tunables.typing_notice_display {
|
|
|
|
return area;
|
|
|
|
}
|
|
|
|
|
|
|
|
let top = Rect::new(area.x, area.y, area.width, area.height - 1);
|
|
|
|
let bar = Rect::new(area.x, area.y + top.height, area.width, 1);
|
|
|
|
|
2023-01-06 16:56:28 -08:00
|
|
|
Paragraph::new(self.get_typing_spans(settings))
|
2023-01-03 13:57:28 -08:00
|
|
|
.alignment(Alignment::Center)
|
|
|
|
.render(bar, buf);
|
|
|
|
|
|
|
|
return top;
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn emoji_map() -> CompletionMap<String, &'static Emoji> {
|
|
|
|
let mut emojis = CompletionMap::default();
|
|
|
|
|
|
|
|
for emoji in emojis::iter() {
|
|
|
|
for shortcode in emoji.shortcodes() {
|
|
|
|
emojis.insert(shortcode.to_string(), emoji);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return emojis;
|
|
|
|
}
|
|
|
|
|
2023-07-05 15:25:42 -07:00
|
|
|
#[derive(Default)]
|
|
|
|
pub struct SyncInfo {
|
|
|
|
pub spaces: Vec<MatrixRoom>,
|
|
|
|
pub rooms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
|
|
|
|
pub dms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub struct ChatStore {
|
2023-03-01 18:46:33 -08:00
|
|
|
pub cmds: ProgramCommands,
|
2022-12-29 18:00:59 -08:00
|
|
|
pub worker: Requester,
|
2023-03-01 18:46:33 -08:00
|
|
|
pub rooms: CompletionMap<OwnedRoomId, RoomInfo>,
|
|
|
|
pub names: CompletionMap<String, OwnedRoomId>,
|
|
|
|
pub presences: CompletionMap<OwnedUserId, PresenceState>,
|
2022-12-29 18:00:59 -08:00
|
|
|
pub verifications: HashMap<String, SasVerification>,
|
|
|
|
pub settings: ApplicationSettings,
|
|
|
|
pub need_load: HashSet<OwnedRoomId>,
|
2023-03-01 18:46:33 -08:00
|
|
|
pub emojis: CompletionMap<String, &'static Emoji>,
|
2023-07-05 15:25:42 -07:00
|
|
|
pub sync_info: SyncInfo,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ChatStore {
|
|
|
|
pub fn new(worker: Requester, settings: ApplicationSettings) -> Self {
|
|
|
|
ChatStore {
|
|
|
|
worker,
|
|
|
|
settings,
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
cmds: crate::commands::setup_commands(),
|
2023-07-05 15:25:42 -07:00
|
|
|
emojis: emoji_map(),
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
names: Default::default(),
|
|
|
|
rooms: Default::default(),
|
2023-03-01 18:46:33 -08:00
|
|
|
presences: Default::default(),
|
2022-12-29 18:00:59 -08:00
|
|
|
verifications: Default::default(),
|
|
|
|
need_load: Default::default(),
|
2023-07-05 15:25:42 -07:00
|
|
|
sync_info: Default::default(),
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-25 17:54:16 -08:00
|
|
|
pub fn get_joined_room(&self, room_id: &RoomId) -> Option<Joined> {
|
|
|
|
self.worker.client.get_joined_room(room_id)
|
|
|
|
}
|
|
|
|
|
2023-01-04 12:51:33 -08:00
|
|
|
pub fn get_room_title(&self, room_id: &RoomId) -> String {
|
|
|
|
self.rooms
|
|
|
|
.get(room_id)
|
|
|
|
.and_then(|i| i.name.as_ref())
|
|
|
|
.map(String::from)
|
|
|
|
.unwrap_or_else(|| "Untitled Matrix Room".to_string())
|
|
|
|
}
|
|
|
|
|
2023-05-12 17:42:25 -07:00
|
|
|
pub async fn set_receipts(
|
|
|
|
&mut self,
|
|
|
|
receipts: Vec<(OwnedRoomId, Receipts)>,
|
|
|
|
) -> Vec<(OwnedRoomId, OwnedEventId)> {
|
2023-01-26 15:40:16 -08:00
|
|
|
let mut updates = vec![];
|
|
|
|
|
|
|
|
for (room_id, receipts) in receipts.into_iter() {
|
|
|
|
if let Some(info) = self.rooms.get_mut(&room_id) {
|
|
|
|
info.receipts = receipts;
|
|
|
|
|
|
|
|
if let Some(read_till) = info.read_till.take() {
|
|
|
|
updates.push((room_id, read_till));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-12 17:42:25 -07:00
|
|
|
return updates;
|
2023-01-26 15:40:16 -08:00
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn mark_for_load(&mut self, room_id: OwnedRoomId) {
|
|
|
|
self.need_load.insert(room_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo {
|
2023-03-01 18:46:33 -08:00
|
|
|
self.rooms.get_or_default(room_id)
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) {
|
2023-03-01 18:46:33 -08:00
|
|
|
self.rooms.get_or_default(room_id.to_owned()).name = name.to_string().into();
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-06-28 23:42:31 -07:00
|
|
|
/// A Matrix room.
|
2022-12-29 18:00:59 -08:00
|
|
|
Room(OwnedRoomId),
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:rooms` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
DirectList,
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:members` window for a given Matrix room.
|
2023-01-04 12:51:33 -08:00
|
|
|
MemberList(OwnedRoomId),
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:rooms` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
RoomList,
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:spaces` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
SpaceList,
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:verify` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
VerifyList,
|
2023-06-28 23:42:31 -07:00
|
|
|
|
|
|
|
/// The `:welcome` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
Welcome,
|
|
|
|
}
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
impl Display for IambId {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
IambId::Room(room_id) => {
|
|
|
|
write!(f, "iamb://room/{room_id}")
|
|
|
|
},
|
|
|
|
IambId::MemberList(room_id) => {
|
|
|
|
write!(f, "iamb://members/{room_id}")
|
|
|
|
},
|
|
|
|
IambId::DirectList => f.write_str("iamb://dms"),
|
|
|
|
IambId::RoomList => f.write_str("iamb://rooms"),
|
|
|
|
IambId::SpaceList => f.write_str("iamb://spaces"),
|
|
|
|
IambId::VerifyList => f.write_str("iamb://verify"),
|
|
|
|
IambId::Welcome => f.write_str("iamb://welcome"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
impl ApplicationWindowId for IambId {}
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
impl Serialize for IambId {
|
|
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
|
|
where
|
|
|
|
S: Serializer,
|
|
|
|
{
|
|
|
|
serializer.serialize_str(&self.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for IambId {
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
deserializer.deserialize_str(IambIdVisitor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct IambIdVisitor;
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for IambIdVisitor {
|
|
|
|
type Value = IambId;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
formatter.write_str("a valid window URL")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: SerdeError,
|
|
|
|
{
|
|
|
|
let Ok(url) = Url::parse(value) else {
|
|
|
|
return Err(E::custom("Invalid iamb window URL"));
|
|
|
|
};
|
|
|
|
|
|
|
|
if url.scheme() != "iamb" {
|
|
|
|
return Err(E::custom("Invalid iamb window URL"));
|
|
|
|
}
|
|
|
|
|
|
|
|
match url.domain() {
|
|
|
|
Some("room") => {
|
|
|
|
let Some(path) = url.path_segments() else {
|
|
|
|
return Err(E::custom("Invalid members window URL"));
|
|
|
|
};
|
|
|
|
|
|
|
|
let &[room_id] = path.collect::<Vec<_>>().as_slice() else {
|
|
|
|
return Err(E::custom("Invalid members window URL"));
|
|
|
|
};
|
|
|
|
|
|
|
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
|
|
|
return Err(E::custom("Invalid room identifier"));
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(IambId::Room(room_id))
|
|
|
|
},
|
|
|
|
Some("members") => {
|
|
|
|
let Some(path) = url.path_segments() else {
|
2023-07-06 23:15:58 -07:00
|
|
|
return Err(E::custom("Invalid members window URL"));
|
2023-06-28 23:42:31 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
let &[room_id] = path.collect::<Vec<_>>().as_slice() else {
|
2023-07-06 23:15:58 -07:00
|
|
|
return Err(E::custom("Invalid members window URL"));
|
2023-06-28 23:42:31 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
|
|
|
return Err(E::custom("Invalid room identifier"));
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(IambId::MemberList(room_id))
|
|
|
|
},
|
|
|
|
Some("dms") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://dms takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::DirectList)
|
|
|
|
},
|
|
|
|
Some("rooms") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://rooms takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::RoomList)
|
|
|
|
},
|
|
|
|
Some("spaces") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://spaces takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::SpaceList)
|
|
|
|
},
|
|
|
|
Some("verify") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://verify takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::VerifyList)
|
|
|
|
},
|
|
|
|
Some("welcome") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://welcome takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::Welcome)
|
|
|
|
},
|
|
|
|
Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))),
|
|
|
|
None => Err(E::custom("Invalid iamb window URL")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
#[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 {
|
2023-03-01 18:46:33 -08:00
|
|
|
Command(CommandType),
|
2022-12-29 18:00:59 -08:00
|
|
|
Room(OwnedRoomId, RoomFocus),
|
|
|
|
DirectList,
|
2023-01-04 12:51:33 -08:00
|
|
|
MemberList(OwnedRoomId),
|
2022-12-29 18:00:59 -08:00
|
|
|
RoomList,
|
|
|
|
SpaceList,
|
|
|
|
VerifyList,
|
|
|
|
Welcome,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl IambBufferId {
|
|
|
|
pub fn to_window(&self) -> Option<IambId> {
|
|
|
|
match self {
|
2023-03-01 18:46:33 -08:00
|
|
|
IambBufferId::Command(_) => None,
|
2022-12-29 18:00:59 -08:00
|
|
|
IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())),
|
|
|
|
IambBufferId::DirectList => Some(IambId::DirectList),
|
2023-01-04 12:51:33 -08:00
|
|
|
IambBufferId::MemberList(room) => Some(IambId::MemberList(room.clone())),
|
2022-12-29 18:00:59 -08:00
|
|
|
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;
|
2023-03-01 18:46:33 -08:00
|
|
|
|
|
|
|
fn complete(
|
|
|
|
text: &EditRope,
|
|
|
|
cursor: &mut Cursor,
|
|
|
|
content: &IambBufferId,
|
|
|
|
store: &mut ProgramStore,
|
|
|
|
) -> Vec<String> {
|
|
|
|
match content {
|
|
|
|
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
|
|
|
|
IambBufferId::Command(CommandType::Search) => vec![],
|
|
|
|
|
2023-05-24 21:14:13 -07:00
|
|
|
IambBufferId::Room(_, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store),
|
2023-03-01 18:46:33 -08:00
|
|
|
IambBufferId::Room(_, RoomFocus::Scrollback) => vec![],
|
|
|
|
|
|
|
|
IambBufferId::DirectList => vec![],
|
|
|
|
IambBufferId::MemberList(_) => vec![],
|
|
|
|
IambBufferId::RoomList => vec![],
|
|
|
|
IambBufferId::SpaceList => vec![],
|
|
|
|
IambBufferId::VerifyList => vec![],
|
|
|
|
IambBufferId::Welcome => vec![],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn content_of_command(ct: CommandType) -> IambBufferId {
|
|
|
|
IambBufferId::Command(ct)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
|
|
|
let id = text
|
|
|
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
|
|
|
.unwrap_or_else(EditRope::empty);
|
|
|
|
let id = Cow::from(&id);
|
|
|
|
|
|
|
|
store
|
|
|
|
.application
|
|
|
|
.presences
|
|
|
|
.complete(id.as_ref())
|
|
|
|
.into_iter()
|
|
|
|
.map(|i| i.to_string())
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
2023-05-24 21:14:13 -07:00
|
|
|
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
|
|
|
let id = text
|
|
|
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
|
|
|
.unwrap_or_else(EditRope::empty);
|
|
|
|
let id = Cow::from(&id);
|
|
|
|
|
|
|
|
match id.chars().next() {
|
|
|
|
// Complete room aliases.
|
|
|
|
Some('#') => {
|
|
|
|
return store.application.names.complete(id.as_ref());
|
|
|
|
},
|
|
|
|
|
|
|
|
// Complete room identifiers.
|
|
|
|
Some('!') => {
|
|
|
|
return store
|
|
|
|
.application
|
|
|
|
.rooms
|
|
|
|
.complete(id.as_ref())
|
|
|
|
.into_iter()
|
|
|
|
.map(|i| i.to_string())
|
|
|
|
.collect();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Complete Emoji shortcodes.
|
|
|
|
Some(':') => {
|
|
|
|
let list = store.application.emojis.complete(&id[1..]);
|
|
|
|
let iter = list.into_iter().take(200).map(|s| format!(":{}:", s));
|
|
|
|
|
|
|
|
return iter.collect();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Complete usernames for @ and empty strings.
|
|
|
|
Some('@') | None => {
|
|
|
|
return store
|
|
|
|
.application
|
|
|
|
.presences
|
|
|
|
.complete(id.as_ref())
|
|
|
|
.into_iter()
|
|
|
|
.map(|i| i.to_string())
|
|
|
|
.collect();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Unknown sigil.
|
|
|
|
Some(_) => return vec![],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn complete_matrix_names(
|
|
|
|
text: &EditRope,
|
|
|
|
cursor: &mut Cursor,
|
|
|
|
store: &ProgramStore,
|
|
|
|
) -> Vec<String> {
|
|
|
|
let id = text
|
|
|
|
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
|
|
|
|
.unwrap_or_else(EditRope::empty);
|
|
|
|
let id = Cow::from(&id);
|
|
|
|
|
|
|
|
let list = store.application.names.complete(id.as_ref());
|
|
|
|
if !list.is_empty() {
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
let list = store.application.presences.complete(id.as_ref());
|
|
|
|
if !list.is_empty() {
|
|
|
|
return list.into_iter().map(|i| i.to_string()).collect();
|
|
|
|
}
|
|
|
|
|
|
|
|
store
|
|
|
|
.application
|
|
|
|
.rooms
|
|
|
|
.complete(id.as_ref())
|
|
|
|
.into_iter()
|
|
|
|
.map(|i| i.to_string())
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
|
|
|
let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
|
|
|
let sc = sc.unwrap_or_else(EditRope::empty);
|
|
|
|
let sc = Cow::from(&sc);
|
|
|
|
|
|
|
|
store.application.emojis.complete(sc.as_ref())
|
|
|
|
}
|
|
|
|
|
2023-05-01 21:14:19 -07:00
|
|
|
fn complete_cmdname(
|
|
|
|
desc: CommandDescription,
|
|
|
|
text: &EditRope,
|
|
|
|
cursor: &mut Cursor,
|
|
|
|
store: &ProgramStore,
|
|
|
|
) -> Vec<String> {
|
|
|
|
// Complete command name and set cursor position.
|
|
|
|
let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little);
|
|
|
|
store.application.cmds.complete_name(desc.command.as_str())
|
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
fn complete_cmdarg(
|
|
|
|
desc: CommandDescription,
|
|
|
|
text: &EditRope,
|
|
|
|
cursor: &mut Cursor,
|
|
|
|
store: &ProgramStore,
|
|
|
|
) -> Vec<String> {
|
|
|
|
let cmd = match store.application.cmds.get(desc.command.as_str()) {
|
|
|
|
Ok(cmd) => cmd,
|
|
|
|
Err(_) => return vec![],
|
|
|
|
};
|
|
|
|
|
|
|
|
match cmd.name.as_str() {
|
|
|
|
"cancel" | "dms" | "edit" | "redact" | "reply" => vec![],
|
|
|
|
"members" | "rooms" | "spaces" | "welcome" => vec![],
|
|
|
|
"download" | "open" | "upload" => complete_path(text, cursor),
|
|
|
|
"react" | "unreact" => complete_emoji(text, cursor, store),
|
|
|
|
|
|
|
|
"invite" => complete_users(text, cursor, store),
|
2023-05-01 21:14:19 -07:00
|
|
|
"join" | "split" | "vsplit" | "tabedit" => complete_matrix_names(text, cursor, store),
|
2023-03-01 18:46:33 -08:00
|
|
|
"room" => vec![],
|
|
|
|
"verify" => vec![],
|
2023-05-01 21:14:19 -07:00
|
|
|
"vertical" | "horizontal" | "aboveleft" | "belowright" | "tab" => {
|
|
|
|
complete_cmd(desc.arg.text.as_str(), text, cursor, store)
|
|
|
|
},
|
|
|
|
_ => vec![],
|
2023-03-01 18:46:33 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-01 21:14:19 -07:00
|
|
|
fn complete_cmd(
|
|
|
|
cmd: &str,
|
|
|
|
text: &EditRope,
|
|
|
|
cursor: &mut Cursor,
|
|
|
|
store: &ProgramStore,
|
|
|
|
) -> Vec<String> {
|
|
|
|
match CommandDescription::from_str(cmd) {
|
2023-03-01 18:46:33 -08:00
|
|
|
Ok(desc) => {
|
|
|
|
if desc.arg.untrimmed.is_empty() {
|
2023-05-01 21:14:19 -07:00
|
|
|
complete_cmdname(desc, text, cursor, store)
|
2023-03-01 18:46:33 -08:00
|
|
|
} else {
|
|
|
|
// Complete command argument.
|
|
|
|
complete_cmdarg(desc, text, cursor, store)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Can't parse command text, so return zero completions.
|
|
|
|
Err(_) => vec![],
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
2023-01-03 13:57:28 -08:00
|
|
|
|
2023-05-01 21:14:19 -07:00
|
|
|
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
|
|
|
|
let eo = text.cursor_to_offset(cursor);
|
|
|
|
let slice = text.slice(0.into(), eo, false);
|
|
|
|
let cow = Cow::from(&slice);
|
|
|
|
|
|
|
|
complete_cmd(cow.as_ref(), text, cursor, store)
|
|
|
|
}
|
|
|
|
|
2023-01-03 13:57:28 -08:00
|
|
|
#[cfg(test)]
|
|
|
|
pub mod tests {
|
|
|
|
use super::*;
|
2023-01-26 15:40:16 -08:00
|
|
|
use crate::config::user_style_from_color;
|
2023-01-03 13:57:28 -08:00
|
|
|
use crate::tests::*;
|
2023-01-06 16:56:28 -08:00
|
|
|
use modalkit::tui::style::Color;
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_typing_spans() {
|
|
|
|
let mut info = RoomInfo::default();
|
2023-01-06 16:56:28 -08:00
|
|
|
let settings = mock_settings();
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
let users0 = vec![];
|
|
|
|
let users1 = vec![TEST_USER1.clone()];
|
|
|
|
let users2 = vec![TEST_USER1.clone(), TEST_USER2.clone()];
|
|
|
|
let users4 = vec![
|
|
|
|
TEST_USER1.clone(),
|
|
|
|
TEST_USER2.clone(),
|
|
|
|
TEST_USER3.clone(),
|
|
|
|
TEST_USER4.clone(),
|
|
|
|
];
|
|
|
|
let users5 = vec![
|
|
|
|
TEST_USER1.clone(),
|
|
|
|
TEST_USER2.clone(),
|
|
|
|
TEST_USER3.clone(),
|
|
|
|
TEST_USER4.clone(),
|
|
|
|
TEST_USER5.clone(),
|
|
|
|
];
|
|
|
|
|
|
|
|
// Nothing set.
|
|
|
|
assert_eq!(info.users_typing, None);
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
// Empty typing list.
|
|
|
|
info.set_typing(users0);
|
|
|
|
assert!(info.users_typing.is_some());
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Spans(vec![]));
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
// Single user typing.
|
|
|
|
info.set_typing(users1);
|
|
|
|
assert!(info.users_typing.is_some());
|
|
|
|
assert_eq!(
|
2023-01-06 16:56:28 -08:00
|
|
|
info.get_typing_spans(&settings),
|
2023-01-03 13:57:28 -08:00
|
|
|
Spans(vec![
|
|
|
|
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
|
|
|
Span::from(" is typing...")
|
|
|
|
])
|
|
|
|
);
|
|
|
|
|
|
|
|
// Two users typing.
|
|
|
|
info.set_typing(users2);
|
|
|
|
assert!(info.users_typing.is_some());
|
|
|
|
assert_eq!(
|
2023-01-06 16:56:28 -08:00
|
|
|
info.get_typing_spans(&settings),
|
2023-01-03 13:57:28 -08:00
|
|
|
Spans(vec![
|
|
|
|
Span::styled("@user1:example.com", user_style("@user1:example.com")),
|
|
|
|
Span::raw(" and "),
|
|
|
|
Span::styled("@user2:example.com", user_style("@user2:example.com")),
|
|
|
|
Span::raw(" are typing...")
|
|
|
|
])
|
|
|
|
);
|
|
|
|
|
|
|
|
// Four users typing.
|
|
|
|
info.set_typing(users4);
|
|
|
|
assert!(info.users_typing.is_some());
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Spans::from("Several people are typing..."));
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
// Five users typing.
|
|
|
|
info.set_typing(users5);
|
|
|
|
assert!(info.users_typing.is_some());
|
2023-01-06 16:56:28 -08:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Spans::from("Many people are typing..."));
|
|
|
|
|
|
|
|
// Test that USER5 gets rendered using the configured color and name.
|
|
|
|
info.set_typing(vec![TEST_USER5.clone()]);
|
|
|
|
assert!(info.users_typing.is_some());
|
|
|
|
assert_eq!(
|
|
|
|
info.get_typing_spans(&settings),
|
|
|
|
Spans(vec![
|
|
|
|
Span::styled("USER 5", user_style_from_color(Color::Black)),
|
|
|
|
Span::from(" is typing...")
|
|
|
|
])
|
|
|
|
);
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
2023-03-01 18:46:33 -08:00
|
|
|
|
2023-05-24 21:14:13 -07:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_complete_msgbar() {
|
|
|
|
let store = mock_store().await;
|
|
|
|
|
|
|
|
let text = EditRope::from("going for a walk :walk ");
|
|
|
|
let mut cursor = Cursor::new(0, 22);
|
|
|
|
let res = complete_msgbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec![":walking:", ":walking_man:", ":walking_woman:"]);
|
|
|
|
assert_eq!(cursor, Cursor::new(0, 17));
|
|
|
|
|
|
|
|
let text = EditRope::from("hello @user1 ");
|
|
|
|
let mut cursor = Cursor::new(0, 12);
|
|
|
|
let res = complete_msgbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec!["@user1:example.com"]);
|
|
|
|
assert_eq!(cursor, Cursor::new(0, 6));
|
|
|
|
|
|
|
|
let text = EditRope::from("see #room ");
|
|
|
|
let mut cursor = Cursor::new(0, 9);
|
|
|
|
let res = complete_msgbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec!["#room1:example.com"]);
|
|
|
|
assert_eq!(cursor, Cursor::new(0, 4));
|
|
|
|
}
|
|
|
|
|
2023-03-01 18:46:33 -08:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_complete_cmdbar() {
|
|
|
|
let store = mock_store().await;
|
2023-05-01 21:14:19 -07:00
|
|
|
let users = vec![
|
|
|
|
"@user1:example.com",
|
|
|
|
"@user2:example.com",
|
|
|
|
"@user3:example.com",
|
|
|
|
"@user4:example.com",
|
|
|
|
"@user5:example.com",
|
|
|
|
];
|
2023-03-01 18:46:33 -08:00
|
|
|
|
|
|
|
let text = EditRope::from("invite ");
|
|
|
|
let mut cursor = Cursor::new(0, 7);
|
|
|
|
let id = text
|
|
|
|
.get_prefix_word_mut(&mut cursor, &MATRIX_ID_WORD)
|
|
|
|
.unwrap_or_else(EditRope::empty);
|
|
|
|
assert_eq!(id.to_string(), "");
|
|
|
|
assert_eq!(cursor, Cursor::new(0, 7));
|
|
|
|
|
|
|
|
let text = EditRope::from("invite ");
|
|
|
|
let mut cursor = Cursor::new(0, 7);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
2023-05-01 21:14:19 -07:00
|
|
|
assert_eq!(res, users);
|
2023-03-01 18:46:33 -08:00
|
|
|
|
|
|
|
let text = EditRope::from("invite ignored");
|
|
|
|
let mut cursor = Cursor::new(0, 7);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
2023-05-01 21:14:19 -07:00
|
|
|
assert_eq!(res, users);
|
2023-03-01 18:46:33 -08:00
|
|
|
|
|
|
|
let text = EditRope::from("invite @user1ignored");
|
|
|
|
let mut cursor = Cursor::new(0, 13);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec!["@user1:example.com"]);
|
2023-05-01 21:14:19 -07:00
|
|
|
|
|
|
|
let text = EditRope::from("abo hor");
|
|
|
|
let mut cursor = Cursor::new(0, 7);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec!["horizontal"]);
|
|
|
|
|
|
|
|
let text = EditRope::from("abo hor inv");
|
|
|
|
let mut cursor = Cursor::new(0, 11);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, vec!["invite"]);
|
|
|
|
|
|
|
|
let text = EditRope::from("abo hor invite \n");
|
|
|
|
let mut cursor = Cursor::new(0, 15);
|
|
|
|
let res = complete_cmdbar(&text, &mut cursor, &store);
|
|
|
|
assert_eq!(res, users);
|
2023-03-01 18:46:33 -08:00
|
|
|
}
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|