2023-10-06 22:35:27 -07:00
|
|
|
//! # Common types and utilities
|
|
|
|
//!
|
|
|
|
//! The types defined here get used throughout iamb.
|
2023-03-01 18:46:33 -08:00
|
|
|
use std::borrow::Cow;
|
2023-12-18 20:55:04 -08:00
|
|
|
use std::collections::hash_map::IntoIter;
|
2024-04-02 15:40:25 +00:00
|
|
|
use std::collections::{BTreeSet, 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;
|
2024-02-27 21:21:05 -08:00
|
|
|
use ratatui::{
|
|
|
|
buffer::Buffer,
|
|
|
|
layout::{Alignment, Rect},
|
|
|
|
text::{Line, Span},
|
|
|
|
widgets::{Paragraph, Widget},
|
|
|
|
};
|
2023-11-16 08:36:22 -08:00
|
|
|
use ratatui_image::picker::{Picker, ProtocolType};
|
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,
|
2024-03-02 15:00:29 -08:00
|
|
|
room::Room as MatrixRoom,
|
2023-01-19 16:05:02 -08:00
|
|
|
ruma::{
|
2023-02-09 17:53:33 -08:00
|
|
|
events::{
|
|
|
|
reaction::ReactionEvent,
|
2024-03-09 00:47:05 -08:00
|
|
|
relation::{Replacement, Thread},
|
2023-03-13 15:18:53 -07:00
|
|
|
room::encrypted::RoomEncryptedEvent,
|
2023-02-09 17:53:33 -08:00
|
|
|
room::message::{
|
|
|
|
OriginalRoomMessageEvent,
|
|
|
|
Relation,
|
|
|
|
RoomMessageEvent,
|
|
|
|
RoomMessageEventContent,
|
2024-03-02 15:00:29 -08:00
|
|
|
RoomMessageEventContentWithoutRelation,
|
2023-02-09 17:53:33 -08:00
|
|
|
},
|
2024-03-23 19:20:06 -07:00
|
|
|
room::redaction::{OriginalSyncRoomRedactionEvent, SyncRoomRedactionEvent},
|
2023-02-09 17:53:33 -08:00
|
|
|
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,
|
2024-03-23 19:20:06 -07:00
|
|
|
RoomVersionId,
|
2024-02-28 09:03:28 -08:00
|
|
|
UserId,
|
2023-01-19 16:05:02 -08:00
|
|
|
},
|
2024-03-02 15:00:29 -08:00
|
|
|
RoomState as MatrixRoomState,
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
use modalkit::{
|
2024-02-28 23:00:25 -08:00
|
|
|
actions::Action,
|
2022-12-29 18:00:59 -08:00
|
|
|
editing::{
|
|
|
|
application::{
|
|
|
|
ApplicationAction,
|
|
|
|
ApplicationContentId,
|
|
|
|
ApplicationError,
|
|
|
|
ApplicationInfo,
|
|
|
|
ApplicationStore,
|
|
|
|
ApplicationWindowId,
|
|
|
|
},
|
2023-03-01 18:46:33 -08:00
|
|
|
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,
|
2023-01-03 13:57:28 -08:00
|
|
|
},
|
2024-02-28 23:00:25 -08:00
|
|
|
errors::{UIError, UIResult},
|
2024-02-27 21:21:05 -08:00
|
|
|
key::TerminalKey,
|
|
|
|
keybindings::SequenceStatus,
|
|
|
|
prelude::{CommandType, WordStyle},
|
2022-12-29 18:00:59 -08:00
|
|
|
};
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
use crate::config::ImagePreviewProtocolValues;
|
|
|
|
use crate::message::ImageStatus;
|
|
|
|
use crate::preview::{source_from_event, spawn_insert_preview};
|
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-10-06 22:35:27 -07:00
|
|
|
/// The set of characters used in different Matrix IDs.
|
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
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Empty type used solely to implement [ApplicationInfo].
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum IambInfo {}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action taken against an ongoing verification request.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum VerifyAction {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Accept a verification request.
|
2022-12-29 18:00:59 -08:00
|
|
|
Accept,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Cancel an in-progress verification.
|
2022-12-29 18:00:59 -08:00
|
|
|
Cancel,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Confirm an in-progress verification.
|
2022-12-29 18:00:59 -08:00
|
|
|
Confirm,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Reject an in-progress verification due to mismatched Emoji.
|
2022-12-29 18:00:59 -08:00
|
|
|
Mismatch,
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action taken against the currently selected message.
|
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.
|
2024-08-12 23:21:11 -07:00
|
|
|
///
|
|
|
|
/// `:react` will by default try to convert the [String] argument to an Emoji, and error when
|
|
|
|
/// it doesn't recognize it. The second [bool] argument forces it to be interpreted literally
|
|
|
|
/// when it is `true`.
|
|
|
|
React(String, bool),
|
2023-02-09 17:53:33 -08:00
|
|
|
|
|
|
|
/// 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.
|
2024-08-12 23:21:11 -07:00
|
|
|
///
|
|
|
|
/// Like `:react`, `:unreact` will by default try to convert the [String] argument to an Emoji,
|
|
|
|
/// and error when it doesn't recognize it. The second [bool] argument forces it to be
|
|
|
|
/// interpreted literally when it is `true`.
|
|
|
|
Unreact(Option<String>, bool),
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
/// An action taken in the currently selected space.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SpaceAction {
|
|
|
|
/// Add a room or update metadata.
|
|
|
|
///
|
|
|
|
/// The [`Option<String>`] argument is the order parameter.
|
|
|
|
/// The [`bool`] argument indicates whether the room is suggested.
|
|
|
|
SetChild(OwnedRoomId, Option<String>, bool),
|
|
|
|
|
|
|
|
/// Remove the selected room.
|
|
|
|
RemoveChild,
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The type of room being created.
|
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! {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Available options for newly created rooms.
|
2023-07-01 08:58:48 +01:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2023-03-03 16:37:11 -08:00
|
|
|
pub struct CreateRoomFlags: u32 {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// No flags specified.
|
2023-03-03 16:37:11 -08:00
|
|
|
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! {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Available options when downloading files.
|
2023-07-01 08:58:48 +01:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2023-01-28 12:29:06 +00:00
|
|
|
pub struct DownloadFlags: u32 {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// No flags specified.
|
2023-01-28 12:29:06 +00:00
|
|
|
const NONE = 0b00000000;
|
|
|
|
|
|
|
|
/// Overwrite file if it already exists.
|
|
|
|
const FORCE = 0b00000001;
|
|
|
|
|
|
|
|
/// Open file after downloading.
|
|
|
|
const OPEN = 0b00000010;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-20 19:32:33 -07:00
|
|
|
/// Fields that rooms and spaces can be sorted by.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SortFieldRoom {
|
2024-02-28 09:03:28 -08:00
|
|
|
/// Sort rooms by whether they have the Favorite tag.
|
2023-10-20 19:32:33 -07:00
|
|
|
Favorite,
|
2024-02-28 09:03:28 -08:00
|
|
|
|
|
|
|
/// Sort rooms by whether they have the Low Priority tag.
|
2023-10-20 19:32:33 -07:00
|
|
|
LowPriority,
|
2024-02-28 09:03:28 -08:00
|
|
|
|
|
|
|
/// Sort rooms by their room name.
|
2023-10-20 19:32:33 -07:00
|
|
|
Name,
|
2024-02-28 09:03:28 -08:00
|
|
|
|
|
|
|
/// Sort rooms by their canonical room alias.
|
2023-10-20 19:32:33 -07:00
|
|
|
Alias,
|
2024-02-28 09:03:28 -08:00
|
|
|
|
|
|
|
/// Sort rooms by their Matrix room identifier.
|
2023-10-20 19:32:33 -07:00
|
|
|
RoomId,
|
2024-02-28 09:03:28 -08:00
|
|
|
|
|
|
|
/// Sort rooms by whether they have unread messages.
|
|
|
|
Unread,
|
|
|
|
|
|
|
|
/// Sort rooms by the timestamps of their most recent messages.
|
|
|
|
Recent,
|
2025-05-15 04:39:22 +02:00
|
|
|
|
|
|
|
/// Sort rooms by whether they are invites.
|
|
|
|
Invite,
|
2023-10-20 19:32:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Fields that users can be sorted by.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SortFieldUser {
|
|
|
|
PowerLevel,
|
|
|
|
UserId,
|
|
|
|
LocalPart,
|
|
|
|
Server,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Whether to use the default sort direction for a field, or to reverse it.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SortOrder {
|
|
|
|
Ascending,
|
|
|
|
Descending,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// One of the columns to sort on.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub struct SortColumn<T>(pub T, pub SortOrder);
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for SortColumn<SortFieldRoom> {
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
deserializer.deserialize_str(SortRoomVisitor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// [serde] visitor for deserializing [SortColumn] for rooms and spaces.
|
|
|
|
struct SortRoomVisitor;
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for SortRoomVisitor {
|
|
|
|
type Value = SortColumn<SortFieldRoom>;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
formatter.write_str("a valid field for sorting rooms")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_str<E>(self, mut value: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: SerdeError,
|
|
|
|
{
|
|
|
|
if value.is_empty() {
|
|
|
|
return Err(E::custom("Invalid sort field"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let order = if value.starts_with('~') {
|
|
|
|
value = &value[1..];
|
|
|
|
SortOrder::Descending
|
|
|
|
} else {
|
|
|
|
SortOrder::Ascending
|
|
|
|
};
|
|
|
|
|
|
|
|
let field = match value {
|
|
|
|
"favorite" => SortFieldRoom::Favorite,
|
|
|
|
"lowpriority" => SortFieldRoom::LowPriority,
|
2024-02-28 09:03:28 -08:00
|
|
|
"recent" => SortFieldRoom::Recent,
|
|
|
|
"unread" => SortFieldRoom::Unread,
|
2023-10-20 19:32:33 -07:00
|
|
|
"name" => SortFieldRoom::Name,
|
|
|
|
"alias" => SortFieldRoom::Alias,
|
|
|
|
"id" => SortFieldRoom::RoomId,
|
2025-05-15 04:39:22 +02:00
|
|
|
"invite" => SortFieldRoom::Invite,
|
2023-10-20 19:32:33 -07:00
|
|
|
_ => {
|
|
|
|
let msg = format!("Unknown sort field: {value:?}");
|
|
|
|
return Err(E::custom(msg));
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(SortColumn(field, order))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for SortColumn<SortFieldUser> {
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
deserializer.deserialize_str(SortUserVisitor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// [serde] visitor for deserializing [SortColumn] for users.
|
|
|
|
struct SortUserVisitor;
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for SortUserVisitor {
|
|
|
|
type Value = SortColumn<SortFieldUser>;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
formatter.write_str("a valid field for sorting rooms")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_str<E>(self, mut value: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: SerdeError,
|
|
|
|
{
|
|
|
|
if value.is_empty() {
|
|
|
|
return Err(E::custom("Invalid field for sorting users"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let order = if value.starts_with('~') {
|
|
|
|
value = &value[1..];
|
|
|
|
SortOrder::Descending
|
|
|
|
} else {
|
|
|
|
SortOrder::Ascending
|
|
|
|
};
|
|
|
|
|
|
|
|
let field = match value {
|
|
|
|
"id" => SortFieldUser::UserId,
|
|
|
|
"localpart" => SortFieldUser::LocalPart,
|
|
|
|
"server" => SortFieldUser::Server,
|
|
|
|
"power" => SortFieldUser::PowerLevel,
|
|
|
|
_ => {
|
|
|
|
let msg = format!("Unknown sort field: {value:?}");
|
|
|
|
return Err(E::custom(msg));
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(SortColumn(field, order))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A room property.
|
2023-01-05 18:12:25 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
2023-01-25 17:54:16 -08:00
|
|
|
pub enum RoomField {
|
2024-08-18 00:33:45 -07:00
|
|
|
/// The room's history visibility.
|
|
|
|
History,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The room name.
|
2023-01-25 17:54:16 -08:00
|
|
|
Name,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
/// The room id.
|
|
|
|
Id,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A room tag.
|
2023-01-25 17:54:16 -08:00
|
|
|
Tag(TagName),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The room topic.
|
2023-01-25 17:54:16 -08:00
|
|
|
Topic,
|
2024-08-01 08:51:01 +02:00
|
|
|
|
2024-08-17 23:43:19 +02:00
|
|
|
/// Notification level.
|
|
|
|
NotificationMode,
|
|
|
|
|
2024-08-01 08:51:01 +02:00
|
|
|
/// The room's entire list of alternative aliases.
|
|
|
|
Aliases,
|
|
|
|
|
|
|
|
/// A specific alternative alias to the room.
|
|
|
|
Alias(String),
|
|
|
|
|
|
|
|
/// The room's canonical alias.
|
|
|
|
CanonicalAlias,
|
2023-01-05 18:12:25 -08:00
|
|
|
}
|
|
|
|
|
2024-08-17 18:50:48 -07:00
|
|
|
/// An action that operates on a room member.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum MemberUpdateAction {
|
|
|
|
Ban,
|
|
|
|
Kick,
|
|
|
|
Unban,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for MemberUpdateAction {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
MemberUpdateAction::Ban => write!(f, "ban"),
|
|
|
|
MemberUpdateAction::Kick => write!(f, "kick"),
|
|
|
|
MemberUpdateAction::Unban => write!(f, "unban"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action that operates on a focused room.
|
2023-01-04 12:51:33 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum RoomAction {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Accept an invitation to join this room.
|
2023-01-11 17:54:49 -08:00
|
|
|
InviteAccept,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Reject an invitation to join this room.
|
2023-01-11 17:54:49 -08:00
|
|
|
InviteReject,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Invite a user to this room.
|
2023-01-11 17:54:49 -08:00
|
|
|
InviteSend(OwnedUserId),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Leave this room.
|
2023-04-28 16:52:33 -07:00
|
|
|
Leave(bool),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
2024-08-17 18:50:48 -07:00
|
|
|
/// Update a user's membership in this room.
|
|
|
|
MemberUpdate(MemberUpdateAction, String, Option<String>, bool),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Open the members window.
|
2024-02-27 21:21:05 -08:00
|
|
|
Members(Box<CommandContext>),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
2024-03-31 00:12:57 -07:00
|
|
|
/// Set whether a room is a direct message.
|
|
|
|
SetDirect(bool),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Set a room property.
|
2023-01-25 17:54:16 -08:00
|
|
|
Set(RoomField, String),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Unset a room property.
|
2023-01-25 17:54:16 -08:00
|
|
|
Unset(RoomField),
|
2024-08-01 08:51:01 +02:00
|
|
|
|
|
|
|
/// List the values in a list room property.
|
|
|
|
Show(RoomField),
|
2023-01-04 12:51:33 -08:00
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action that sends a message to a room.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum SendAction {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Send the text in the message bar.
|
2023-01-10 19:59:30 -08:00
|
|
|
Submit,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Send text provided from an external editor.
|
2023-09-10 16:45:27 +03:00
|
|
|
SubmitFromEditor,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Upload a file.
|
2023-01-10 19:59:30 -08:00
|
|
|
Upload(String),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Upload the image data.
|
2023-05-06 20:56:02 +01:00
|
|
|
UploadImage(usize, usize, Cow<'static, [u8]>),
|
2023-01-10 19:59:30 -08:00
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action performed against the user's homeserver.
|
2023-03-04 12:23:17 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum HomeserverAction {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Create a new room with an optional localpart.
|
2023-03-04 12:23:17 -08:00
|
|
|
CreateRoom(Option<String>, CreateRoomType, CreateRoomFlags),
|
2023-10-13 00:58:59 -05:00
|
|
|
Logout(String, bool),
|
2023-03-04 12:23:17 -08:00
|
|
|
}
|
|
|
|
|
2024-03-28 20:58:34 -07:00
|
|
|
/// An action performed against the user's room keys.
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum KeysAction {
|
|
|
|
/// Export room keys to a file, encrypted with a passphrase.
|
|
|
|
Export(String, String),
|
|
|
|
/// Import room keys from a file, encrypted with a passphrase.
|
|
|
|
Import(String, String),
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An action that the main program loop should.
|
|
|
|
///
|
|
|
|
/// See [the commands module][super::commands] for where these are usually created.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
|
|
pub enum IambAction {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Perform an action against the homeserver.
|
2023-03-04 12:23:17 -08:00
|
|
|
Homeserver(HomeserverAction),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
2024-03-28 20:58:34 -07:00
|
|
|
/// Perform an action over room keys.
|
|
|
|
Keys(KeysAction),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Perform an action on the currently selected message.
|
2023-01-10 19:59:30 -08:00
|
|
|
Message(MessageAction),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
/// Perform an action on the current space.
|
|
|
|
Space(SpaceAction),
|
|
|
|
|
2023-10-07 18:24:25 -07:00
|
|
|
/// Open a URL.
|
|
|
|
OpenLink(String),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Perform an action on the currently focused room.
|
2023-01-04 12:51:33 -08:00
|
|
|
Room(RoomAction),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Send a message to the currently focused room.
|
2023-01-10 19:59:30 -08:00
|
|
|
Send(SendAction),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Perform an action for an in-progress verification.
|
2022-12-29 18:00:59 -08:00
|
|
|
Verify(VerifyAction, String),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Request a new verification with the specified user.
|
2022-12-29 18:00:59 -08:00
|
|
|
VerifyRequest(String),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Toggle the focus within the focused room.
|
2022-12-29 18:00:59 -08:00
|
|
|
ToggleScrollbackFocus,
|
2024-08-20 19:33:46 -07:00
|
|
|
|
|
|
|
/// Clear all unread messages.
|
|
|
|
ClearUnreads,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2023-09-10 16:45:27 +03:00
|
|
|
impl IambAction {
|
|
|
|
/// Indicates whether this action will draw over the screen.
|
|
|
|
pub fn scribbles(&self) -> bool {
|
2023-09-12 17:17:29 -07:00
|
|
|
matches!(self, IambAction::Send(SendAction::SubmitFromEditor))
|
2023-09-10 16:45:27 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
impl From<SpaceAction> for IambAction {
|
|
|
|
fn from(act: SpaceAction) -> Self {
|
|
|
|
IambAction::Space(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 {
|
2024-02-27 21:21:05 -08:00
|
|
|
fn is_edit_sequence(&self, _: &EditContext) -> SequenceStatus {
|
2022-12-29 18:00:59 -08:00
|
|
|
match self {
|
2024-08-20 19:33:46 -07:00
|
|
|
IambAction::ClearUnreads => SequenceStatus::Break,
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Break,
|
2024-03-28 20:58:34 -07:00
|
|
|
IambAction::Keys(..) => SequenceStatus::Break,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Break,
|
2025-05-15 03:26:35 +00:00
|
|
|
IambAction::Space(..) => SequenceStatus::Break,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => SequenceStatus::Break,
|
2023-10-07 18:24:25 -07:00
|
|
|
IambAction::OpenLink(..) => 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
fn is_last_action(&self, _: &EditContext) -> SequenceStatus {
|
2022-12-29 18:00:59 -08:00
|
|
|
match self {
|
2024-08-20 19:33:46 -07:00
|
|
|
IambAction::ClearUnreads => SequenceStatus::Atom,
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Atom,
|
2024-03-28 20:58:34 -07:00
|
|
|
IambAction::Keys(..) => SequenceStatus::Atom,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Atom,
|
2025-05-15 03:26:35 +00:00
|
|
|
IambAction::Space(..) => SequenceStatus::Atom,
|
2023-10-07 18:24:25 -07:00
|
|
|
IambAction::OpenLink(..) => 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
fn is_last_selection(&self, _: &EditContext) -> SequenceStatus {
|
2022-12-29 18:00:59 -08:00
|
|
|
match self {
|
2024-08-20 19:33:46 -07:00
|
|
|
IambAction::ClearUnreads => SequenceStatus::Ignore,
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => SequenceStatus::Ignore,
|
2024-03-28 20:58:34 -07:00
|
|
|
IambAction::Keys(..) => SequenceStatus::Ignore,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => SequenceStatus::Ignore,
|
2025-05-15 03:26:35 +00:00
|
|
|
IambAction::Space(..) => SequenceStatus::Ignore,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => SequenceStatus::Ignore,
|
2023-10-07 18:24:25 -07:00
|
|
|
IambAction::OpenLink(..) => 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,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-27 21:21:05 -08:00
|
|
|
fn is_switchable(&self, _: &EditContext) -> bool {
|
2022-12-29 18:00:59 -08:00
|
|
|
match self {
|
2024-08-20 19:33:46 -07:00
|
|
|
IambAction::ClearUnreads => false,
|
2023-03-04 12:23:17 -08:00
|
|
|
IambAction::Homeserver(..) => false,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Message(..) => false,
|
2025-05-15 03:26:35 +00:00
|
|
|
IambAction::Space(..) => false,
|
2023-01-04 12:51:33 -08:00
|
|
|
IambAction::Room(..) => false,
|
2024-03-28 20:58:34 -07:00
|
|
|
IambAction::Keys(..) => false,
|
2023-01-10 19:59:30 -08:00
|
|
|
IambAction::Send(..) => false,
|
2023-10-07 18:24:25 -07:00
|
|
|
IambAction::OpenLink(..) => 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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
impl From<SpaceAction> for ProgramAction {
|
|
|
|
fn from(act: SpaceAction) -> 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for program actions.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub type ProgramAction = Action<IambInfo>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for program context.
|
2024-02-27 21:21:05 -08:00
|
|
|
pub type ProgramContext = EditContext;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for program keybindings.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub type Keybindings = VimMachine<TerminalKey, IambInfo>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for a program command.
|
2024-02-27 21:21:05 -08:00
|
|
|
pub type ProgramCommand = VimCommand<IambInfo>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for mapped program commands.
|
2024-02-27 21:21:05 -08:00
|
|
|
pub type ProgramCommands = VimCommandMachine<IambInfo>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for program store.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub type ProgramStore = Store<IambInfo>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for shared program store.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub type AsyncProgramStore = Arc<AsyncMutex<ProgramStore>>;
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Alias for an action result.
|
2022-12-29 18:00:59 -08:00
|
|
|
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-10-06 22:35:27 -07:00
|
|
|
/// Errors encountered during application use.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum IambError {
|
2024-08-18 00:33:45 -07:00
|
|
|
/// An invalid history visibility was specified.
|
|
|
|
#[error("Invalid history visibility setting: {0}")]
|
|
|
|
InvalidHistoryVisibility(String),
|
|
|
|
|
2024-08-17 23:43:19 +02:00
|
|
|
/// An invalid notification level was specified.
|
|
|
|
#[error("Invalid notification level: {0}")]
|
|
|
|
InvalidNotificationLevel(String),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An invalid user identifier was specified.
|
2023-01-11 17:54:49 -08:00
|
|
|
#[error("Invalid user identifier: {0}")]
|
2022-12-29 18:00:59 -08:00
|
|
|
InvalidUserId(String),
|
|
|
|
|
2024-08-01 08:51:01 +02:00
|
|
|
/// An invalid user identifier was specified.
|
|
|
|
#[error("Invalid room alias: {0}")]
|
|
|
|
InvalidRoomAlias(String),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An invalid verification identifier was specified.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Invalid verification user/device pair: {0}")]
|
|
|
|
InvalidVerificationId(String),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure related to the cryptographic store.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Cryptographic storage error: {0}")]
|
|
|
|
CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError),
|
|
|
|
|
2024-03-28 20:58:34 -07:00
|
|
|
#[error("Failed to import room keys: {0}")]
|
|
|
|
FailedKeyImport(#[from] matrix_sdk::encryption::RoomKeyImportError),
|
|
|
|
|
2024-03-02 15:00:29 -08:00
|
|
|
/// A failure related to the cryptographic store.
|
|
|
|
#[error("Cannot export keys from sled: {0}")]
|
|
|
|
UpgradeSled(#[from] crate::sled_export::SledMigrationError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An HTTP error.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("HTTP client error: {0}")]
|
|
|
|
Http(#[from] matrix_sdk::HttpError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure from the Matrix client.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Matrix client error: {0}")]
|
|
|
|
Matrix(#[from] matrix_sdk::Error),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure in the sled storage.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Matrix client storage error: {0}")]
|
|
|
|
Store(#[from] matrix_sdk::StoreError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure during serialization or deserialization.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Serialization/deserialization error: {0}")]
|
|
|
|
Serde(#[from] serde_json::Error),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having a configured download directory.
|
2023-07-07 20:35:01 -07:00
|
|
|
#[error("No download directory configured")]
|
|
|
|
NoDownloadDir,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having a message with an attachment selected.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("Selected message does not have any attachments")]
|
|
|
|
NoAttachment,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having a message selected.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("No message currently selected")]
|
|
|
|
NoSelectedMessage,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having a room or space selected.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("Current window is not a room or space")]
|
|
|
|
NoSelectedRoomOrSpace,
|
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
/// A failure due to not having a room or space item selected in a list.
|
|
|
|
#[error("No room or space currently selected in list")]
|
|
|
|
NoSelectedRoomOrSpaceItem,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having a room selected.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("Current window is not a room")]
|
|
|
|
NoSelectedRoom,
|
|
|
|
|
2025-05-15 03:26:35 +00:00
|
|
|
/// A failure due to not having a space selected.
|
|
|
|
#[error("Current window is not a space")]
|
|
|
|
NoSelectedSpace,
|
|
|
|
|
|
|
|
/// A failure due to not having sufficient permission to perform an action in a room.
|
|
|
|
#[error("You do not have the permission to do that")]
|
|
|
|
InsufficientPermission,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not having an outstanding room invitation.
|
2023-01-11 17:54:49 -08:00
|
|
|
#[error("You do not have a current invitation to this room")]
|
|
|
|
NotInvited,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure due to not being a joined room member.
|
2023-01-10 19:59:30 -08:00
|
|
|
#[error("You need to join the room before you can do that")]
|
|
|
|
NotJoined,
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// An unknown room was specified.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Unknown room identifier: {0}")]
|
|
|
|
UnknownRoom(OwnedRoomId),
|
|
|
|
|
2024-08-01 08:51:01 +02:00
|
|
|
/// An invalid room alias id was specified.
|
|
|
|
#[error("Invalid room alias id: {0}")]
|
|
|
|
InvalidRoomAliasId(#[from] matrix_sdk::ruma::IdParseError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure occurred during verification.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[error("Verification request error: {0}")]
|
|
|
|
VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError),
|
2023-05-06 20:56:02 +01:00
|
|
|
|
2024-08-17 23:43:19 +02:00
|
|
|
#[error("Notification setting error: {0}")]
|
|
|
|
NotificationSettingError(#[from] matrix_sdk::NotificationSettingsError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure related to images.
|
2023-05-06 20:56:02 +01:00
|
|
|
#[error("Image error: {0}")]
|
|
|
|
Image(#[from] image::ImageError),
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// A failure to access the system's clipboard.
|
2023-05-06 20:56:02 +01:00
|
|
|
#[error("Could not use system clipboard data")]
|
|
|
|
Clipboard,
|
2023-11-16 08:36:22 -08:00
|
|
|
|
|
|
|
/// An failure during disk/network/ipc/etc. I/O.
|
|
|
|
#[error("Input/Output error: {0}")]
|
|
|
|
IOError(#[from] std::io::Error),
|
|
|
|
|
|
|
|
/// A failure while trying to show an image preview.
|
|
|
|
#[error("Preview error: {0}")]
|
|
|
|
Preview(String),
|
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 {}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Status for tracking how much room scrollback we've fetched.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Default)]
|
|
|
|
pub enum RoomFetchStatus {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Room history has been completely fetched.
|
2022-12-29 18:00:59 -08:00
|
|
|
Done,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// More room history can be fetched.
|
2022-12-29 18:00:59 -08:00
|
|
|
HaveMore(String),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// We have not yet started fetching history for this room.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[default]
|
|
|
|
NotStarted,
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Indicates where an [EventId] lives in the [ChatStore].
|
2023-02-09 17:53:33 -08:00
|
|
|
pub enum EventLocation {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The [EventId] belongs to a message.
|
2024-03-09 00:47:05 -08:00
|
|
|
///
|
|
|
|
/// If the first argument is [None], then it's part of the main scrollback. When [Some],
|
|
|
|
/// it specifies which thread it's in reply to.
|
|
|
|
Message(Option<OwnedEventId>, MessageKey),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The [EventId] belongs to a reaction to the given event.
|
2023-02-09 17:53:33 -08:00
|
|
|
Reaction(OwnedEventId),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl EventLocation {
|
|
|
|
fn to_message_key(&self) -> Option<&MessageKey> {
|
2024-03-09 00:47:05 -08:00
|
|
|
if let EventLocation::Message(_, key) = self {
|
2023-02-09 17:53:33 -08:00
|
|
|
Some(key)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-28 09:03:28 -08:00
|
|
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
|
|
pub struct UnreadInfo {
|
|
|
|
pub(crate) unread: bool,
|
|
|
|
pub(crate) latest: Option<MessageTimeStamp>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl UnreadInfo {
|
|
|
|
pub fn is_unread(&self) -> bool {
|
|
|
|
self.unread
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn latest(&self) -> Option<&MessageTimeStamp> {
|
|
|
|
self.latest.as_ref()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Information about room's the user's joined.
|
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.
|
2024-03-23 19:20:06 -07:00
|
|
|
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-10-15 18:12:39 -07:00
|
|
|
pub event_receipts: HashMap<OwnedEventId, HashSet<OwnedUserId>>,
|
|
|
|
|
|
|
|
/// A map of the most recent read marker for each user.
|
|
|
|
///
|
2025-05-15 01:22:27 +00:00
|
|
|
/// Every receipt in this map should also have an entry in [`event_receipts`](`Self::event_receipts`),
|
2023-10-15 18:12:39 -07:00
|
|
|
/// however not every user has an entry. If a user's most recent receipt is
|
|
|
|
/// older than the oldest loaded event, that user will not be included.
|
|
|
|
pub user_receipts: HashMap<OwnedUserId, 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>,
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
/// A map of message identifiers to thread replies.
|
2024-03-23 19:20:06 -07:00
|
|
|
threads: HashMap<OwnedEventId, Messages>,
|
2024-03-09 00:47:05 -08:00
|
|
|
|
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>,
|
2024-03-22 00:46:46 +00:00
|
|
|
|
|
|
|
/// The last time the room was rendered, used to detect if it is currently open.
|
|
|
|
pub draw_last: Option<Instant>,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl RoomInfo {
|
2024-03-23 19:20:06 -07:00
|
|
|
pub fn get_thread(&self, root: Option<&EventId>) -> Option<&Messages> {
|
|
|
|
if let Some(thread_root) = root {
|
|
|
|
self.threads.get(thread_root)
|
|
|
|
} else {
|
|
|
|
Some(&self.messages)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_thread_mut(&mut self, root: Option<OwnedEventId>) -> &mut Messages {
|
|
|
|
if let Some(thread_root) = root {
|
|
|
|
self.threads.entry(thread_root).or_default()
|
|
|
|
} else {
|
|
|
|
&mut self.messages
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Get the event for the last message in a thread (or the thread root if there are no
|
|
|
|
/// in-thread replies yet).
|
|
|
|
///
|
|
|
|
/// This returns `None` if the event identifier isn't in the room.
|
|
|
|
pub fn get_thread_last<'a>(
|
|
|
|
&'a self,
|
|
|
|
thread_root: &OwnedEventId,
|
|
|
|
) -> Option<&'a OriginalRoomMessageEvent> {
|
|
|
|
let last = self.threads.get(thread_root).and_then(|t| Some(t.last_key_value()?.1));
|
|
|
|
|
|
|
|
let msg = if let Some(last) = last {
|
|
|
|
&last.event
|
|
|
|
} else if let EventLocation::Message(_, key) = self.keys.get(thread_root)? {
|
|
|
|
let msg = self.messages.get(key)?;
|
|
|
|
&msg.event
|
|
|
|
} else {
|
|
|
|
return None;
|
|
|
|
};
|
|
|
|
|
|
|
|
if let MessageEvent::Original(ev) = &msg {
|
|
|
|
Some(ev)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Get the reactions and their counts for a message.
|
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();
|
|
|
|
|
2024-04-02 15:40:25 +00:00
|
|
|
let mut seen_user_reactions = BTreeSet::new();
|
|
|
|
|
|
|
|
for (key, user) in reacts.values() {
|
|
|
|
if !seen_user_reactions.contains(&(key, user)) {
|
|
|
|
seen_user_reactions.insert((key, user));
|
|
|
|
let count = counts.entry(key.as_str()).or_default();
|
|
|
|
*count += 1;
|
|
|
|
}
|
2023-02-09 17:53:33 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
let mut reactions = counts.into_iter().collect::<Vec<_>>();
|
|
|
|
reactions.sort();
|
|
|
|
|
|
|
|
reactions
|
|
|
|
} else {
|
|
|
|
vec![]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Map an event identifier to its [MessageKey].
|
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-10-06 22:35:27 -07:00
|
|
|
/// Get an event for an identifier.
|
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
|
|
|
}
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
/// Get an event for an identifier as mutable.
|
|
|
|
pub fn get_event_mut(&mut self, event_id: &EventId) -> Option<&mut Message> {
|
|
|
|
self.messages.get_mut(self.keys.get(event_id)?.to_message_key()?)
|
|
|
|
}
|
|
|
|
|
2024-03-23 19:20:06 -07:00
|
|
|
pub fn redact(&mut self, ev: OriginalSyncRoomRedactionEvent, room_version: &RoomVersionId) {
|
|
|
|
let Some(redacts) = &ev.redacts else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
match self.keys.get(redacts) {
|
|
|
|
None => return,
|
|
|
|
Some(EventLocation::Message(None, key)) => {
|
|
|
|
if let Some(msg) = self.messages.get_mut(key) {
|
|
|
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
|
|
|
msg.redact(ev, room_version);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Some(EventLocation::Message(Some(root), key)) => {
|
|
|
|
if let Some(thread) = self.threads.get_mut(root) {
|
|
|
|
if let Some(msg) = thread.get_mut(key) {
|
|
|
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
|
|
|
msg.redact(ev, room_version);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Some(EventLocation::Reaction(event_id)) => {
|
|
|
|
if let Some(reactions) = self.reactions.get_mut(event_id) {
|
|
|
|
reactions.remove(redacts);
|
|
|
|
}
|
|
|
|
|
|
|
|
self.keys.remove(redacts);
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Insert a reaction to a message.
|
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
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Insert an edit.
|
2024-03-02 15:00:29 -08:00
|
|
|
pub fn insert_edit(&mut self, msg: Replacement<RoomMessageEventContentWithoutRelation>) {
|
2023-01-19 16:05:02 -08:00
|
|
|
let event_id = msg.event_id;
|
2024-03-02 15:00:29 -08:00
|
|
|
let new_msgtype = msg.new_content;
|
2023-01-19 16:05:02 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let Some(EventLocation::Message(thread, key)) = self.keys.get(&event_id) else {
|
2023-01-19 16:05:02 -08:00
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let source = if let Some(thread) = thread {
|
|
|
|
self.threads.entry(thread.clone()).or_default()
|
2023-01-19 16:05:02 -08:00
|
|
|
} else {
|
2024-03-09 00:47:05 -08:00
|
|
|
&mut self.messages
|
|
|
|
};
|
|
|
|
|
|
|
|
let Some(msg) = source.get_mut(key) else {
|
2023-01-19 16:05:02 -08:00
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
match &mut msg.event {
|
|
|
|
MessageEvent::Original(orig) => {
|
2024-03-02 15:00:29 -08:00
|
|
|
orig.content.apply_replacement(new_msgtype);
|
2023-01-19 16:05:02 -08:00
|
|
|
},
|
2023-01-26 15:40:16 -08:00
|
|
|
MessageEvent::Local(_, content) => {
|
2024-03-02 15:00:29 -08:00
|
|
|
content.apply_replacement(new_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
|
|
|
}
|
|
|
|
|
2024-02-28 09:03:28 -08:00
|
|
|
/// Indicates whether this room has unread messages.
|
|
|
|
pub fn unreads(&self, settings: &ApplicationSettings) -> UnreadInfo {
|
|
|
|
let last_message = self.messages.last_key_value();
|
|
|
|
let last_receipt = self.get_receipt(&settings.profile.user_id);
|
|
|
|
|
|
|
|
match (last_message, last_receipt) {
|
|
|
|
(Some(((ts, recent), _)), Some(last_read)) => {
|
|
|
|
UnreadInfo { unread: last_read != recent, latest: Some(*ts) }
|
|
|
|
},
|
|
|
|
(Some(((ts, _), _)), None) => UnreadInfo { unread: false, latest: Some(*ts) },
|
|
|
|
(None, _) => UnreadInfo::default(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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());
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
self.keys.insert(event_id, EventLocation::Message(None, key.clone()));
|
2023-03-13 15:18:53 -07:00
|
|
|
self.messages.insert(key, msg.into());
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Insert a new message.
|
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());
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
let loc = EventLocation::Message(None, key.clone());
|
|
|
|
self.keys.insert(event_id, loc);
|
|
|
|
self.messages.insert_message(key, msg);
|
|
|
|
}
|
2023-01-19 16:05:02 -08:00
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
fn insert_thread(&mut self, msg: RoomMessageEvent, thread_root: OwnedEventId) {
|
|
|
|
let event_id = msg.event_id().to_owned();
|
|
|
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
|
|
|
|
|
|
|
let replies = self.threads.entry(thread_root.clone()).or_default();
|
|
|
|
let loc = EventLocation::Message(Some(thread_root), key.clone());
|
|
|
|
self.keys.insert(event_id, loc);
|
|
|
|
replies.insert_message(key, msg);
|
2023-01-19 16:05:02 -08:00
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Insert a new message event.
|
2023-01-19 16:05:02 -08:00
|
|
|
pub fn insert(&mut self, msg: RoomMessageEvent) {
|
|
|
|
match msg {
|
|
|
|
RoomMessageEvent::Original(OriginalRoomMessageEvent {
|
2024-03-09 00:47:05 -08:00
|
|
|
content: RoomMessageEventContent { relates_to: Some(ref relates_to), .. },
|
2023-01-19 16:05:02 -08:00
|
|
|
..
|
2024-03-09 00:47:05 -08:00
|
|
|
}) => {
|
|
|
|
match relates_to {
|
|
|
|
Relation::Replacement(repl) => self.insert_edit(repl.clone()),
|
|
|
|
Relation::Thread(Thread { event_id, .. }) => {
|
|
|
|
let event_id = event_id.clone();
|
|
|
|
self.insert_thread(msg, event_id);
|
|
|
|
},
|
|
|
|
Relation::Reply { .. } => self.insert_message(msg),
|
|
|
|
_ => self.insert_message(msg),
|
|
|
|
}
|
|
|
|
},
|
2023-01-19 16:05:02 -08:00
|
|
|
_ => self.insert_message(msg),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
/// Insert a new message event, and spawn a task for image-preview if it has an image
|
|
|
|
/// attachment.
|
|
|
|
pub fn insert_with_preview(
|
|
|
|
&mut self,
|
|
|
|
room_id: OwnedRoomId,
|
|
|
|
store: AsyncProgramStore,
|
|
|
|
picker: Option<Picker>,
|
|
|
|
ev: RoomMessageEvent,
|
|
|
|
settings: &mut ApplicationSettings,
|
|
|
|
media: matrix_sdk::Media,
|
|
|
|
) {
|
|
|
|
let source = picker.and_then(|_| source_from_event(&ev));
|
|
|
|
self.insert(ev);
|
|
|
|
|
|
|
|
if let Some((event_id, source)) = source {
|
|
|
|
if let (Some(msg), Some(image_preview)) =
|
|
|
|
(self.get_event_mut(&event_id), &settings.tunables.image_preview)
|
|
|
|
{
|
|
|
|
msg.image_preview = ImageStatus::Downloading(image_preview.size.clone());
|
|
|
|
spawn_insert_preview(
|
|
|
|
store,
|
|
|
|
room_id,
|
|
|
|
event_id,
|
|
|
|
source,
|
|
|
|
media,
|
|
|
|
settings.dirs.image_previews.clone(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Indicates whether we've recently fetched scrollback for this room.
|
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
|
|
|
|
2023-10-15 18:12:39 -07:00
|
|
|
fn clear_receipt(&mut self, user_id: &OwnedUserId) -> Option<()> {
|
|
|
|
let old_event_id = self.user_receipts.get(user_id)?;
|
|
|
|
let old_receipts = self.event_receipts.get_mut(old_event_id)?;
|
|
|
|
old_receipts.remove(user_id);
|
|
|
|
|
|
|
|
if old_receipts.is_empty() {
|
|
|
|
self.event_receipts.remove(old_event_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn set_receipt(&mut self, user_id: OwnedUserId, event_id: OwnedEventId) {
|
|
|
|
self.clear_receipt(&user_id);
|
|
|
|
self.event_receipts
|
|
|
|
.entry(event_id.clone())
|
|
|
|
.or_default()
|
|
|
|
.insert(user_id.clone());
|
|
|
|
self.user_receipts.insert(user_id, event_id);
|
|
|
|
}
|
|
|
|
|
2024-08-20 19:33:46 -07:00
|
|
|
pub fn fully_read(&mut self, user_id: OwnedUserId) {
|
|
|
|
let Some(((_, event_id), _)) = self.messages.last_key_value() else {
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
|
|
|
|
self.set_receipt(user_id, event_id.clone());
|
|
|
|
}
|
|
|
|
|
2024-02-28 09:03:28 -08:00
|
|
|
pub fn get_receipt(&self, user_id: &UserId) -> Option<&OwnedEventId> {
|
|
|
|
self.user_receipts.get(user_id)
|
|
|
|
}
|
|
|
|
|
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-07-01 08:58:48 +01:00
|
|
|
fn get_typing_spans<'a>(&'a self, settings: &'a ApplicationSettings) -> Line<'a> {
|
2023-01-03 13:57:28 -08:00
|
|
|
let typers = self.get_typers();
|
|
|
|
let n = typers.len();
|
|
|
|
|
|
|
|
match n {
|
2023-07-01 08:58:48 +01:00
|
|
|
0 => Line::from(vec![]),
|
2023-01-03 13:57:28 -08:00
|
|
|
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
|
|
|
|
2023-07-01 08:58:48 +01:00
|
|
|
Line::from(vec![user, Span::from(" is typing...")])
|
2023-01-03 13:57:28 -08:00
|
|
|
},
|
|
|
|
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
|
|
|
|
2023-07-01 08:58:48 +01:00
|
|
|
Line::from(vec![
|
2023-01-03 13:57:28 -08:00
|
|
|
user1,
|
|
|
|
Span::raw(" and "),
|
|
|
|
user2,
|
|
|
|
Span::from(" are typing..."),
|
|
|
|
])
|
|
|
|
},
|
2023-07-01 08:58:48 +01:00
|
|
|
n if n < 5 => Line::from("Several people are typing..."),
|
|
|
|
_ => Line::from("Many people are typing..."),
|
2023-01-03 13:57:28 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Update typing information for this room.
|
2023-01-03 13:57:28 -08:00
|
|
|
pub fn set_typing(&mut self, user_ids: Vec<OwnedUserId>) {
|
|
|
|
self.users_typing = (Instant::now(), user_ids).into();
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Create a [Rect] that displays what users are typing.
|
2023-01-03 13:57:28 -08:00
|
|
|
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;
|
|
|
|
}
|
2024-04-02 15:21:24 +00:00
|
|
|
|
|
|
|
/// Checks if a given user has reacted with the given emoji on the given event
|
|
|
|
pub fn user_reactions_contains(
|
|
|
|
&mut self,
|
|
|
|
user_id: &UserId,
|
|
|
|
event_id: &EventId,
|
|
|
|
emoji: &str,
|
|
|
|
) -> bool {
|
|
|
|
if let Some(reactions) = self.reactions.get(event_id) {
|
|
|
|
reactions
|
|
|
|
.values()
|
|
|
|
.any(|(annotation, user)| annotation == emoji && user == user_id)
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Generate a [CompletionMap] for Emoji shortcodes.
|
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-11-16 08:36:22 -08:00
|
|
|
#[cfg(unix)]
|
|
|
|
fn picker_from_termios(protocol_type: Option<ProtocolType>) -> Option<Picker> {
|
|
|
|
let mut picker = match Picker::from_termios() {
|
|
|
|
Ok(picker) => picker,
|
|
|
|
Err(e) => {
|
|
|
|
tracing::error!("Failed to setup image previews: {e}");
|
|
|
|
return None;
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2024-09-17 07:12:16 +02:00
|
|
|
// `guess_protocol` also does tmux detection,
|
|
|
|
// run it always then overwrite the guessed protocol if needed
|
|
|
|
picker.guess_protocol();
|
|
|
|
|
2023-11-16 08:36:22 -08:00
|
|
|
if let Some(protocol_type) = protocol_type {
|
|
|
|
picker.protocol_type = protocol_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(picker)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Windows cannot guess the right protocol, and always needs type and font_size.
|
|
|
|
#[cfg(windows)]
|
|
|
|
fn picker_from_termios(_: Option<ProtocolType>) -> Option<Picker> {
|
|
|
|
tracing::error!("\"image_preview\" requires \"protocol\" with \"type\" and \"font_size\" options on Windows.");
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
|
|
|
fn picker_from_settings(settings: &ApplicationSettings) -> Option<Picker> {
|
|
|
|
let image_preview = settings.tunables.image_preview.as_ref()?;
|
|
|
|
let image_preview_protocol = image_preview.protocol.as_ref();
|
|
|
|
|
|
|
|
if let Some(&ImagePreviewProtocolValues {
|
|
|
|
r#type: Some(protocol_type),
|
|
|
|
font_size: Some(font_size),
|
|
|
|
}) = image_preview_protocol
|
|
|
|
{
|
|
|
|
// User forced type and font_size: use that.
|
|
|
|
let mut picker = Picker::new(font_size);
|
|
|
|
picker.protocol_type = protocol_type;
|
|
|
|
Some(picker)
|
|
|
|
} else {
|
|
|
|
// Guess, but use type if forced.
|
|
|
|
picker_from_termios(image_preview_protocol.and_then(|p| p.r#type))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Information gathered during server syncs about joined rooms.
|
2023-07-05 15:25:42 -07:00
|
|
|
#[derive(Default)]
|
|
|
|
pub struct SyncInfo {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Spaces that the user is a member of.
|
2023-10-20 19:32:33 -07:00
|
|
|
pub spaces: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Rooms that the user is a member of.
|
2023-07-05 15:25:42 -07:00
|
|
|
pub rooms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// DMs that the user is a member of.
|
2023-07-05 15:25:42 -07:00
|
|
|
pub dms: Vec<Arc<(MatrixRoom, Option<Tags>)>>,
|
|
|
|
}
|
|
|
|
|
2024-08-20 19:33:46 -07:00
|
|
|
impl SyncInfo {
|
|
|
|
pub fn rooms(&self) -> impl Iterator<Item = &RoomId> {
|
|
|
|
self.rooms.iter().map(|r| r.0.room_id())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn dms(&self) -> impl Iterator<Item = &RoomId> {
|
|
|
|
self.dms.iter().map(|r| r.0.room_id())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn chats(&self) -> impl Iterator<Item = &RoomId> {
|
|
|
|
self.rooms().chain(self.dms())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-18 20:55:04 -08:00
|
|
|
bitflags::bitflags! {
|
|
|
|
/// Load-needs
|
|
|
|
#[derive(Debug, Default, PartialEq)]
|
|
|
|
pub struct Need: u32 {
|
|
|
|
const EMPTY = 0b00000000;
|
|
|
|
const MESSAGES = 0b00000001;
|
|
|
|
const MEMBERS = 0b00000010;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Things that need loading for different rooms.
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct RoomNeeds {
|
|
|
|
needs: HashMap<OwnedRoomId, Need>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RoomNeeds {
|
|
|
|
/// Mark a room for needing something to be loaded.
|
|
|
|
pub fn insert(&mut self, room_id: OwnedRoomId, need: Need) {
|
|
|
|
self.needs.entry(room_id).or_default().insert(need);
|
|
|
|
}
|
2024-03-06 23:49:35 -08:00
|
|
|
|
|
|
|
pub fn rooms(&self) -> usize {
|
|
|
|
self.needs.len()
|
|
|
|
}
|
2023-12-18 20:55:04 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IntoIterator for RoomNeeds {
|
|
|
|
type Item = (OwnedRoomId, Need);
|
|
|
|
type IntoIter = IntoIter<OwnedRoomId, Need>;
|
|
|
|
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
|
|
self.needs.into_iter()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The main application state.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub struct ChatStore {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// `:`-commands
|
2023-03-01 18:46:33 -08:00
|
|
|
pub cmds: ProgramCommands,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Handle for communicating w/ the worker thread.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub worker: Requester,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Map of joined rooms.
|
2023-03-01 18:46:33 -08:00
|
|
|
pub rooms: CompletionMap<OwnedRoomId, RoomInfo>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Map of room names.
|
2023-03-01 18:46:33 -08:00
|
|
|
pub names: CompletionMap<String, OwnedRoomId>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Presence information for other users.
|
2023-03-01 18:46:33 -08:00
|
|
|
pub presences: CompletionMap<OwnedUserId, PresenceState>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// In-progress and completed verifications.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub verifications: HashMap<String, SasVerification>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Settings for the current profile loaded from config file.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub settings: ApplicationSettings,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Set of rooms that need more messages loaded in their scrollback.
|
2023-12-18 20:55:04 -08:00
|
|
|
pub need_load: RoomNeeds,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// [CompletionMap] of Emoji shortcodes.
|
2023-03-01 18:46:33 -08:00
|
|
|
pub emojis: CompletionMap<String, &'static Emoji>,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// Information gathered by the background thread.
|
2023-07-05 15:25:42 -07:00
|
|
|
pub sync_info: SyncInfo,
|
2023-11-16 08:36:22 -08:00
|
|
|
|
|
|
|
/// Image preview "protocol" picker.
|
|
|
|
pub picker: Option<Picker>,
|
2024-03-22 00:46:46 +00:00
|
|
|
|
|
|
|
/// Last draw time, used to match with RoomInfo's draw_last.
|
|
|
|
pub draw_curr: Option<Instant>,
|
2024-03-24 10:19:34 -07:00
|
|
|
|
|
|
|
/// Whether to ring the terminal bell on the next redraw.
|
|
|
|
pub ring_bell: bool,
|
2024-08-01 13:37:21 +10:00
|
|
|
|
|
|
|
/// Whether the application is currently focused
|
|
|
|
pub focused: bool,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ChatStore {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Create a new [ChatStore].
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn new(worker: Requester, settings: ApplicationSettings) -> Self {
|
2023-11-16 08:36:22 -08:00
|
|
|
let picker = picker_from_settings(&settings);
|
|
|
|
|
2022-12-29 18:00:59 -08:00
|
|
|
ChatStore {
|
|
|
|
worker,
|
|
|
|
settings,
|
2023-11-16 08:36:22 -08:00
|
|
|
picker,
|
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(),
|
2024-03-22 00:46:46 +00:00
|
|
|
draw_curr: None,
|
2024-03-24 10:19:34 -07:00
|
|
|
ring_bell: false,
|
2024-08-01 13:37:21 +10:00
|
|
|
focused: true,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Get a joined room.
|
2024-03-02 15:00:29 -08:00
|
|
|
pub fn get_joined_room(&self, room_id: &RoomId) -> Option<MatrixRoom> {
|
2024-08-01 06:02:42 +03:00
|
|
|
let room = self.worker.client.get_room(room_id)?;
|
2024-03-02 15:00:29 -08:00
|
|
|
|
|
|
|
if room.state() == MatrixRoomState::Joined {
|
|
|
|
Some(room)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2023-01-25 17:54:16 -08:00
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Get the title for a room.
|
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-10-06 22:35:27 -07:00
|
|
|
/// Get the [RoomInfo] for a given room identifier.
|
2022-12-29 18:00:59 -08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Set the name for a room.
|
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
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Insert a new E2EE verification.
|
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 {}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Identified used to track window content.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
|
|
pub enum IambId {
|
2024-03-09 00:47:05 -08:00
|
|
|
/// A Matrix room, with an optional thread to show.
|
|
|
|
Room(OwnedRoomId, Option<OwnedEventId>),
|
2023-06-28 23:42:31 -07:00
|
|
|
|
2024-02-27 18:36:09 -08:00
|
|
|
/// The `:dms` 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,
|
2024-02-27 18:36:09 -08:00
|
|
|
|
|
|
|
/// The `:chats` window.
|
|
|
|
ChatList,
|
2024-08-20 19:33:46 -07:00
|
|
|
|
|
|
|
/// The `:unreads` window.
|
|
|
|
UnreadList,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
2023-06-28 23:42:31 -07:00
|
|
|
impl Display for IambId {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
match self {
|
2024-03-09 00:47:05 -08:00
|
|
|
IambId::Room(room_id, None) => {
|
2023-06-28 23:42:31 -07:00
|
|
|
write!(f, "iamb://room/{room_id}")
|
|
|
|
},
|
2024-03-09 00:47:05 -08:00
|
|
|
IambId::Room(room_id, Some(thread)) => {
|
|
|
|
write!(f, "iamb://room/{room_id}/threads/{thread}")
|
|
|
|
},
|
2023-06-28 23:42:31 -07:00
|
|
|
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"),
|
2024-02-27 18:36:09 -08:00
|
|
|
IambId::ChatList => f.write_str("iamb://chats"),
|
2024-08-20 19:33:46 -07:00
|
|
|
IambId::UnreadList => f.write_str("iamb://unreads"),
|
2023-06-28 23:42:31 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// [serde] visitor for deserializing [IambId].
|
2023-06-28 23:42:31 -07:00
|
|
|
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"));
|
|
|
|
};
|
|
|
|
|
2024-03-09 00:47:05 -08:00
|
|
|
match *path.collect::<Vec<_>>().as_slice() {
|
|
|
|
[room_id] => {
|
|
|
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
|
|
|
return Err(E::custom("Invalid room identifier"));
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(IambId::Room(room_id, None))
|
|
|
|
},
|
|
|
|
[room_id, "threads", thread_root] => {
|
|
|
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
|
|
|
return Err(E::custom("Invalid room identifier"));
|
|
|
|
};
|
|
|
|
|
|
|
|
let Ok(thread_root) = OwnedEventId::try_from(thread_root) else {
|
|
|
|
return Err(E::custom("Invalid thread root identifier"));
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(IambId::Room(room_id, Some(thread_root)))
|
|
|
|
},
|
|
|
|
_ => return Err(E::custom("Invalid members window URL")),
|
|
|
|
}
|
2023-06-28 23:42:31 -07:00
|
|
|
},
|
|
|
|
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)
|
|
|
|
},
|
2024-02-27 18:36:09 -08:00
|
|
|
Some("chats") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://chats takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::ChatList)
|
|
|
|
},
|
2024-08-20 19:33:46 -07:00
|
|
|
Some("unreads") => {
|
|
|
|
if url.path() != "" {
|
|
|
|
return Err(E::custom("iamb://unreads takes no path"));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(IambId::UnreadList)
|
|
|
|
},
|
2023-06-28 23:42:31 -07:00
|
|
|
Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))),
|
|
|
|
None => Err(E::custom("Invalid iamb window URL")),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Which part of the room window's UI is focused.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
|
|
pub enum RoomFocus {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The scrollback for a room window is focused.
|
2022-12-29 18:00:59 -08:00
|
|
|
Scrollback,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The message bar for a room window is focused.
|
2022-12-29 18:00:59 -08:00
|
|
|
MessageBar,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RoomFocus {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Whether this is [RoomFocus::Scrollback].
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn is_scrollback(&self) -> bool {
|
|
|
|
matches!(self, RoomFocus::Scrollback)
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Whether this is [RoomFocus::MessageBar].
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn is_msgbar(&self) -> bool {
|
|
|
|
matches!(self, RoomFocus::MessageBar)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Identifiers used to track where a mark was placed.
|
|
|
|
///
|
|
|
|
/// While this is the "buffer identifier" for the mark,
|
|
|
|
/// not all of these are necessarily actual buffers.
|
2022-12-29 18:00:59 -08:00
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
|
|
pub enum IambBufferId {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// The command bar buffer.
|
2023-03-01 18:46:33 -08:00
|
|
|
Command(CommandType),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The message buffer or a specific message in a room.
|
2024-03-09 00:47:05 -08:00
|
|
|
Room(OwnedRoomId, Option<OwnedEventId>, RoomFocus),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The `:dms` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
DirectList,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The `:members` window for a room.
|
2023-01-04 12:51:33 -08:00
|
|
|
MemberList(OwnedRoomId),
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The `:rooms` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
RoomList,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The `:spaces` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
SpaceList,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The `:verify` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
VerifyList,
|
2023-10-06 22:35:27 -07:00
|
|
|
|
|
|
|
/// The buffer for the `:rooms` window.
|
2022-12-29 18:00:59 -08:00
|
|
|
Welcome,
|
2024-02-27 18:36:09 -08:00
|
|
|
|
|
|
|
/// The `:chats` window.
|
|
|
|
ChatList,
|
2024-08-20 19:33:46 -07:00
|
|
|
|
|
|
|
/// The `:unreads` window.
|
|
|
|
UnreadList,
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IambBufferId {
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Get the identifier for the window that contains this buffer.
|
2022-12-29 18:00:59 -08:00
|
|
|
pub fn to_window(&self) -> Option<IambId> {
|
2024-03-09 00:47:05 -08:00
|
|
|
let id = match self {
|
|
|
|
IambBufferId::Command(_) => return None,
|
|
|
|
IambBufferId::Room(room, thread, _) => IambId::Room(room.clone(), thread.clone()),
|
|
|
|
IambBufferId::DirectList => IambId::DirectList,
|
|
|
|
IambBufferId::MemberList(room) => IambId::MemberList(room.clone()),
|
|
|
|
IambBufferId::RoomList => IambId::RoomList,
|
|
|
|
IambBufferId::SpaceList => IambId::SpaceList,
|
|
|
|
IambBufferId::VerifyList => IambId::VerifyList,
|
|
|
|
IambBufferId::Welcome => IambId::Welcome,
|
|
|
|
IambBufferId::ChatList => IambId::ChatList,
|
2024-08-20 19:33:46 -07:00
|
|
|
IambBufferId::UnreadList => IambId::UnreadList,
|
2024-03-09 00:47:05 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
Some(id)
|
2022-12-29 18:00:59 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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![],
|
2024-03-09 00:47:05 -08:00
|
|
|
IambBufferId::Room(_, _, RoomFocus::MessageBar) => complete_msgbar(text, cursor, store),
|
|
|
|
IambBufferId::Room(_, _, RoomFocus::Scrollback) => vec![],
|
2023-03-01 18:46:33 -08:00
|
|
|
|
|
|
|
IambBufferId::DirectList => vec![],
|
|
|
|
IambBufferId::MemberList(_) => vec![],
|
|
|
|
IambBufferId::RoomList => vec![],
|
|
|
|
IambBufferId::SpaceList => vec![],
|
|
|
|
IambBufferId::VerifyList => vec![],
|
|
|
|
IambBufferId::Welcome => vec![],
|
2024-02-27 18:36:09 -08:00
|
|
|
IambBufferId::ChatList => vec![],
|
2024-08-20 19:33:46 -07:00
|
|
|
IambBufferId::UnreadList => vec![],
|
2023-03-01 18:46:33 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn content_of_command(ct: CommandType) -> IambBufferId {
|
|
|
|
IambBufferId::Command(ct)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for user IDs.
|
2023-03-01 18:46:33 -08:00
|
|
|
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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion within the message bar.
|
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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for Matrix identifiers (usernames, room aliases, etc.)
|
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()
|
|
|
|
}
|
|
|
|
|
2023-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for Emoji shortcode names.
|
2023-03-01 18:46:33 -08:00
|
|
|
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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for command names.
|
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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for command arguments.
|
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![],
|
2024-03-28 20:58:34 -07:00
|
|
|
"download" | "keys" | "open" | "upload" => complete_path(text, cursor),
|
2023-03-01 18:46:33 -08:00
|
|
|
"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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for commands.
|
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-10-06 22:35:27 -07:00
|
|
|
/// Tab completion for the command bar.
|
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);
|
2024-02-27 21:21:05 -08:00
|
|
|
let slice = text.slice(..eo);
|
2023-05-01 21:14:19 -07:00
|
|
|
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::*;
|
2024-04-02 15:40:25 +00:00
|
|
|
use matrix_sdk::ruma::{
|
|
|
|
events::{reaction::ReactionEventContent, relation::Annotation, MessageLikeUnsigned},
|
|
|
|
owned_event_id,
|
|
|
|
owned_room_id,
|
|
|
|
owned_user_id,
|
|
|
|
MilliSecondsSinceUnixEpoch,
|
|
|
|
};
|
|
|
|
use pretty_assertions::assert_eq;
|
2024-02-27 21:21:05 -08:00
|
|
|
use ratatui::style::Color;
|
2023-01-03 13:57:28 -08:00
|
|
|
|
2024-04-02 15:40:25 +00:00
|
|
|
#[test]
|
|
|
|
fn multiple_identical_reactions() {
|
|
|
|
let mut info = RoomInfo::default();
|
|
|
|
|
|
|
|
let content = ReactionEventContent::new(Annotation::new(
|
|
|
|
owned_event_id!("$my_reaction"),
|
|
|
|
"🏠".to_owned(),
|
|
|
|
));
|
|
|
|
|
|
|
|
for i in 0..3 {
|
|
|
|
let event_id = format!("$house_{}", i);
|
|
|
|
info.insert_reaction(MessageLikeEvent::Original(
|
|
|
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
|
|
|
content: content.clone(),
|
|
|
|
event_id: OwnedEventId::from_str(&event_id).unwrap(),
|
|
|
|
sender: owned_user_id!("@foo:example.org"),
|
|
|
|
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
|
|
|
room_id: owned_room_id!("!foo:example.org"),
|
|
|
|
unsigned: MessageLikeUnsigned::new(),
|
|
|
|
},
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let content = ReactionEventContent::new(Annotation::new(
|
|
|
|
owned_event_id!("$my_reaction"),
|
|
|
|
"🙂".to_owned(),
|
|
|
|
));
|
|
|
|
|
|
|
|
for i in 0..2 {
|
|
|
|
let event_id = format!("$smile_{}", i);
|
|
|
|
info.insert_reaction(MessageLikeEvent::Original(
|
|
|
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
|
|
|
content: content.clone(),
|
|
|
|
event_id: OwnedEventId::from_str(&event_id).unwrap(),
|
|
|
|
sender: owned_user_id!("@foo:example.org"),
|
|
|
|
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
|
|
|
room_id: owned_room_id!("!foo:example.org"),
|
|
|
|
unsigned: MessageLikeUnsigned::new(),
|
|
|
|
},
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
for i in 2..4 {
|
|
|
|
let event_id = format!("$smile_{}", i);
|
|
|
|
info.insert_reaction(MessageLikeEvent::Original(
|
|
|
|
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
|
|
|
|
content: content.clone(),
|
|
|
|
event_id: OwnedEventId::from_str(&event_id).unwrap(),
|
|
|
|
sender: owned_user_id!("@bar:example.org"),
|
|
|
|
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
|
|
|
|
room_id: owned_room_id!("!foo:example.org"),
|
|
|
|
unsigned: MessageLikeUnsigned::new(),
|
|
|
|
},
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_eq!(info.get_reactions(&owned_event_id!("$my_reaction")), vec![
|
|
|
|
("🏠", 1),
|
|
|
|
("🙂", 2)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
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-07-01 08:58:48 +01:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Line::from(vec![]));
|
2023-01-03 13:57:28 -08:00
|
|
|
|
|
|
|
// Empty typing list.
|
|
|
|
info.set_typing(users0);
|
|
|
|
assert!(info.users_typing.is_some());
|
2023-07-01 08:58:48 +01:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Line::from(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-07-01 08:58:48 +01:00
|
|
|
Line::from(vec![
|
2023-01-03 13:57:28 -08:00
|
|
|
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-07-01 08:58:48 +01:00
|
|
|
Line::from(vec![
|
2023-01-03 13:57:28 -08:00
|
|
|
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-07-01 08:58:48 +01:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Line::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-07-01 08:58:48 +01:00
|
|
|
assert_eq!(info.get_typing_spans(&settings), Line::from("Many people are typing..."));
|
2023-01-06 16:56:28 -08:00
|
|
|
|
|
|
|
// 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),
|
2023-07-01 08:58:48 +01:00
|
|
|
Line::from(vec![
|
2023-01-06 16:56:28 -08:00
|
|
|
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-12-18 20:55:04 -08:00
|
|
|
#[test]
|
|
|
|
fn test_need_load() {
|
|
|
|
let room_id = TEST_ROOM1_ID.clone();
|
|
|
|
|
|
|
|
let mut need_load = RoomNeeds::default();
|
|
|
|
|
|
|
|
need_load.insert(room_id.clone(), Need::MESSAGES);
|
|
|
|
need_load.insert(room_id.clone(), Need::MEMBERS);
|
|
|
|
|
|
|
|
assert_eq!(need_load.into_iter().collect::<Vec<(OwnedRoomId, Need)>>(), vec![(
|
|
|
|
room_id,
|
|
|
|
Need::MESSAGES | Need::MEMBERS,
|
|
|
|
)],);
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|