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]]
|
||||
name = "iamb"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"chrono",
|
||||
@@ -1882,9 +1882,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "modalkit"
|
||||
version = "0.0.13"
|
||||
version = "0.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038bb42efcd659fb123708bf8e4ea12ca9023f07d44514f9358b9334ea7ba80f"
|
||||
checksum = "5c48c7d7e6d764a09435b43a7e4d342ba2d2e026626ca773b16a5ba34b90b933"
|
||||
dependencies = [
|
||||
"anymap2",
|
||||
"arboard",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "iamb"
|
||||
version = "0.0.6"
|
||||
version = "0.0.7"
|
||||
edition = "2018"
|
||||
authors = ["Ulyssa <git@ulyssa.dev>"]
|
||||
repository = "https://github.com/ulyssa/iamb"
|
||||
@@ -39,7 +39,7 @@ unicode-width = "0.1.10"
|
||||
url = {version = "^2.2.2", features = ["serde"]}
|
||||
|
||||
[dependencies.modalkit]
|
||||
version = "0.0.13"
|
||||
version = "0.0.14"
|
||||
|
||||
[dependencies.matrix-sdk]
|
||||
version = "0.6"
|
||||
@@ -52,3 +52,7 @@ features = ["macros", "net", "rt-multi-thread", "sync", "time"]
|
||||
|
||||
[dev-dependencies]
|
||||
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://crates.io/crates/iamb)
|
||||
[](https://matrix.to/#/#iamb:0x.badd.cafe)
|
||||
[](https://crates.io/crates/iamb)
|
||||
|
||||
## About
|
||||
@@ -26,6 +27,22 @@ Install Rust and Cargo, and then run:
|
||||
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
|
||||
|
||||
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 tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::warn;
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::SasVerification,
|
||||
@@ -15,6 +14,7 @@ use matrix_sdk::{
|
||||
ruma::{
|
||||
events::{
|
||||
reaction::ReactionEvent,
|
||||
room::encrypted::RoomEncryptedEvent,
|
||||
room::message::{
|
||||
OriginalRoomMessageEvent,
|
||||
Relation,
|
||||
@@ -23,7 +23,6 @@ use matrix_sdk::{
|
||||
RoomMessageEventContent,
|
||||
},
|
||||
tag::{TagName, Tags},
|
||||
AnyMessageLikeEvent,
|
||||
MessageLikeEvent,
|
||||
},
|
||||
presence::PresenceState,
|
||||
@@ -412,6 +411,9 @@ pub struct RoomInfo {
|
||||
/// A map of message identifiers to a map of reaction events.
|
||||
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.
|
||||
pub fetch_id: RoomFetchStatus,
|
||||
|
||||
@@ -489,7 +491,9 @@ impl RoomInfo {
|
||||
MessageEvent::Local(_, content) => {
|
||||
*content = new_content;
|
||||
},
|
||||
MessageEvent::Redacted(_) => {
|
||||
MessageEvent::Redacted(_) |
|
||||
MessageEvent::EncryptedOriginal(_) |
|
||||
MessageEvent::EncryptedRedacted(_) => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
@@ -497,6 +501,15 @@ impl RoomInfo {
|
||||
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) {
|
||||
let event_id = msg.event_id().to_owned();
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -668,61 +681,6 @@ impl ChatStore {
|
||||
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 {
|
||||
self.rooms.get_or_default(room_id)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::process;
|
||||
use clap::Parser;
|
||||
use matrix_sdk::ruma::{OwnedUserId, UserId};
|
||||
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
||||
use tracing::Level;
|
||||
use url::Url;
|
||||
|
||||
use modalkit::tui::{
|
||||
@@ -25,6 +26,8 @@ macro_rules! usage {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_REQ_TIMEOUT: u64 = 120;
|
||||
|
||||
const COLORS: [Color; 13] = [
|
||||
Color::Blue,
|
||||
Color::Cyan,
|
||||
@@ -106,6 +109,47 @@ pub enum ConfigError {
|
||||
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)]
|
||||
pub struct UserColor(pub Color);
|
||||
pub struct UserColorVisitor;
|
||||
@@ -178,10 +222,12 @@ fn merge_users(a: Option<UserOverrides>, b: Option<UserOverrides>) -> Option<Use
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TunableValues {
|
||||
pub log_level: Level,
|
||||
pub reaction_display: bool,
|
||||
pub reaction_shortcode_display: bool,
|
||||
pub read_receipt_send: bool,
|
||||
pub read_receipt_display: bool,
|
||||
pub request_timeout: u64,
|
||||
pub typing_notice_send: bool,
|
||||
pub typing_notice_display: bool,
|
||||
pub users: UserOverrides,
|
||||
@@ -190,10 +236,12 @@ pub struct TunableValues {
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
pub struct Tunables {
|
||||
pub log_level: Option<LogLevel>,
|
||||
pub reaction_display: Option<bool>,
|
||||
pub reaction_shortcode_display: Option<bool>,
|
||||
pub read_receipt_send: Option<bool>,
|
||||
pub read_receipt_display: Option<bool>,
|
||||
pub request_timeout: Option<u64>,
|
||||
pub typing_notice_send: Option<bool>,
|
||||
pub typing_notice_display: Option<bool>,
|
||||
pub users: Option<UserOverrides>,
|
||||
@@ -203,12 +251,14 @@ pub struct Tunables {
|
||||
impl Tunables {
|
||||
fn merge(self, other: Self) -> Self {
|
||||
Tunables {
|
||||
log_level: self.log_level.or(other.log_level),
|
||||
reaction_display: self.reaction_display.or(other.reaction_display),
|
||||
reaction_shortcode_display: self
|
||||
.reaction_shortcode_display
|
||||
.or(other.reaction_shortcode_display),
|
||||
read_receipt_send: self.read_receipt_send.or(other.read_receipt_send),
|
||||
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_display: self.typing_notice_display.or(other.typing_notice_display),
|
||||
users: merge_users(self.users, other.users),
|
||||
@@ -218,10 +268,12 @@ impl Tunables {
|
||||
|
||||
fn values(self) -> TunableValues {
|
||||
TunableValues {
|
||||
log_level: self.log_level.map(Level::from).unwrap_or(Level::INFO),
|
||||
reaction_display: self.reaction_display.unwrap_or(true),
|
||||
reaction_shortcode_display: self.reaction_shortcode_display.unwrap_or(false),
|
||||
read_receipt_send: self.read_receipt_send.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_display: self.typing_notice_display.unwrap_or(true),
|
||||
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 tokio::sync::Mutex as AsyncMutex;
|
||||
use tracing::{self, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
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 {
|
||||
store: AsyncProgramStore,
|
||||
worker: Requester,
|
||||
@@ -190,8 +181,6 @@ impl Application {
|
||||
}
|
||||
f.set_cursor(cx, cy);
|
||||
}
|
||||
|
||||
store.application.load_older(msg_load_req(area));
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -544,7 +533,7 @@ fn main() -> IambResult<()> {
|
||||
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_writer(appender)
|
||||
.with_max_level(Level::INFO)
|
||||
.with_max_level(settings.tunables.log_level)
|
||||
.finish();
|
||||
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(
|
||||
RcDom::default(),
|
||||
ParseOpts::default(),
|
||||
QualName::new(None, ns!(), local_name!("div")),
|
||||
QualName::new(None, ns!(html), local_name!("body")),
|
||||
vec![],
|
||||
)
|
||||
.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::{
|
||||
events::{
|
||||
room::{
|
||||
encrypted::{
|
||||
OriginalRoomEncryptedEvent,
|
||||
RedactedRoomEncryptedEvent,
|
||||
RoomEncryptedEvent,
|
||||
},
|
||||
message::{
|
||||
FormattedBody,
|
||||
MessageFormat,
|
||||
@@ -26,6 +31,7 @@ use matrix_sdk::ruma::{
|
||||
},
|
||||
AnyMessageLikeEvent,
|
||||
Redact,
|
||||
RedactedUnsigned,
|
||||
},
|
||||
EventId,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
@@ -318,6 +324,8 @@ impl PartialOrd for MessageCursor {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum MessageEvent {
|
||||
EncryptedOriginal(Box<OriginalRoomEncryptedEvent>),
|
||||
EncryptedRedacted(Box<RedactedRoomEncryptedEvent>),
|
||||
Original(Box<OriginalRoomMessageEvent>),
|
||||
Redacted(Box<RedactedRoomMessageEvent>),
|
||||
Local(OwnedEventId, Box<RoomMessageEventContent>),
|
||||
@@ -326,35 +334,45 @@ pub enum MessageEvent {
|
||||
impl MessageEvent {
|
||||
pub fn event_id(&self) -> &EventId {
|
||||
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::Redacted(ev) => ev.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> {
|
||||
match self {
|
||||
MessageEvent::EncryptedOriginal(_) => "[Unable to decrypt message]".into(),
|
||||
MessageEvent::Original(ev) => body_cow_content(&ev.content),
|
||||
MessageEvent::Redacted(ev) => {
|
||||
let reason = ev
|
||||
.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::EncryptedRedacted(ev) => body_cow_reason(&ev.unsigned),
|
||||
MessageEvent::Redacted(ev) => body_cow_reason(&ev.unsigned),
|
||||
MessageEvent::Local(_, content) => body_cow_content(content),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn html(&self) -> Option<StyleTree> {
|
||||
let content = match self {
|
||||
MessageEvent::EncryptedOriginal(_) => return None,
|
||||
MessageEvent::EncryptedRedacted(_) => return None,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
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 {
|
||||
MessageEvent::EncryptedOriginal(_) => return,
|
||||
MessageEvent::EncryptedRedacted(_) => return,
|
||||
MessageEvent::Redacted(_) => return,
|
||||
MessageEvent::Local(_, _) => return,
|
||||
MessageEvent::Original(ev) => {
|
||||
@@ -411,6 +431,20 @@ fn body_cow_content(content: &RoomMessageEventContent) -> Cow<'_, str> {
|
||||
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 {
|
||||
/// Four columns: sender, message, timestamp, read receipts.
|
||||
Four,
|
||||
@@ -548,6 +582,8 @@ impl Message {
|
||||
|
||||
pub fn reply_to(&self) -> Option<OwnedEventId> {
|
||||
let content = match &self.event {
|
||||
MessageEvent::EncryptedOriginal(_) => return None,
|
||||
MessageEvent::EncryptedRedacted(_) => return None,
|
||||
MessageEvent::Local(_, content) => content,
|
||||
MessageEvent::Original(ev) => &ev.content,
|
||||
MessageEvent::Redacted(_) => return None,
|
||||
@@ -752,7 +788,10 @@ impl Message {
|
||||
settings: &ApplicationSettings,
|
||||
) -> Option<Span> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -769,6 +808,24 @@ impl Message {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -17,6 +17,7 @@ use matrix_sdk::ruma::{
|
||||
use lazy_static::lazy_static;
|
||||
use modalkit::tui::style::{Color, Style};
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use tracing::Level;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
@@ -153,6 +154,7 @@ pub fn mock_room() -> RoomInfo {
|
||||
read_till: None,
|
||||
reactions: HashMap::new(),
|
||||
|
||||
fetching: false,
|
||||
fetch_id: RoomFetchStatus::NotStarted,
|
||||
fetch_last: None,
|
||||
users_typing: None,
|
||||
@@ -170,10 +172,12 @@ pub fn mock_dirs() -> DirectoryValues {
|
||||
pub fn mock_tunables() -> TunableValues {
|
||||
TunableValues {
|
||||
default_room: None,
|
||||
log_level: Level::INFO,
|
||||
reaction_display: true,
|
||||
reaction_shortcode_display: false,
|
||||
read_receipt_send: true,
|
||||
read_receipt_display: true,
|
||||
request_timeout: 120,
|
||||
typing_notice_send: true,
|
||||
typing_notice_display: true,
|
||||
users: vec![(TEST_USER5.clone(), UserDisplayTunables {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::cmp::{Ord, Ordering, PartialOrd};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::{format_emojis, SasVerification},
|
||||
@@ -77,6 +78,8 @@ use self::{room::RoomState, welcome::WelcomeState};
|
||||
pub mod room;
|
||||
pub mod welcome;
|
||||
|
||||
const MEMBER_FETCH_DEBOUNCE: Duration = Duration::from_secs(5);
|
||||
|
||||
#[inline]
|
||||
fn bold_style() -> Style {
|
||||
Style::default().add_modifier(StyleModifier::BOLD)
|
||||
@@ -211,7 +214,7 @@ macro_rules! delegate {
|
||||
match $s {
|
||||
IambWindow::Room($id) => $e,
|
||||
IambWindow::DirectList($id) => $e,
|
||||
IambWindow::MemberList($id, _) => $e,
|
||||
IambWindow::MemberList($id, _, _) => $e,
|
||||
IambWindow::RoomList($id) => $e,
|
||||
IambWindow::SpaceList($id) => $e,
|
||||
IambWindow::VerifyList($id) => $e,
|
||||
@@ -222,7 +225,7 @@ macro_rules! delegate {
|
||||
|
||||
pub enum IambWindow {
|
||||
DirectList(DirectListState),
|
||||
MemberList(MemberListState, OwnedRoomId),
|
||||
MemberList(MemberListState, OwnedRoomId, Option<Instant>),
|
||||
Room(RoomState),
|
||||
VerifyList(VerifyListState),
|
||||
RoomList(RoomListState),
|
||||
@@ -392,10 +395,18 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||
.focus(focused)
|
||||
.render(area, buf, state);
|
||||
},
|
||||
IambWindow::MemberList(state, room_id) => {
|
||||
IambWindow::MemberList(state, room_id, last_fetch) => {
|
||||
let need_fetch = match last_fetch {
|
||||
Some(i) => i.elapsed() >= MEMBER_FETCH_DEBOUNCE,
|
||||
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)
|
||||
@@ -456,8 +467,8 @@ impl WindowOps<IambInfo> for IambWindow {
|
||||
match self {
|
||||
IambWindow::Room(w) => w.dup(store).into(),
|
||||
IambWindow::DirectList(w) => w.dup(store).into(),
|
||||
IambWindow::MemberList(w, room_id) => {
|
||||
IambWindow::MemberList(w.dup(store), room_id.clone())
|
||||
IambWindow::MemberList(w, room_id, last_fetch) => {
|
||||
IambWindow::MemberList(w.dup(store), room_id.clone(), *last_fetch)
|
||||
},
|
||||
IambWindow::RoomList(w) => w.dup(store).into(),
|
||||
IambWindow::SpaceList(w) => w.dup(store).into(),
|
||||
@@ -497,7 +508,7 @@ impl Window<IambInfo> for IambWindow {
|
||||
match self {
|
||||
IambWindow::Room(room) => IambId::Room(room.id().to_owned()),
|
||||
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::SpaceList(_) => IambId::SpaceList,
|
||||
IambWindow::VerifyList(_) => IambId::VerifyList,
|
||||
@@ -518,7 +529,7 @@ impl Window<IambInfo> for IambWindow {
|
||||
|
||||
Spans::from(title)
|
||||
},
|
||||
IambWindow::MemberList(_, room_id) => {
|
||||
IambWindow::MemberList(_, room_id, _) => {
|
||||
let title = store.application.get_room_title(room_id.as_ref());
|
||||
|
||||
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::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());
|
||||
|
||||
Spans(vec![bold_span("Room Members: "), title.into()])
|
||||
@@ -559,7 +570,7 @@ impl Window<IambInfo> for IambWindow {
|
||||
IambId::MemberList(room_id) => {
|
||||
let id = IambBufferId::MemberList(room_id.clone());
|
||||
let list = MemberListState::new(id, vec![]);
|
||||
let win = IambWindow::MemberList(list, room_id);
|
||||
let win = IambWindow::MemberList(list, room_id, None);
|
||||
|
||||
return Ok(win);
|
||||
},
|
||||
|
||||
@@ -294,6 +294,8 @@ impl ChatState {
|
||||
MessageAction::React(emoji) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
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::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
@@ -313,6 +315,8 @@ impl ChatState {
|
||||
MessageAction::Redact(reason) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
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::Local(event_id, _) => event_id.clone(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
@@ -338,6 +342,8 @@ impl ChatState {
|
||||
MessageAction::Unreact(emoji) => {
|
||||
let room = self.get_joined(&store.application.worker)?;
|
||||
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::Local(event_id, _) => event_id.as_ref(),
|
||||
MessageEvent::Redacted(_) => {
|
||||
@@ -395,13 +401,13 @@ impl ChatState {
|
||||
|
||||
let (event_id, msg) = match act {
|
||||
SendAction::Submit => {
|
||||
let msg = self.tbox.get_text();
|
||||
let msg = self.tbox.get();
|
||||
|
||||
if msg.is_empty() {
|
||||
if msg.is_blank() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let msg = TextMessageEventContent::markdown(msg);
|
||||
let msg = TextMessageEventContent::markdown(msg.to_string());
|
||||
let msg = MessageType::Text(msg);
|
||||
|
||||
let mut msg = RoomMessageEventContent::new(msg);
|
||||
|
||||
242
src/worker.rs
242
src/worker.rs
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
@@ -5,21 +6,22 @@ use std::io::BufWriter;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gethostname::gethostname;
|
||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::error;
|
||||
use tracing::{error, warn};
|
||||
|
||||
use matrix_sdk::{
|
||||
config::{RequestConfig, StoreConfig, SyncSettings},
|
||||
config::{RequestConfig, SyncSettings},
|
||||
encryption::verification::{SasVerification, Verification},
|
||||
event_handler::Ctx,
|
||||
reqwest,
|
||||
room::{Invited, Messages, MessagesOptions, Room as MatrixRoom, RoomMember},
|
||||
ruma::{
|
||||
api::client::{
|
||||
filter::{FilterDefinition, LazyLoadOptions, RoomEventFilter, RoomFilter},
|
||||
room::create_room::v3::{CreationContent, Request as CreateRoomRequest, RoomPreset},
|
||||
room::Visibility,
|
||||
space::get_hierarchy::v1::Request as SpaceHierarchyRequest,
|
||||
@@ -44,6 +46,7 @@ use matrix_sdk::{
|
||||
tag::Tags,
|
||||
typing::SyncTypingEvent,
|
||||
AnyInitialStateEvent,
|
||||
AnyMessageLikeEvent,
|
||||
AnyTimelineEvent,
|
||||
EmptyStateKey,
|
||||
InitialStateEvent,
|
||||
@@ -56,6 +59,7 @@ use matrix_sdk::{
|
||||
OwnedRoomId,
|
||||
OwnedRoomOrAliasId,
|
||||
OwnedUserId,
|
||||
RoomId,
|
||||
RoomVersionId,
|
||||
},
|
||||
Client,
|
||||
@@ -68,12 +72,14 @@ use modalkit::editing::action::{EditInfo, InfoMessage, UIError};
|
||||
use crate::{
|
||||
base::{
|
||||
AsyncProgramStore,
|
||||
ChatStore,
|
||||
CreateRoomFlags,
|
||||
CreateRoomType,
|
||||
EventLocation,
|
||||
IambError,
|
||||
IambResult,
|
||||
Receipts,
|
||||
RoomFetchStatus,
|
||||
VerifyAction,
|
||||
},
|
||||
message::MessageFetchResult,
|
||||
@@ -82,7 +88,7 @@ use crate::{
|
||||
|
||||
const IAMB_DEVICE_NAME: &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 {
|
||||
format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy())
|
||||
@@ -159,6 +165,116 @@ pub async fn create_room(
|
||||
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)]
|
||||
pub enum LoginStyle {
|
||||
SessionRestore(Session),
|
||||
@@ -217,7 +333,6 @@ pub enum WorkerTask {
|
||||
ActiveRooms(ClientReply<Vec<FetchedRoom>>),
|
||||
DirectMessages(ClientReply<Vec<FetchedRoom>>),
|
||||
Init(AsyncProgramStore, ClientReply<()>),
|
||||
LoadOlder(OwnedRoomId, Option<String>, u32, ClientReply<MessageFetchResult>),
|
||||
Login(LoginStyle, ClientReply<IambResult<EditInfo>>),
|
||||
GetInviter(Invited, ClientReply<IambResult<Option<RoomMember>>>),
|
||||
GetRoom(OwnedRoomId, ClientReply<IambResult<FetchedRoom>>),
|
||||
@@ -247,14 +362,6 @@ impl Debug for WorkerTask {
|
||||
.field(&format_args!("_"))
|
||||
.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, _) => {
|
||||
f.debug_tuple("WorkerTask::Login")
|
||||
.field(style)
|
||||
@@ -326,21 +433,6 @@ impl Requester {
|
||||
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> {
|
||||
let (reply, response) = oneshot();
|
||||
|
||||
@@ -438,8 +530,9 @@ pub struct ClientWorker {
|
||||
initialized: bool,
|
||||
settings: ApplicationSettings,
|
||||
client: Client,
|
||||
sync_handle: Option<JoinHandle<()>>,
|
||||
load_handle: Option<JoinHandle<()>>,
|
||||
rcpt_handle: Option<JoinHandle<()>>,
|
||||
sync_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl ClientWorker {
|
||||
@@ -447,28 +540,27 @@ impl ClientWorker {
|
||||
let (tx, rx) = unbounded_channel();
|
||||
let account = &settings.profile;
|
||||
|
||||
// Set up a custom client that only uses HTTP/1.
|
||||
//
|
||||
// During my testing, I kept stumbling across something weird with sync and HTTP/2 that
|
||||
// will need to be revisited in the future.
|
||||
let req_timeout = Duration::from_secs(settings.tunables.request_timeout);
|
||||
|
||||
// Set up the HTTP client.
|
||||
let http = reqwest::Client::builder()
|
||||
.user_agent(IAMB_USER_AGENT)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.timeout(req_timeout)
|
||||
.pool_idle_timeout(Duration::from_secs(60))
|
||||
.pool_max_idle_per_host(10)
|
||||
.tcp_keepalive(Duration::from_secs(10))
|
||||
.http1_only()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let req_config = RequestConfig::new().timeout(req_timeout).retry_timeout(req_timeout);
|
||||
|
||||
// Set up the Matrix client for the selected profile.
|
||||
let client = Client::builder()
|
||||
.http_client(Arc::new(http))
|
||||
.homeserver_url(account.url.clone())
|
||||
.store_config(StoreConfig::default())
|
||||
.sled_store(settings.matrix_dir.as_path(), None)
|
||||
.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()
|
||||
.await
|
||||
.expect("Failed to instantiate Matrix client");
|
||||
@@ -477,8 +569,9 @@ impl ClientWorker {
|
||||
initialized: false,
|
||||
settings,
|
||||
client: client.clone(),
|
||||
sync_handle: None,
|
||||
load_handle: None,
|
||||
rcpt_handle: None,
|
||||
sync_handle: None,
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -536,10 +629,6 @@ impl ClientWorker {
|
||||
assert!(self.initialized);
|
||||
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) => {
|
||||
assert!(self.initialized);
|
||||
reply.send(self.login_and_sync(style).await);
|
||||
@@ -685,7 +774,7 @@ impl ClientWorker {
|
||||
Some(EventLocation::Message(key)) => {
|
||||
if let Some(msg) = info.messages.get_mut(key) {
|
||||
let ev = SyncRoomRedactionEvent::Original(ev);
|
||||
msg.event.redact(ev, room_version);
|
||||
msg.redact(ev, room_version);
|
||||
}
|
||||
},
|
||||
Some(EventLocation::Reaction(event_id)) => {
|
||||
@@ -812,10 +901,12 @@ impl ClientWorker {
|
||||
},
|
||||
);
|
||||
|
||||
self.rcpt_handle = tokio::spawn({
|
||||
let store = store.clone();
|
||||
let client = self.client.clone();
|
||||
|
||||
self.rcpt_handle = tokio::spawn(async move {
|
||||
// Update the displayed read receipts ever 5 seconds.
|
||||
async move {
|
||||
// Update the displayed read receipts every 5 seconds.
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(5));
|
||||
|
||||
loop {
|
||||
@@ -824,6 +915,21 @@ impl ClientWorker {
|
||||
let receipts = update_receipts(&client).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();
|
||||
|
||||
@@ -861,10 +967,19 @@ impl ClientWorker {
|
||||
|
||||
self.sync_handle = Some(handle);
|
||||
|
||||
self.client
|
||||
.sync_once(SyncSettings::default())
|
||||
.await
|
||||
.map_err(IambError::from)?;
|
||||
// Perform an initial, lazily-loaded sync.
|
||||
let mut room = RoomEventFilter::default();
|
||||
room.lazy_load_options = LazyLoadOptions::Enabled { include_redundant_members: false };
|
||||
|
||||
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!")))
|
||||
}
|
||||
@@ -992,35 +1107,6 @@ impl ClientWorker {
|
||||
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>> {
|
||||
if let Some(room) = self.client.get_room(room_id.as_ref()) {
|
||||
Ok(room.active_members().await.map_err(IambError::from)?)
|
||||
|
||||
Reference in New Issue
Block a user