Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2590b6bbb | ||
|
|
725ebb9fd6 | ||
|
|
ca395097e1 | ||
|
|
e98d58a8cc | ||
|
|
e6cdd02f22 | ||
|
|
0bc4ff07b0 | ||
|
|
14fe916d94 | ||
|
|
db35581d07 | ||
|
|
7c1c62897a | ||
|
|
61897ea6f2 | ||
|
|
6a0722795a | ||
|
|
f3bbc6ad9f | ||
|
|
2dd8c0fddf | ||
|
|
a786369b14 | ||
|
|
066f60ad32 |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -1308,7 +1308,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iamb"
|
name = "iamb"
|
||||||
version = "0.0.6"
|
version = "0.0.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1882,9 +1882,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "modalkit"
|
name = "modalkit"
|
||||||
version = "0.0.13"
|
version = "0.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "038bb42efcd659fb123708bf8e4ea12ca9023f07d44514f9358b9334ea7ba80f"
|
checksum = "5c48c7d7e6d764a09435b43a7e4d342ba2d2e026626ca773b16a5ba34b90b933"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anymap2",
|
"anymap2",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "iamb"
|
name = "iamb"
|
||||||
version = "0.0.6"
|
version = "0.0.7"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Ulyssa <git@ulyssa.dev>"]
|
authors = ["Ulyssa <git@ulyssa.dev>"]
|
||||||
repository = "https://github.com/ulyssa/iamb"
|
repository = "https://github.com/ulyssa/iamb"
|
||||||
@@ -39,7 +39,7 @@ unicode-width = "0.1.10"
|
|||||||
url = {version = "^2.2.2", features = ["serde"]}
|
url = {version = "^2.2.2", features = ["serde"]}
|
||||||
|
|
||||||
[dependencies.modalkit]
|
[dependencies.modalkit]
|
||||||
version = "0.0.13"
|
version = "0.0.14"
|
||||||
|
|
||||||
[dependencies.matrix-sdk]
|
[dependencies.matrix-sdk]
|
||||||
version = "0.6"
|
version = "0.6"
|
||||||
@@ -52,3 +52,7 @@ features = ["macros", "net", "rt-multi-thread", "sync", "time"]
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
incremental = false
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
|
[](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
|
||||||
[](https://crates.io/crates/iamb)
|
[](https://crates.io/crates/iamb)
|
||||||
|
[](https://matrix.to/#/#iamb:0x.badd.cafe)
|
||||||
[](https://crates.io/crates/iamb)
|
[](https://crates.io/crates/iamb)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
@@ -26,6 +27,22 @@ Install Rust and Cargo, and then run:
|
|||||||
cargo install --locked iamb
|
cargo install --locked iamb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### NetBSD
|
||||||
|
|
||||||
|
On NetBSD a package is available from the official repositories. To install it simply run:
|
||||||
|
|
||||||
|
```
|
||||||
|
pkgin install iamb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch Linux
|
||||||
|
|
||||||
|
On Arch Linux a package is available in the Arch User Repositories (AUR). To install it simply run with your favorite AUR helper:
|
||||||
|
|
||||||
|
```
|
||||||
|
paru iamb-git
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
You can create a basic configuration in `$CONFIG_DIR/iamb/config.json` that looks like:
|
You can create a basic configuration in `$CONFIG_DIR/iamb/config.json` that looks like:
|
||||||
|
|||||||
76
src/base.rs
76
src/base.rs
@@ -7,7 +7,6 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
use emojis::Emoji;
|
use emojis::Emoji;
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
encryption::verification::SasVerification,
|
encryption::verification::SasVerification,
|
||||||
@@ -15,6 +14,7 @@ use matrix_sdk::{
|
|||||||
ruma::{
|
ruma::{
|
||||||
events::{
|
events::{
|
||||||
reaction::ReactionEvent,
|
reaction::ReactionEvent,
|
||||||
|
room::encrypted::RoomEncryptedEvent,
|
||||||
room::message::{
|
room::message::{
|
||||||
OriginalRoomMessageEvent,
|
OriginalRoomMessageEvent,
|
||||||
Relation,
|
Relation,
|
||||||
@@ -23,7 +23,6 @@ use matrix_sdk::{
|
|||||||
RoomMessageEventContent,
|
RoomMessageEventContent,
|
||||||
},
|
},
|
||||||
tag::{TagName, Tags},
|
tag::{TagName, Tags},
|
||||||
AnyMessageLikeEvent,
|
|
||||||
MessageLikeEvent,
|
MessageLikeEvent,
|
||||||
},
|
},
|
||||||
presence::PresenceState,
|
presence::PresenceState,
|
||||||
@@ -412,6 +411,9 @@ pub struct RoomInfo {
|
|||||||
/// A map of message identifiers to a map of reaction events.
|
/// A map of message identifiers to a map of reaction events.
|
||||||
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
pub reactions: HashMap<OwnedEventId, MessageReactions>,
|
||||||
|
|
||||||
|
/// Whether the scrollback for this room is currently being fetched.
|
||||||
|
pub fetching: bool,
|
||||||
|
|
||||||
/// Where to continue fetching from when we continue loading scrollback history.
|
/// Where to continue fetching from when we continue loading scrollback history.
|
||||||
pub fetch_id: RoomFetchStatus,
|
pub fetch_id: RoomFetchStatus,
|
||||||
|
|
||||||
@@ -489,7 +491,9 @@ impl RoomInfo {
|
|||||||
MessageEvent::Local(_, content) => {
|
MessageEvent::Local(_, content) => {
|
||||||
*content = new_content;
|
*content = new_content;
|
||||||
},
|
},
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) |
|
||||||
|
MessageEvent::EncryptedOriginal(_) |
|
||||||
|
MessageEvent::EncryptedRedacted(_) => {
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -497,6 +501,15 @@ impl RoomInfo {
|
|||||||
msg.html = msg.event.html();
|
msg.html = msg.event.html();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts events that couldn't be decrypted into the scrollback.
|
||||||
|
pub fn insert_encrypted(&mut self, msg: RoomEncryptedEvent) {
|
||||||
|
let event_id = msg.event_id().to_owned();
|
||||||
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||||
|
|
||||||
|
self.keys.insert(event_id, EventLocation::Message(key.clone()));
|
||||||
|
self.messages.insert(key, msg.into());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_message(&mut self, msg: RoomMessageEvent) {
|
pub fn insert_message(&mut self, msg: RoomMessageEvent) {
|
||||||
let event_id = msg.event_id().to_owned();
|
let event_id = msg.event_id().to_owned();
|
||||||
let key = (msg.origin_server_ts().into(), event_id.clone());
|
let key = (msg.origin_server_ts().into(), event_id.clone());
|
||||||
@@ -520,7 +533,7 @@ impl RoomInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recently_fetched(&self) -> bool {
|
pub fn recently_fetched(&self) -> bool {
|
||||||
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,61 +681,6 @@ impl ChatStore {
|
|||||||
self.need_load.insert(room_id);
|
self.need_load.insert(room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_older(&mut self, limit: u32) {
|
|
||||||
let ChatStore { need_load, presences, rooms, worker, .. } = self;
|
|
||||||
|
|
||||||
for room_id in std::mem::take(need_load).into_iter() {
|
|
||||||
let info = rooms.get_or_default(room_id.clone());
|
|
||||||
|
|
||||||
if info.recently_fetched() {
|
|
||||||
need_load.insert(room_id);
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
info.fetch_last = Instant::now().into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let fetch_id = match &info.fetch_id {
|
|
||||||
RoomFetchStatus::Done => continue,
|
|
||||||
RoomFetchStatus::HaveMore(fetch_id) => Some(fetch_id.clone()),
|
|
||||||
RoomFetchStatus::NotStarted => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = worker.load_older(room_id.clone(), fetch_id, limit);
|
|
||||||
|
|
||||||
match res {
|
|
||||||
Ok((fetch_id, msgs)) => {
|
|
||||||
for msg in msgs.into_iter() {
|
|
||||||
let sender = msg.sender().to_owned();
|
|
||||||
let _ = presences.get_or_default(sender);
|
|
||||||
|
|
||||||
match msg {
|
|
||||||
AnyMessageLikeEvent::RoomMessage(msg) => {
|
|
||||||
info.insert(msg);
|
|
||||||
},
|
|
||||||
AnyMessageLikeEvent::Reaction(ev) => {
|
|
||||||
info.insert_reaction(ev);
|
|
||||||
},
|
|
||||||
_ => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.fetch_id =
|
|
||||||
fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
warn!(
|
|
||||||
room_id = room_id.as_str(),
|
|
||||||
err = e.to_string(),
|
|
||||||
"Failed to load older messages"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait and try again.
|
|
||||||
need_load.insert(room_id);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo {
|
pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo {
|
||||||
self.rooms.get_or_default(room_id)
|
self.rooms.get_or_default(room_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use std::process;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use matrix_sdk::ruma::{OwnedUserId, UserId};
|
use matrix_sdk::ruma::{OwnedUserId, UserId};
|
||||||
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
||||||
|
use tracing::Level;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use modalkit::tui::{
|
use modalkit::tui::{
|
||||||
@@ -25,6 +26,8 @@ macro_rules! usage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_REQ_TIMEOUT: u64 = 120;
|
||||||
|
|
||||||
const COLORS: [Color; 13] = [
|
const COLORS: [Color; 13] = [
|
||||||
Color::Blue,
|
Color::Blue,
|
||||||
Color::Cyan,
|
Color::Cyan,
|
||||||
@@ -106,6 +109,47 @@ pub enum ConfigError {
|
|||||||
Invalid(#[from] serde_json::Error),
|
Invalid(#[from] serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct LogLevel(pub Level);
|
||||||
|
pub struct LogLevelVisitor;
|
||||||
|
|
||||||
|
impl From<LogLevel> for Level {
|
||||||
|
fn from(level: LogLevel) -> Level {
|
||||||
|
level.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for LogLevelVisitor {
|
||||||
|
type Value = LogLevel;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a valid log level (e.g. \"warn\" or \"debug\")")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: SerdeError,
|
||||||
|
{
|
||||||
|
match value {
|
||||||
|
"info" => Ok(LogLevel(Level::INFO)),
|
||||||
|
"debug" => Ok(LogLevel(Level::DEBUG)),
|
||||||
|
"warn" => Ok(LogLevel(Level::WARN)),
|
||||||
|
"error" => Ok(LogLevel(Level::ERROR)),
|
||||||
|
"trace" => Ok(LogLevel(Level::TRACE)),
|
||||||
|
_ => Err(E::custom("Could not parse log level")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for LogLevel {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(LogLevelVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct UserColor(pub Color);
|
pub struct UserColor(pub Color);
|
||||||
pub struct UserColorVisitor;
|
pub struct UserColorVisitor;
|
||||||
@@ -178,10 +222,12 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TunableValues {
|
pub struct TunableValues {
|
||||||
|
pub log_level: Level,
|
||||||
pub reaction_display: bool,
|
pub reaction_display: bool,
|
||||||
pub reaction_shortcode_display: bool,
|
pub reaction_shortcode_display: bool,
|
||||||
pub read_receipt_send: bool,
|
pub read_receipt_send: bool,
|
||||||
pub read_receipt_display: bool,
|
pub read_receipt_display: bool,
|
||||||
|
pub request_timeout: u64,
|
||||||
pub typing_notice_send: bool,
|
pub typing_notice_send: bool,
|
||||||
pub typing_notice_display: bool,
|
pub typing_notice_display: bool,
|
||||||
pub users: UserOverrides,
|
pub users: UserOverrides,
|
||||||
@@ -190,10 +236,12 @@ pub struct TunableValues {
|
|||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
pub struct Tunables {
|
pub struct Tunables {
|
||||||
|
pub log_level: Option<LogLevel>,
|
||||||
pub reaction_display: Option<bool>,
|
pub reaction_display: Option<bool>,
|
||||||
pub reaction_shortcode_display: Option<bool>,
|
pub reaction_shortcode_display: Option<bool>,
|
||||||
pub read_receipt_send: Option<bool>,
|
pub read_receipt_send: Option<bool>,
|
||||||
pub read_receipt_display: Option<bool>,
|
pub read_receipt_display: Option<bool>,
|
||||||
|
pub request_timeout: Option<u64>,
|
||||||
pub typing_notice_send: Option<bool>,
|
pub typing_notice_send: Option<bool>,
|
||||||
pub typing_notice_display: Option<bool>,
|
pub typing_notice_display: Option<bool>,
|
||||||
pub users: Option<UserOverrides>,
|
pub users: Option<UserOverrides>,
|
||||||
@@ -203,12 +251,14 @@ pub struct Tunables {
|
|||||||
impl Tunables {
|
impl Tunables {
|
||||||
fn merge(self, other: Self) -> Self {
|
fn merge(self, other: Self) -> Self {
|
||||||
Tunables {
|
Tunables {
|
||||||
|
log_level: self.log_level.or(other.log_level),
|
||||||
reaction_display: self.reaction_display.or(other.reaction_display),
|
reaction_display: self.reaction_display.or(other.reaction_display),
|
||||||
reaction_shortcode_display: self
|
reaction_shortcode_display: self
|
||||||
.reaction_shortcode_display
|
.reaction_shortcode_display
|
||||||
.or(other.reaction_shortcode_display),
|
.or(other.reaction_shortcode_display),
|
||||||
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
||||||
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
read_receipt_display: self.read_receipt_display.or(other.read_receipt_display),
|
||||||
|
request_timeout: self.request_timeout.or(other.request_timeout),
|
||||||
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
typing_notice_send: self.typing_notice_send.or(other.typing_notice_send),
|
||||||
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||||
users: merge_users(self.users, other.users),
|
users: merge_users(self.users, other.users),
|
||||||
@@ -218,10 +268,12 @@ impl Tunables {
|
|||||||
|
|
||||||
fn values(self) -> TunableValues {
|
fn values(self) -> TunableValues {
|
||||||
TunableValues {
|
TunableValues {
|
||||||
|
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
||||||
reaction_display: self.reaction_display.unwrap_or(true),
|
reaction_display: self.reaction_display.unwrap_or(true),
|
||||||
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||||
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
read_receipt_send: self.read_receipt_send.unwrap_or(true),
|
||||||
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
read_receipt_display: self.read_receipt_display.unwrap_or(true),
|
||||||
|
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQ_TIMEOUT),
|
||||||
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
typing_notice_send: self.typing_notice_send.unwrap_or(true),
|
||||||
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
typing_notice_display: self.typing_notice_display.unwrap_or(true),
|
||||||
users: self.users.unwrap_or_default(),
|
users: self.users.unwrap_or_default(),
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -15,7 +15,6 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
use tracing::{self, Level};
|
|
||||||
use tracing_subscriber::FmtSubscriber;
|
use tracing_subscriber::FmtSubscriber;
|
||||||
|
|
||||||
use matrix_sdk::ruma::OwnedUserId;
|
use matrix_sdk::ruma::OwnedUserId;
|
||||||
@@ -101,14 +100,6 @@ use modalkit::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MIN_MSG_LOAD: u32 = 50;
|
|
||||||
|
|
||||||
fn msg_load_req(area: Rect) -> u32 {
|
|
||||||
let n = area.height as u32;
|
|
||||||
|
|
||||||
n.max(MIN_MSG_LOAD)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Application {
|
struct Application {
|
||||||
store: AsyncProgramStore,
|
store: AsyncProgramStore,
|
||||||
worker: Requester,
|
worker: Requester,
|
||||||
@@ -190,8 +181,6 @@ impl Application {
|
|||||||
}
|
}
|
||||||
f.set_cursor(cx, cy);
|
f.set_cursor(cx, cy);
|
||||||
}
|
}
|
||||||
|
|
||||||
store.application.load_older(msg_load_req(area));
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -544,7 +533,7 @@ fn main() -> IambResult<()> {
|
|||||||
|
|
||||||
let subscriber = FmtSubscriber::builder()
|
let subscriber = FmtSubscriber::builder()
|
||||||
.with_writer(appender)
|
.with_writer(appender)
|
||||||
.with_max_level(Level::INFO)
|
.with_max_level(settings.tunables.log_level)
|
||||||
.finish();
|
.finish();
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||||
|
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ pub fn parse_matrix_html(s: &str) -> StyleTree {
|
|||||||
let dom = parse_fragment(
|
let dom = parse_fragment(
|
||||||
RcDom::default(),
|
RcDom::default(),
|
||||||
ParseOpts::default(),
|
ParseOpts::default(),
|
||||||
QualName::new(None, ns!(), local_name!("div")),
|
QualName::new(None, ns!(html), local_name!("body")),
|
||||||
vec![],
|
vec![],
|
||||||
)
|
)
|
||||||
.one(StrTendril::from(s));
|
.one(StrTendril::from(s));
|
||||||
@@ -1147,4 +1147,15 @@ pub mod tests {
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_self_closing() {
|
||||||
|
let s = "Hello<br>World<br>Goodbye";
|
||||||
|
let tree = parse_matrix_html(s);
|
||||||
|
let text = tree.to_text(7, Style::default(), true);
|
||||||
|
assert_eq!(text.lines.len(), 3);
|
||||||
|
assert_eq!(text.lines[0], Spans(vec![Span::raw("Hello"), Span::raw(" "),]));
|
||||||
|
assert_eq!(text.lines[1], Spans(vec![Span::raw("World"), Span::raw(" "),]));
|
||||||
|
assert_eq!(text.lines[2], Spans(vec![Span::raw("Goodbye")]),);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ use unicode_width::UnicodeWidthStr;
|
|||||||
use matrix_sdk::ruma::{
|
use matrix_sdk::ruma::{
|
||||||
events::{
|
events::{
|
||||||
room::{
|
room::{
|
||||||
|
encrypted::{
|
||||||
|
OriginalRoomEncryptedEvent,
|
||||||
|
RedactedRoomEncryptedEvent,
|
||||||
|
RoomEncryptedEvent,
|
||||||
|
},
|
||||||
message::{
|
message::{
|
||||||
FormattedBody,
|
FormattedBody,
|
||||||
MessageFormat,
|
MessageFormat,
|
||||||
@@ -26,6 +31,7 @@ use matrix_sdk::ruma::{
|
|||||||
},
|
},
|
||||||
AnyMessageLikeEvent,
|
AnyMessageLikeEvent,
|
||||||
Redact,
|
Redact,
|
||||||
|
RedactedUnsigned,
|
||||||
},
|
},
|
||||||
EventId,
|
EventId,
|
||||||
MilliSecondsSinceUnixEpoch,
|
MilliSecondsSinceUnixEpoch,
|
||||||
@@ -318,6 +324,8 @@ impl PartialOrd for MessageCursor {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum MessageEvent {
|
pub enum MessageEvent {
|
||||||
|
EncryptedOriginal(Box<OriginalRoomEncryptedEvent>),
|
||||||
|
EncryptedRedacted(Box<RedactedRoomEncryptedEvent>),
|
||||||
Original(Box<OriginalRoomMessageEvent>),
|
Original(Box<OriginalRoomMessageEvent>),
|
||||||
Redacted(Box<RedactedRoomMessageEvent>),
|
Redacted(Box<RedactedRoomMessageEvent>),
|
||||||
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
||||||
@@ -326,35 +334,45 @@ pub enum MessageEvent {
|
|||||||
impl MessageEvent {
|
impl MessageEvent {
|
||||||
pub fn event_id(&self) -> &EventId {
|
pub fn event_id(&self) -> &EventId {
|
||||||
match self {
|
match self {
|
||||||
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.as_ref(),
|
||||||
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Redacted(ev) => ev.event_id.as_ref(),
|
MessageEvent::Redacted(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> Option<&RoomMessageEventContent> {
|
||||||
|
match self {
|
||||||
|
MessageEvent::EncryptedOriginal(_) => None,
|
||||||
|
MessageEvent::Original(ev) => Some(&ev.content),
|
||||||
|
MessageEvent::EncryptedRedacted(_) => None,
|
||||||
|
MessageEvent::Redacted(_) => None,
|
||||||
|
MessageEvent::Local(_, content) => Some(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_emote(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.content(),
|
||||||
|
Some(RoomMessageEventContent { msgtype: MessageType::Emote(_), .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn body(&self) -> Cow<'_, str> {
|
pub fn body(&self) -> Cow<'_, str> {
|
||||||
match self {
|
match self {
|
||||||
|
MessageEvent::EncryptedOriginal(_) => "[Unable to decrypt message]".into(),
|
||||||
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
||||||
MessageEvent::Redacted(ev) => {
|
MessageEvent::EncryptedRedacted(ev) => body_cow_reason(&ev.unsigned),
|
||||||
let reason = ev
|
MessageEvent::Redacted(ev) => body_cow_reason(&ev.unsigned),
|
||||||
.unsigned
|
|
||||||
.redacted_because
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|e| e.as_original())
|
|
||||||
.and_then(|r| r.content.reason.as_ref());
|
|
||||||
|
|
||||||
if let Some(r) = reason {
|
|
||||||
Cow::Owned(format!("[Redacted: {r:?}]"))
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed("[Redacted]")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MessageEvent::Local(_, content) => body_cow_content(content),
|
MessageEvent::Local(_, content) => body_cow_content(content),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn html(&self) -> Option<StyleTree> {
|
pub fn html(&self) -> Option<StyleTree> {
|
||||||
let content = match self {
|
let content = match self {
|
||||||
|
MessageEvent::EncryptedOriginal(_) => return None,
|
||||||
|
MessageEvent::EncryptedRedacted(_) => return None,
|
||||||
MessageEvent::Original(ev) => &ev.content,
|
MessageEvent::Original(ev) => &ev.content,
|
||||||
MessageEvent::Redacted(_) => return None,
|
MessageEvent::Redacted(_) => return None,
|
||||||
MessageEvent::Local(_, content) => content,
|
MessageEvent::Local(_, content) => content,
|
||||||
@@ -371,8 +389,10 @@ impl MessageEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||||
match self {
|
match self {
|
||||||
|
MessageEvent::EncryptedOriginal(_) => return,
|
||||||
|
MessageEvent::EncryptedRedacted(_) => return,
|
||||||
MessageEvent::Redacted(_) => return,
|
MessageEvent::Redacted(_) => return,
|
||||||
MessageEvent::Local(_, _) => return,
|
MessageEvent::Local(_, _) => return,
|
||||||
MessageEvent::Original(ev) => {
|
MessageEvent::Original(ev) => {
|
||||||
@@ -411,6 +431,20 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
|
|||||||
Cow::Borrowed(s)
|
Cow::Borrowed(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn body_cow_reason(unsigned: &RedactedUnsigned) -> Cow<'_, str> {
|
||||||
|
let reason = unsigned
|
||||||
|
.redacted_because
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|e| e.as_original())
|
||||||
|
.and_then(|r| r.content.reason.as_ref());
|
||||||
|
|
||||||
|
if let Some(r) = reason {
|
||||||
|
Cow::Owned(format!("[Redacted: {r:?}]"))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed("[Redacted]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum MessageColumns {
|
enum MessageColumns {
|
||||||
/// Four columns: sender, message, timestamp, read receipts.
|
/// Four columns: sender, message, timestamp, read receipts.
|
||||||
Four,
|
Four,
|
||||||
@@ -548,6 +582,8 @@ impl Message {
|
|||||||
|
|
||||||
pub fn reply_to(&self) -> Option<OwnedEventId> {
|
pub fn reply_to(&self) -> Option<OwnedEventId> {
|
||||||
let content = match &self.event {
|
let content = match &self.event {
|
||||||
|
MessageEvent::EncryptedOriginal(_) => return None,
|
||||||
|
MessageEvent::EncryptedRedacted(_) => return None,
|
||||||
MessageEvent::Local(_, content) => content,
|
MessageEvent::Local(_, content) => content,
|
||||||
MessageEvent::Original(ev) => &ev.content,
|
MessageEvent::Original(ev) => &ev.content,
|
||||||
MessageEvent::Redacted(_) => return None,
|
MessageEvent::Redacted(_) => return None,
|
||||||
@@ -752,7 +788,10 @@ impl Message {
|
|||||||
settings: &ApplicationSettings,
|
settings: &ApplicationSettings,
|
||||||
) -> Option<Span> {
|
) -> Option<Span> {
|
||||||
if let Some(prev) = prev {
|
if let Some(prev) = prev {
|
||||||
if self.sender == prev.sender && self.timestamp.same_day(&prev.timestamp) {
|
if self.sender == prev.sender &&
|
||||||
|
self.timestamp.same_day(&prev.timestamp) &&
|
||||||
|
!self.event.is_emote()
|
||||||
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -769,6 +808,24 @@ impl Message {
|
|||||||
|
|
||||||
Span::styled(sender, style).into()
|
Span::styled(sender, style).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn redact(&mut self, redaction: SyncRoomRedactionEvent, version: &RoomVersionId) {
|
||||||
|
self.event.redact(redaction, version);
|
||||||
|
self.html = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RoomEncryptedEvent> for Message {
|
||||||
|
fn from(event: RoomEncryptedEvent) -> Self {
|
||||||
|
let timestamp = event.origin_server_ts().into();
|
||||||
|
let user_id = event.sender().to_owned();
|
||||||
|
let content = match event {
|
||||||
|
RoomEncryptedEvent::Original(ev) => MessageEvent::EncryptedOriginal(ev.into()),
|
||||||
|
RoomEncryptedEvent::Redacted(ev) => MessageEvent::EncryptedRedacted(ev.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Message::new(content, user_id, timestamp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<OriginalRoomMessageEvent> for Message {
|
impl From<OriginalRoomMessageEvent> for Message {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use matrix_sdk::ruma::{
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use modalkit::tui::style::{Color, Style};
|
use modalkit::tui::style::{Color, Style};
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
use tracing::Level;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -153,6 +154,7 @@ pub fn mock_room() -> RoomInfo {
|
|||||||
read_till: None,
|
read_till: None,
|
||||||
reactions: HashMap::new(),
|
reactions: HashMap::new(),
|
||||||
|
|
||||||
|
fetching: false,
|
||||||
fetch_id: RoomFetchStatus::NotStarted,
|
fetch_id: RoomFetchStatus::NotStarted,
|
||||||
fetch_last: None,
|
fetch_last: None,
|
||||||
users_typing: None,
|
users_typing: None,
|
||||||
@@ -170,10 +172,12 @@ pub fn mock_dirs() -> DirectoryValues {
|
|||||||
pub fn mock_tunables() -> TunableValues {
|
pub fn mock_tunables() -> TunableValues {
|
||||||
TunableValues {
|
TunableValues {
|
||||||
default_room: None,
|
default_room: None,
|
||||||
|
log_level: Level::INFO,
|
||||||
reaction_display: true,
|
reaction_display: true,
|
||||||
reaction_shortcode_display: false,
|
reaction_shortcode_display: false,
|
||||||
read_receipt_send: true,
|
read_receipt_send: true,
|
||||||
read_receipt_display: true,
|
read_receipt_display: true,
|
||||||
|
request_timeout: 120,
|
||||||
typing_notice_send: true,
|
typing_notice_send: true,
|
||||||
typing_notice_display: true,
|
typing_notice_display: true,
|
||||||
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
encryption::verification::{format_emojis, SasVerification},
|
encryption::verification::{format_emojis, SasVerification},
|
||||||
@@ -77,6 +78,8 @@ use self::{room::RoomState, welcome::WelcomeState};
|
|||||||
pub mod room;
|
pub mod room;
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
|
||||||
|
const MEMBER_FETCH_DEBOUNCE: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn bold_style() -> Style {
|
fn bold_style() -> Style {
|
||||||
Style::default().add_modifier(StyleModifier::BOLD)
|
Style::default().add_modifier(StyleModifier::BOLD)
|
||||||
@@ -211,7 +214,7 @@ macro_rules! delegate {
|
|||||||
match $s {
|
match $s {
|
||||||
IambWindow::Room($id) => $e,
|
IambWindow::Room($id) => $e,
|
||||||
IambWindow::DirectList($id) => $e,
|
IambWindow::DirectList($id) => $e,
|
||||||
IambWindow::MemberList($id, _) => $e,
|
IambWindow::MemberList($id, _, _) => $e,
|
||||||
IambWindow::RoomList($id) => $e,
|
IambWindow::RoomList($id) => $e,
|
||||||
IambWindow::SpaceList($id) => $e,
|
IambWindow::SpaceList($id) => $e,
|
||||||
IambWindow::VerifyList($id) => $e,
|
IambWindow::VerifyList($id) => $e,
|
||||||
@@ -222,7 +225,7 @@ macro_rules! delegate {
|
|||||||
|
|
||||||
pub enum IambWindow {
|
pub enum IambWindow {
|
||||||
DirectList(DirectListState),
|
DirectList(DirectListState),
|
||||||
MemberList(MemberListState, OwnedRoomId),
|
MemberList(MemberListState, OwnedRoomId, Option<Instant>),
|
||||||
Room(RoomState),
|
Room(RoomState),
|
||||||
VerifyList(VerifyListState),
|
VerifyList(VerifyListState),
|
||||||
RoomList(RoomListState),
|
RoomList(RoomListState),
|
||||||
@@ -392,10 +395,18 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||||||
.focus(focused)
|
.focus(focused)
|
||||||
.render(area, buf, state);
|
.render(area, buf, state);
|
||||||
},
|
},
|
||||||
IambWindow::MemberList(state, room_id) => {
|
IambWindow::MemberList(state, room_id, last_fetch) => {
|
||||||
if let Ok(mems) = store.application.worker.members(room_id.clone()) {
|
let need_fetch = match last_fetch {
|
||||||
let items = mems.into_iter().map(MemberItem::new);
|
Some(i) => i.elapsed() >= MEMBER_FETCH_DEBOUNCE,
|
||||||
state.set(items.collect());
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if need_fetch {
|
||||||
|
if let Ok(mems) = store.application.worker.members(room_id.clone()) {
|
||||||
|
let items = mems.into_iter().map(MemberItem::new);
|
||||||
|
state.set(items.collect());
|
||||||
|
*last_fetch = Some(Instant::now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List::new(store)
|
List::new(store)
|
||||||
@@ -456,8 +467,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
|||||||
match self {
|
match self {
|
||||||
IambWindow::Room(w) => w.dup(store).into(),
|
IambWindow::Room(w) => w.dup(store).into(),
|
||||||
IambWindow::DirectList(w) => w.dup(store).into(),
|
IambWindow::DirectList(w) => w.dup(store).into(),
|
||||||
IambWindow::MemberList(w, room_id) => {
|
IambWindow::MemberList(w, room_id, last_fetch) => {
|
||||||
IambWindow::MemberList(w.dup(store), room_id.clone())
|
IambWindow::MemberList(w.dup(store), room_id.clone(), *last_fetch)
|
||||||
},
|
},
|
||||||
IambWindow::RoomList(w) => w.dup(store).into(),
|
IambWindow::RoomList(w) => w.dup(store).into(),
|
||||||
IambWindow::SpaceList(w) => w.dup(store).into(),
|
IambWindow::SpaceList(w) => w.dup(store).into(),
|
||||||
@@ -497,7 +508,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
match self {
|
match self {
|
||||||
IambWindow::Room(room) => IambId::Room(room.id().to_owned()),
|
IambWindow::Room(room) => IambId::Room(room.id().to_owned()),
|
||||||
IambWindow::DirectList(_) => IambId::DirectList,
|
IambWindow::DirectList(_) => IambId::DirectList,
|
||||||
IambWindow::MemberList(_, room_id) => IambId::MemberList(room_id.clone()),
|
IambWindow::MemberList(_, room_id, _) => IambId::MemberList(room_id.clone()),
|
||||||
IambWindow::RoomList(_) => IambId::RoomList,
|
IambWindow::RoomList(_) => IambId::RoomList,
|
||||||
IambWindow::SpaceList(_) => IambId::SpaceList,
|
IambWindow::SpaceList(_) => IambId::SpaceList,
|
||||||
IambWindow::VerifyList(_) => IambId::VerifyList,
|
IambWindow::VerifyList(_) => IambId::VerifyList,
|
||||||
@@ -518,7 +529,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
|
|
||||||
Spans::from(title)
|
Spans::from(title)
|
||||||
},
|
},
|
||||||
IambWindow::MemberList(_, room_id) => {
|
IambWindow::MemberList(_, room_id, _) => {
|
||||||
let title = store.application.get_room_title(room_id.as_ref());
|
let title = store.application.get_room_title(room_id.as_ref());
|
||||||
|
|
||||||
Spans(vec![bold_span("Room Members: "), title.into()])
|
Spans(vec![bold_span("Room Members: "), title.into()])
|
||||||
@@ -535,7 +546,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
IambWindow::Welcome(_) => bold_spans("Welcome to iamb"),
|
IambWindow::Welcome(_) => bold_spans("Welcome to iamb"),
|
||||||
|
|
||||||
IambWindow::Room(w) => w.get_title(store),
|
IambWindow::Room(w) => w.get_title(store),
|
||||||
IambWindow::MemberList(_, room_id) => {
|
IambWindow::MemberList(_, room_id, _) => {
|
||||||
let title = store.application.get_room_title(room_id.as_ref());
|
let title = store.application.get_room_title(room_id.as_ref());
|
||||||
|
|
||||||
Spans(vec![bold_span("Room Members: "), title.into()])
|
Spans(vec![bold_span("Room Members: "), title.into()])
|
||||||
@@ -559,7 +570,7 @@ impl Window<IambInfo> for IambWindow {
|
|||||||
IambId::MemberList(room_id) => {
|
IambId::MemberList(room_id) => {
|
||||||
let id = IambBufferId::MemberList(room_id.clone());
|
let id = IambBufferId::MemberList(room_id.clone());
|
||||||
let list = MemberListState::new(id, vec![]);
|
let list = MemberListState::new(id, vec![]);
|
||||||
let win = IambWindow::MemberList(list, room_id);
|
let win = IambWindow::MemberList(list, room_id, None);
|
||||||
|
|
||||||
return Ok(win);
|
return Ok(win);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ impl ChatState {
|
|||||||
MessageAction::React(emoji) => {
|
MessageAction::React(emoji) => {
|
||||||
let room = self.get_joined(&store.application.worker)?;
|
let room = self.get_joined(&store.application.worker)?;
|
||||||
let event_id = match &msg.event {
|
let event_id = match &msg.event {
|
||||||
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.clone(),
|
||||||
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
@@ -313,6 +315,8 @@ impl ChatState {
|
|||||||
MessageAction::Redact(reason) => {
|
MessageAction::Redact(reason) => {
|
||||||
let room = self.get_joined(&store.application.worker)?;
|
let room = self.get_joined(&store.application.worker)?;
|
||||||
let event_id = match &msg.event {
|
let event_id = match &msg.event {
|
||||||
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.clone(),
|
||||||
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.clone(),
|
MessageEvent::Original(ev) => ev.event_id.clone(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.clone(),
|
MessageEvent::Local(event_id, _) => event_id.clone(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
@@ -338,6 +342,8 @@ impl ChatState {
|
|||||||
MessageAction::Unreact(emoji) => {
|
MessageAction::Unreact(emoji) => {
|
||||||
let room = self.get_joined(&store.application.worker)?;
|
let room = self.get_joined(&store.application.worker)?;
|
||||||
let event_id: &EventId = match &msg.event {
|
let event_id: &EventId = match &msg.event {
|
||||||
|
MessageEvent::EncryptedOriginal(ev) => ev.event_id.as_ref(),
|
||||||
|
MessageEvent::EncryptedRedacted(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
MessageEvent::Original(ev) => ev.event_id.as_ref(),
|
||||||
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
MessageEvent::Local(event_id, _) => event_id.as_ref(),
|
||||||
MessageEvent::Redacted(_) => {
|
MessageEvent::Redacted(_) => {
|
||||||
@@ -395,13 +401,13 @@ impl ChatState {
|
|||||||
|
|
||||||
let (event_id, msg) = match act {
|
let (event_id, msg) = match act {
|
||||||
SendAction::Submit => {
|
SendAction::Submit => {
|
||||||
let msg = self.tbox.get_text();
|
let msg = self.tbox.get();
|
||||||
|
|
||||||
if msg.is_empty() {
|
if msg.is_blank() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = TextMessageEventContent::markdown(msg);
|
let msg = TextMessageEventContent::markdown(msg.to_string());
|
||||||
let msg = MessageType::Text(msg);
|
let msg = MessageType::Text(msg);
|
||||||
|
|
||||||
let mut msg = RoomMessageEventContent::new(msg);
|
let mut msg = RoomMessageEventContent::new(msg);
|
||||||
|
|||||||
254
src/worker.rs
254
src/worker.rs
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -5,21 +6,22 @@ use std::io::BufWriter;
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gethostname::gethostname;
|
use gethostname::gethostname;
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::error;
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
config::{RequestConfig, StoreConfig, SyncSettings},
|
config::{RequestConfig, SyncSettings},
|
||||||
encryption::verification::{SasVerification, Verification},
|
encryption::verification::{SasVerification, Verification},
|
||||||
event_handler::Ctx,
|
event_handler::Ctx,
|
||||||
reqwest,
|
reqwest,
|
||||||
room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
||||||
ruma::{
|
ruma::{
|
||||||
api::client::{
|
api::client::{
|
||||||
|
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
|
||||||
room::create_room::v3::{CreationContent, Request as CreateRoomRequest, RoomPreset},
|
room::create_room::v3::{CreationContent, Request as CreateRoomRequest, RoomPreset},
|
||||||
room::Visibility,
|
room::Visibility,
|
||||||
space::get_hierarchy::v1::Request as SpaceHierarchyRequest,
|
space::get_hierarchy::v1::Request as SpaceHierarchyRequest,
|
||||||
@@ -44,6 +46,7 @@ use matrix_sdk::{
|
|||||||
tag::Tags,
|
tag::Tags,
|
||||||
typing::SyncTypingEvent,
|
typing::SyncTypingEvent,
|
||||||
AnyInitialStateEvent,
|
AnyInitialStateEvent,
|
||||||
|
AnyMessageLikeEvent,
|
||||||
AnyTimelineEvent,
|
AnyTimelineEvent,
|
||||||
EmptyStateKey,
|
EmptyStateKey,
|
||||||
InitialStateEvent,
|
InitialStateEvent,
|
||||||
@@ -56,6 +59,7 @@ use matrix_sdk::{
|
|||||||
OwnedRoomId,
|
OwnedRoomId,
|
||||||
OwnedRoomOrAliasId,
|
OwnedRoomOrAliasId,
|
||||||
OwnedUserId,
|
OwnedUserId,
|
||||||
|
RoomId,
|
||||||
RoomVersionId,
|
RoomVersionId,
|
||||||
},
|
},
|
||||||
Client,
|
Client,
|
||||||
@@ -68,12 +72,14 @@ use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
|
|||||||
use crate::{
|
use crate::{
|
||||||
base::{
|
base::{
|
||||||
AsyncProgramStore,
|
AsyncProgramStore,
|
||||||
|
ChatStore,
|
||||||
CreateRoomFlags,
|
CreateRoomFlags,
|
||||||
CreateRoomType,
|
CreateRoomType,
|
||||||
EventLocation,
|
EventLocation,
|
||||||
IambError,
|
IambError,
|
||||||
IambResult,
|
IambResult,
|
||||||
Receipts,
|
Receipts,
|
||||||
|
RoomFetchStatus,
|
||||||
VerifyAction,
|
VerifyAction,
|
||||||
},
|
},
|
||||||
message::MessageFetchResult,
|
message::MessageFetchResult,
|
||||||
@@ -82,7 +88,7 @@ use crate::{
|
|||||||
|
|
||||||
const IAMB_DEVICE_NAME: &str = "iamb";
|
const IAMB_DEVICE_NAME: &str = "iamb";
|
||||||
const IAMB_USER_AGENT: &str = "iamb";
|
const IAMB_USER_AGENT: &str = "iamb";
|
||||||
const REQ_TIMEOUT: Duration = Duration::from_secs(60);
|
const MIN_MSG_LOAD: u32 = 50;
|
||||||
|
|
||||||
fn initial_devname() -> String {
|
fn initial_devname() -> String {
|
||||||
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
||||||
@@ -159,6 +165,116 @@ pub async fn create_room(
|
|||||||
return Ok(resp.room_id);
|
return Ok(resp.room_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_plan(store: &AsyncProgramStore) -> HashMap<OwnedRoomId, Option<String>> {
|
||||||
|
let mut locked = store.lock().await;
|
||||||
|
let ChatStore { need_load, rooms, .. } = &mut locked.application;
|
||||||
|
let mut plan = HashMap::new();
|
||||||
|
|
||||||
|
for room_id in std::mem::take(need_load).into_iter() {
|
||||||
|
let info = rooms.get_or_default(room_id.clone());
|
||||||
|
|
||||||
|
if info.recently_fetched() || info.fetching {
|
||||||
|
need_load.insert(room_id);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
info.fetch_last = Instant::now().into();
|
||||||
|
info.fetching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetch_id = match &info.fetch_id {
|
||||||
|
RoomFetchStatus::Done => continue,
|
||||||
|
RoomFetchStatus::HaveMore(fetch_id) => Some(fetch_id.clone()),
|
||||||
|
RoomFetchStatus::NotStarted => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
plan.insert(room_id, fetch_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_older_one(
|
||||||
|
client: Client,
|
||||||
|
room_id: &RoomId,
|
||||||
|
fetch_id: Option<String>,
|
||||||
|
limit: u32,
|
||||||
|
) -> MessageFetchResult {
|
||||||
|
if let Some(room) = client.get_room(room_id) {
|
||||||
|
let mut opts = match &fetch_id {
|
||||||
|
Some(id) => MessagesOptions::backward().from(id.as_str()),
|
||||||
|
None => MessagesOptions::backward(),
|
||||||
|
};
|
||||||
|
opts.limit = limit.into();
|
||||||
|
|
||||||
|
let Messages { end, chunk, .. } = room.messages(opts).await.map_err(IambError::from)?;
|
||||||
|
|
||||||
|
let msgs = chunk.into_iter().filter_map(|ev| {
|
||||||
|
match ev.event.deserialize() {
|
||||||
|
Ok(AnyTimelineEvent::MessageLike(msg)) => Some(msg),
|
||||||
|
Ok(AnyTimelineEvent::State(_)) => None,
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((end, msgs.collect()))
|
||||||
|
} else {
|
||||||
|
Err(IambError::UnknownRoom(room_id.to_owned()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_insert(room_id: OwnedRoomId, res: MessageFetchResult, store: AsyncProgramStore) {
|
||||||
|
let mut locked = store.lock().await;
|
||||||
|
let ChatStore { need_load, presences, rooms, .. } = &mut locked.application;
|
||||||
|
let info = rooms.get_or_default(room_id.clone());
|
||||||
|
info.fetching = false;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok((fetch_id, msgs)) => {
|
||||||
|
for msg in msgs.into_iter() {
|
||||||
|
let sender = msg.sender().to_owned();
|
||||||
|
let _ = presences.get_or_default(sender);
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
AnyMessageLikeEvent::RoomEncrypted(msg) => {
|
||||||
|
info.insert_encrypted(msg);
|
||||||
|
},
|
||||||
|
AnyMessageLikeEvent::RoomMessage(msg) => {
|
||||||
|
info.insert(msg);
|
||||||
|
},
|
||||||
|
AnyMessageLikeEvent::Reaction(ev) => {
|
||||||
|
info.insert_reaction(ev);
|
||||||
|
},
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info.fetch_id = fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
warn!(room_id = room_id.as_str(), err = e.to_string(), "Failed to load older messages");
|
||||||
|
|
||||||
|
// Wait and try again.
|
||||||
|
need_load.insert(room_id);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_older(client: &Client, store: &AsyncProgramStore) {
|
||||||
|
let limit = MIN_MSG_LOAD;
|
||||||
|
let plan = load_plan(store).await;
|
||||||
|
|
||||||
|
// Fetch each room separately, so they don't block each other.
|
||||||
|
for (room_id, fetch_id) in plan.into_iter() {
|
||||||
|
let client = client.clone();
|
||||||
|
let store = store.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let res = load_older_one(client, room_id.as_ref(), fetch_id, limit).await;
|
||||||
|
load_insert(room_id, res, store).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum LoginStyle {
|
pub enum LoginStyle {
|
||||||
SessionRestore(Session),
|
SessionRestore(Session),
|
||||||
@@ -217,7 +333,6 @@ pub enum WorkerTask {
|
|||||||
ActiveRooms(ClientReply<Vec<FetchedRoom>>),
|
ActiveRooms(ClientReply<Vec<FetchedRoom>>),
|
||||||
DirectMessages(ClientReply<Vec<FetchedRoom>>),
|
DirectMessages(ClientReply<Vec<FetchedRoom>>),
|
||||||
Init(AsyncProgramStore, ClientReply<()>),
|
Init(AsyncProgramStore, ClientReply<()>),
|
||||||
LoadOlder(OwnedRoomId, Option<String>, u32, ClientReply<MessageFetchResult>),
|
|
||||||
Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
|
Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
|
||||||
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>),
|
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>),
|
||||||
GetRoom(OwnedRoomId, ClientReply<IambResult<FetchedRoom>>),
|
GetRoom(OwnedRoomId, ClientReply<IambResult<FetchedRoom>>),
|
||||||
@@ -247,14 +362,6 @@ impl Debug for WorkerTask {
|
|||||||
.field(&format_args!("_"))
|
.field(&format_args!("_"))
|
||||||
.finish()
|
.finish()
|
||||||
},
|
},
|
||||||
WorkerTask::LoadOlder(room_id, from, n, _) => {
|
|
||||||
f.debug_tuple("WorkerTask::LoadOlder")
|
|
||||||
.field(room_id)
|
|
||||||
.field(from)
|
|
||||||
.field(n)
|
|
||||||
.field(&format_args!("_"))
|
|
||||||
.finish()
|
|
||||||
},
|
|
||||||
WorkerTask::Login(style, _) => {
|
WorkerTask::Login(style, _) => {
|
||||||
f.debug_tuple("WorkerTask::Login")
|
f.debug_tuple("WorkerTask::Login")
|
||||||
.field(style)
|
.field(style)
|
||||||
@@ -326,21 +433,6 @@ impl Requester {
|
|||||||
return response.recv();
|
return response.recv();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_older(
|
|
||||||
&self,
|
|
||||||
room_id: OwnedRoomId,
|
|
||||||
fetch_id: Option<String>,
|
|
||||||
limit: u32,
|
|
||||||
) -> MessageFetchResult {
|
|
||||||
let (reply, response) = oneshot();
|
|
||||||
|
|
||||||
self.tx
|
|
||||||
.send(WorkerTask::LoadOlder(room_id, fetch_id, limit, reply))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
return response.recv();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login(&self, style: LoginStyle) -> IambResult<EditInfo> {
|
pub fn login(&self, style: LoginStyle) -> IambResult<EditInfo> {
|
||||||
let (reply, response) = oneshot();
|
let (reply, response) = oneshot();
|
||||||
|
|
||||||
@@ -438,8 +530,9 @@ pub struct ClientWorker {
|
|||||||
initialized: bool,
|
initialized: bool,
|
||||||
settings: ApplicationSettings,
|
settings: ApplicationSettings,
|
||||||
client: Client,
|
client: Client,
|
||||||
sync_handle: Option<JoinHandle<()>>,
|
load_handle: Option<JoinHandle<()>>,
|
||||||
rcpt_handle: Option<JoinHandle<()>>,
|
rcpt_handle: Option<JoinHandle<()>>,
|
||||||
|
sync_handle: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientWorker {
|
impl ClientWorker {
|
||||||
@@ -447,28 +540,27 @@ impl ClientWorker {
|
|||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
let account = &settings.profile;
|
let account = &settings.profile;
|
||||||
|
|
||||||
// Set up a custom client that only uses HTTP/1.
|
let req_timeout = Duration::from_secs(settings.tunables.request_timeout);
|
||||||
//
|
|
||||||
// During my testing, I kept stumbling across something weird with sync and HTTP/2 that
|
// Set up the HTTP client.
|
||||||
// will need to be revisited in the future.
|
|
||||||
let http = reqwest::Client::builder()
|
let http = reqwest::Client::builder()
|
||||||
.user_agent(IAMB_USER_AGENT)
|
.user_agent(IAMB_USER_AGENT)
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(req_timeout)
|
||||||
.pool_idle_timeout(Duration::from_secs(60))
|
.pool_idle_timeout(Duration::from_secs(60))
|
||||||
.pool_max_idle_per_host(10)
|
.pool_max_idle_per_host(10)
|
||||||
.tcp_keepalive(Duration::from_secs(10))
|
.tcp_keepalive(Duration::from_secs(10))
|
||||||
.http1_only()
|
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout);
|
||||||
|
|
||||||
// Set up the Matrix client for the selected profile.
|
// Set up the Matrix client for the selected profile.
|
||||||
let client = Client::builder()
|
let client = Client::builder()
|
||||||
.http_client(Arc::new(http))
|
.http_client(Arc::new(http))
|
||||||
.homeserver_url(account.url.clone())
|
.homeserver_url(account.url.clone())
|
||||||
.store_config(StoreConfig::default())
|
|
||||||
.sled_store(settings.matrix_dir.as_path(), None)
|
.sled_store(settings.matrix_dir.as_path(), None)
|
||||||
.expect("Failed to setup up sled store for Matrix SDK")
|
.expect("Failed to setup up sled store for Matrix SDK")
|
||||||
.request_config(RequestConfig::new().timeout(REQ_TIMEOUT).retry_timeout(REQ_TIMEOUT))
|
.request_config(req_config)
|
||||||
.build()
|
.build()
|
||||||
.await
|
.await
|
||||||
.expect("Failed to instantiate Matrix client");
|
.expect("Failed to instantiate Matrix client");
|
||||||
@@ -477,8 +569,9 @@ impl ClientWorker {
|
|||||||
initialized: false,
|
initialized: false,
|
||||||
settings,
|
settings,
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
sync_handle: None,
|
load_handle: None,
|
||||||
rcpt_handle: None,
|
rcpt_handle: None,
|
||||||
|
sync_handle: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -536,10 +629,6 @@ impl ClientWorker {
|
|||||||
assert!(self.initialized);
|
assert!(self.initialized);
|
||||||
reply.send(self.active_rooms().await);
|
reply.send(self.active_rooms().await);
|
||||||
},
|
},
|
||||||
WorkerTask::LoadOlder(room_id, fetch_id, limit, reply) => {
|
|
||||||
assert!(self.initialized);
|
|
||||||
reply.send(self.load_older(room_id, fetch_id, limit).await);
|
|
||||||
},
|
|
||||||
WorkerTask::Login(style, reply) => {
|
WorkerTask::Login(style, reply) => {
|
||||||
assert!(self.initialized);
|
assert!(self.initialized);
|
||||||
reply.send(self.login_and_sync(style).await);
|
reply.send(self.login_and_sync(style).await);
|
||||||
@@ -685,7 +774,7 @@ impl ClientWorker {
|
|||||||
Some(EventLocation::Message(key)) => {
|
Some(EventLocation::Message(key)) => {
|
||||||
if let Some(msg) = info.messages.get_mut(key) {
|
if let Some(msg) = info.messages.get_mut(key) {
|
||||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||||
msg.event.redact(ev, room_version);
|
msg.redact(ev, room_version);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(EventLocation::Reaction(event_id)) => {
|
Some(EventLocation::Reaction(event_id)) => {
|
||||||
@@ -812,17 +901,34 @@ impl ClientWorker {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let client = self.client.clone();
|
self.rcpt_handle = tokio::spawn({
|
||||||
|
let store = store.clone();
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
self.rcpt_handle = tokio::spawn(async move {
|
async move {
|
||||||
// Update the displayed read receipts ever 5 seconds.
|
// Update the displayed read receipts every 5 seconds.
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
let receipts = update_receipts(&client).await;
|
let receipts = update_receipts(&client).await;
|
||||||
store.lock().await.application.set_receipts(receipts).await;
|
store.lock().await.application.set_receipts(receipts).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.load_handle = tokio::spawn({
|
||||||
|
let client = self.client.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
// Load older messages every 2 seconds.
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(2));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
load_older(&client, &store).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
@@ -861,10 +967,19 @@ impl ClientWorker {
|
|||||||
|
|
||||||
self.sync_handle = Some(handle);
|
self.sync_handle = Some(handle);
|
||||||
|
|
||||||
self.client
|
// Perform an initial, lazily-loaded sync.
|
||||||
.sync_once(SyncSettings::default())
|
let mut room = RoomEventFilter::default();
|
||||||
.await
|
room.lazy_load_options = LazyLoadOptions::Enabled { include_redundant_members: false };
|
||||||
.map_err(IambError::from)?;
|
|
||||||
|
let mut room_ev = RoomFilter::default();
|
||||||
|
room_ev.state = room;
|
||||||
|
|
||||||
|
let mut filter = FilterDefinition::default();
|
||||||
|
filter.room = room_ev;
|
||||||
|
|
||||||
|
let settings = SyncSettings::new().filter(filter.into());
|
||||||
|
|
||||||
|
self.client.sync_once(settings).await.map_err(IambError::from)?;
|
||||||
|
|
||||||
Ok(Some(InfoMessage::from("Successfully logged in!")))
|
Ok(Some(InfoMessage::from("Successfully logged in!")))
|
||||||
}
|
}
|
||||||
@@ -992,35 +1107,6 @@ impl ClientWorker {
|
|||||||
return rooms;
|
return rooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_older(
|
|
||||||
&mut self,
|
|
||||||
room_id: OwnedRoomId,
|
|
||||||
fetch_id: Option<String>,
|
|
||||||
limit: u32,
|
|
||||||
) -> MessageFetchResult {
|
|
||||||
if let Some(room) = self.client.get_room(room_id.as_ref()) {
|
|
||||||
let mut opts = match &fetch_id {
|
|
||||||
Some(id) => MessagesOptions::backward().from(id.as_str()),
|
|
||||||
None => MessagesOptions::backward(),
|
|
||||||
};
|
|
||||||
opts.limit = limit.into();
|
|
||||||
|
|
||||||
let Messages { end, chunk, .. } = room.messages(opts).await.map_err(IambError::from)?;
|
|
||||||
|
|
||||||
let msgs = chunk.into_iter().filter_map(|ev| {
|
|
||||||
match ev.event.deserialize() {
|
|
||||||
Ok(AnyTimelineEvent::MessageLike(msg)) => Some(msg),
|
|
||||||
Ok(AnyTimelineEvent::State(_)) => None,
|
|
||||||
Err(_) => None,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok((end, msgs.collect()))
|
|
||||||
} else {
|
|
||||||
Err(IambError::UnknownRoom(room_id).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
|
async fn members(&mut self, room_id: OwnedRoomId) -> IambResult<Vec<RoomMember>> {
|
||||||
if let Some(room) = self.client.get_room(room_id.as_ref()) {
|
if let Some(room) = self.client.get_room(room_id.as_ref()) {
|
||||||
Ok(room.active_members().await.map_err(IambError::from)?)
|
Ok(room.active_members().await.map_err(IambError::from)?)
|
||||||
|
|||||||
Reference in New Issue
Block a user