15 Commits

Author SHA1 Message Date
Ulyssa
a2590b6bbb Release v0.0.7 (#68) 2023-03-22 21:28:34 -07:00
Ulyssa
725ebb9fd6 Redacted messages should have their HTML removed (#67) 2023-03-22 21:25:37 -07:00
jahway603
ca395097e1 Update README.md to include Arch Linux package (#66) 2023-03-22 20:13:25 -07:00
Ulyssa
e98d58a8cc Emote messages should always show sender (#65) 2023-03-21 14:02:42 -07:00
Ulyssa
e6cdd02f22 HTML self-closing tags are getting parsed incorrectly (#63) 2023-03-20 17:53:55 -07:00
Ulyssa
0bc4ff07b0 Lazy load room state events on initial sync (#62) 2023-03-20 16:17:59 -07:00
Ulyssa
14fe916d94 Allow log level to be configured (#58) 2023-03-13 16:43:08 -07:00
Ulyssa
db35581d07 Indicate when an encrypted room event has been redacted (#59) 2023-03-13 16:43:04 -07:00
Ulyssa
7c1c62897a Show events that couldn't be decrypted (#57) 2023-03-13 15:18:53 -07:00
Ulyssa
61897ea6f2 Fetch scrollback history independently of main loop (#39) 2023-03-13 10:46:26 -07:00
Ulyssa
6a0722795a Fix empty message check when sending (#56) 2023-03-13 09:26:49 -07:00
Ulyssa
f3bbc6ad9f Support configuring client request timeout (#54) 2023-03-12 15:43:13 -07:00
Ulyssa
2dd8c0fddf Link to iamb space in README (#55) 2023-03-10 18:08:42 -08:00
Pavlo Rudy
a786369b14 Create release profile with LTO (#52) 2023-03-10 16:41:32 -08:00
pin
066f60ad32 Add NetBSD install instructions (#51) 2023-03-09 09:27:40 -08:00
12 changed files with 387 additions and 192 deletions

6
Cargo.lock generated
View File

@@ -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",

View File

@@ -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

View File

@@ -2,6 +2,7 @@
[![Build Status](https://github.com/ulyssa/iamb/workflows/CI/badge.svg)](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+) [![Build Status](https://github.com/ulyssa/iamb/workflows/CI/badge.svg)](https://github.com/ulyssa/iamb/actions?query=workflow%3ACI+)
[![License: Apache 2.0](https://img.shields.io/crates/l/iamb.svg?logo=apache)](https://crates.io/crates/iamb) [![License: Apache 2.0](https://img.shields.io/crates/l/iamb.svg?logo=apache)](https://crates.io/crates/iamb)
[![#iamb:0x.badd.cafe](https://img.shields.io/badge/matrix-%23iamb:0x.badd.cafe-blue)](https://matrix.to/#/#iamb:0x.badd.cafe)
[![Latest Version](https://img.shields.io/crates/v/iamb.svg?logo=rust)](https://crates.io/crates/iamb) [![Latest Version](https://img.shields.io/crates/v/iamb.svg?logo=rust)](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:

View File

@@ -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)
} }

View File

@@ -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(),

View File

@@ -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");

View File

@@ -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")]),);
}
} }

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}, },

View File

@@ -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);

View File

@@ -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)?)