diff --git a/Cargo.lock b/Cargo.lock index 55744d7e0..d61483611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -624,9 +624,9 @@ checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -2265,6 +2265,7 @@ name = "jack-in" version = "0.2.0" dependencies = [ "app_dirs2", + "chrono", "dialoguer", "eyre", "futures", diff --git a/bindings/matrix-sdk-ffi/Cargo.toml b/bindings/matrix-sdk-ffi/Cargo.toml index 173fb3ea9..d210bfaf8 100644 --- a/bindings/matrix-sdk-ffi/Cargo.toml +++ b/bindings/matrix-sdk-ffi/Cargo.toml @@ -12,10 +12,6 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk" [lib] crate-type = ["cdylib", "staticlib"] -[features] -default = ["experimental-room-preview"] # the whole crate is still very experimental, so this is fine -experimental-room-preview = ["matrix-sdk/experimental-room-preview"] - [build-dependencies] uniffi_build = { workspace = true, features = ["builtin-bindgen"] } diff --git a/bindings/matrix-sdk-ffi/src/sliding_sync.rs b/bindings/matrix-sdk-ffi/src/sliding_sync.rs index ec256f87d..6a432cb6e 100644 --- a/bindings/matrix-sdk-ffi/src/sliding_sync.rs +++ b/bindings/matrix-sdk-ffi/src/sliding_sync.rs @@ -5,10 +5,6 @@ use futures_signals::{ signal_vec::{SignalVecExt, VecDiff}, }; use futures_util::{pin_mut, StreamExt}; -#[cfg(feature = "experimental-room-preview")] -use matrix_sdk::ruma::events::{ - room::message::SyncRoomMessageEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent, -}; use matrix_sdk::ruma::{ api::client::sync::sync_events::{ v4::RoomSubscription as RumaRoomSubscription, @@ -23,9 +19,7 @@ pub use matrix_sdk::{ use tokio::task::JoinHandle; use super::{Client, Room, RUNTIME}; -use crate::helpers::unwrap_or_clone_arc; -#[cfg(feature = "experimental-room-preview")] -use crate::EventTimelineItem; +use crate::{helpers::unwrap_or_clone_arc, EventTimelineItem}; pub struct StoppableSpawn { handle: Arc>>>, @@ -126,25 +120,14 @@ impl SlidingSyncRoom { } } -#[cfg(feature = "experimental-room-preview")] #[uniffi::export] impl SlidingSyncRoom { #[allow(clippy::significant_drop_in_scrutinee)] pub fn latest_room_message(&self) -> Option> { - let messages = self.inner.timeline(); - // room is having the latest events at the end, - let lock = messages.lock_ref(); - for ev in lock.iter().rev() { - if let Ok(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage( - SyncRoomMessageEvent::Original(o), - ))) = ev.event.deserialize() - { - let inner = - matrix_sdk::room::timeline::EventTimelineItem::_new(o, ev.event.clone()); - return Some(Arc::new(EventTimelineItem(inner))); - } - } - None + RUNTIME.block_on(async { + let item = self.inner.timeline().await.latest()?.as_event()?.to_owned(); + Some(Arc::new(EventTimelineItem(item))) + }) } } diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 9363882a2..2f30f045d 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -42,7 +42,6 @@ appservice = ["ruma/appservice-api-s"] image-proc = ["dep:image"] image-rayon = ["image-proc", "image?/jpeg_rayon"] -experimental-room-preview = [] experimental-timeline = ["ruma/unstable-msc2677"] sliding-sync = [ diff --git a/crates/matrix-sdk/src/room/timeline/event_item.rs b/crates/matrix-sdk/src/room/timeline/event_item.rs index 1d5420909..8bacadc36 100644 --- a/crates/matrix-sdk/src/room/timeline/event_item.rs +++ b/crates/matrix-sdk/src/room/timeline/event_item.rs @@ -16,8 +16,6 @@ use std::fmt; use indexmap::IndexMap; use matrix_sdk_base::deserialized_responses::EncryptionInfo; -#[cfg(feature = "experimental-room-preview")] -use ruma::events::room::message::{OriginalSyncRoomMessageEvent, Relation}; use ruma::{ events::{ relation::{AnnotationChunk, AnnotationType}, @@ -85,37 +83,6 @@ macro_rules! build { } impl EventTimelineItem { - #[cfg(feature = "experimental-room-preview")] - #[doc(hidden)] // FIXME: Remove. Used for matrix-sdk-ffi temporarily. - pub fn _new(ev: OriginalSyncRoomMessageEvent, raw: Raw) -> Self { - let edited = ev.unsigned.relations.as_ref().map_or(false, |r| r.replace.is_some()); - let reactions = ev - .unsigned - .relations - .and_then(|r| r.annotation) - .map(BundledReactions::from) - .unwrap_or_default(); - - Self { - key: TimelineKey::EventId(ev.event_id), - event_id: None, - sender: ev.sender, - content: TimelineItemContent::Message(Message { - msgtype: ev.content.msgtype, - in_reply_to: ev.content.relates_to.and_then(|rel| match rel { - Relation::Reply { in_reply_to } => Some(in_reply_to.event_id), - _ => None, - }), - edited, - }), - reactions, - origin_server_ts: Some(ev.origin_server_ts), - is_own: false, // FIXME: Potentially wrong - encryption_info: None, // FIXME: Potentially wrong - raw: Some(raw), - } - } - /// Get the [`TimelineKey`] of this item. pub fn key(&self) -> &TimelineKey { &self.key diff --git a/crates/matrix-sdk/src/room/timeline/mod.rs b/crates/matrix-sdk/src/room/timeline/mod.rs index d3e918a17..b0b2dc8fa 100644 --- a/crates/matrix-sdk/src/room/timeline/mod.rs +++ b/crates/matrix-sdk/src/room/timeline/mod.rs @@ -23,7 +23,10 @@ use std::{ use futures_core::Stream; use futures_signals::signal_vec::{MutableVec, SignalVec, SignalVecExt, VecDiff}; -use matrix_sdk_base::{deserialized_responses::EncryptionInfo, locks::Mutex}; +use matrix_sdk_base::{ + deserialized_responses::{EncryptionInfo, SyncTimelineEvent}, + locks::Mutex, +}; use ruma::{ assign, events::{ @@ -89,7 +92,22 @@ struct TimelineInnerMetadata { impl Timeline { pub(super) async fn new(room: &room::Common) -> Self { + Self::with_events(room, None, Vec::new()).await + } + + pub(crate) async fn with_events( + room: &room::Common, + prev_token: Option, + events: Vec, + ) -> Self { let inner = TimelineInner::default(); + let own_user_id = room.own_user_id(); + + for ev in events.into_iter().rev() { + inner + .handle_back_paginated_event(ev.event.cast(), ev.encryption_info, own_user_id) + .await; + } match room.account_data_static::().await { Ok(Some(fully_read)) => match fully_read.deserialize() { @@ -176,7 +194,7 @@ impl Timeline { Timeline { inner, room: room.clone(), - start_token: StdMutex::new(None), + start_token: StdMutex::new(prev_token), _end_token: StdMutex::new(None), _timeline_event_handler_guard, _fully_read_handler_guard, @@ -255,6 +273,11 @@ impl Timeline { .await; } + /// Get the latest of the timeline's items. + pub fn latest(&self) -> Option> { + self.inner.items.lock_ref().last().cloned() + } + /// Get a signal of the timeline's items. /// /// You can poll this signal to receive updates, the first of which will diff --git a/crates/matrix-sdk/src/sliding_sync.rs b/crates/matrix-sdk/src/sliding_sync.rs index cf3a69980..85531960f 100644 --- a/crates/matrix-sdk/src/sliding_sync.rs +++ b/crates/matrix-sdk/src/sliding_sync.rs @@ -29,6 +29,8 @@ use ruma::{ use thiserror::Error; use url::Url; +#[cfg(feature = "experimental-timeline")] +use crate::room::timeline::Timeline; use crate::{Client, Result}; /// Internal representation of errors in Sliding Sync @@ -106,6 +108,7 @@ pub type AliveRoomTimeline = Arc, @@ -115,6 +118,7 @@ pub struct SlidingSyncRoom { impl SlidingSyncRoom { fn from( + client: Client, room_id: OwnedRoomId, mut inner: v4::SlidingSyncRoom, timeline: Vec, @@ -122,6 +126,7 @@ impl SlidingSyncRoom { // we overwrite to only keep one copy inner.timeline = vec![]; Self { + client, room_id, is_loading_more: Mutable::new(false), prev_batch: Mutable::new(inner.prev_batch.clone()), @@ -146,10 +151,20 @@ impl SlidingSyncRoom { } /// `AliveTimeline` of this room + #[cfg(not(feature = "experimental-timeline"))] pub fn timeline(&self) -> AliveRoomTimeline { self.timeline.clone() } + /// `Timeline` of this room + #[cfg(feature = "experimental-timeline")] + pub async fn timeline(&self) -> Timeline { + let current_timeline = self.timeline.lock_ref().to_vec(); + let prev_batch = self.prev_batch.lock_ref().clone(); + let room = self.client.get_room(&self.room_id).unwrap(); + Timeline::with_events(&room, prev_batch, current_timeline).await + } + /// This rooms name as calculated by the server, if any pub fn name(&self) -> Option<&str> { self.inner.name.as_deref() @@ -490,7 +505,7 @@ impl SlidingSync { } else { rooms_map.insert_cloned( id.clone(), - SlidingSyncRoom::from(id.clone(), room_data, timeline), + SlidingSyncRoom::from(self.client.clone(), id.clone(), room_data, timeline), ); rooms.push(id); } diff --git a/labs/jack-in/Cargo.toml b/labs/jack-in/Cargo.toml index ef98c0b57..311374f1f 100644 --- a/labs/jack-in/Cargo.toml +++ b/labs/jack-in/Cargo.toml @@ -10,11 +10,12 @@ file-logging = ["dep:log4rs"] [dependencies] app_dirs2 = "2" +chrono = "0.4.23" dialoguer = "0.10.2" eyre = "0.6" futures = { version = "0.3.1" } futures-signals = "0.3.24" -matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync"], version = "0.6.0" } +matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "sliding-sync", "experimental-timeline"], version = "0.6.0" } matrix-sdk-common = { path = "../../crates/matrix-sdk-common", version = "0.6.0" } matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", features = ["state-store", "crypto-store"], version = "0.2.0" } sanitize-filename-reader-friendly = "2.2.1" @@ -22,7 +23,7 @@ serde_json = "1.0.85" structopt = "0.3" tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros"] } tracing-flame = "0.2" -tracing-subscriber = "0.3.15" +tracing-subscriber = "0.3.15" tui-logger = "0.8.1" tuirealm = "~1.8" tui-realm-stdlib = "1.2.0" diff --git a/labs/jack-in/src/client/state.rs b/labs/jack-in/src/client/state.rs index 9c0e35901..d0a8bfacd 100644 --- a/labs/jack-in/src/client/state.rs +++ b/labs/jack-in/src/client/state.rs @@ -1,7 +1,15 @@ -use std::time::{Duration, Instant}; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; -use futures_signals::signal::Mutable; -use matrix_sdk::{ruma::OwnedRoomId, SlidingSyncView}; +use futures::{pin_mut, StreamExt}; +use futures_signals::{ + signal::Mutable, + signal_vec::{MutableVec, VecDiff}, +}; +use matrix_sdk::{room::timeline::TimelineItem, ruma::OwnedRoomId, SlidingSyncView}; +use tokio::task::JoinHandle; #[derive(Clone, Default)] pub struct CurrentRoomSummary { @@ -17,7 +25,9 @@ pub struct SlidingSyncState { /// the current list selector for the room first_render: Option, full_sync: Option, + tl_handle: Mutable>>, pub selected_room: Mutable>, + pub current_timeline: MutableVec>, } impl SlidingSyncState { @@ -27,7 +37,9 @@ impl SlidingSyncState { view, first_render: None, full_sync: None, + tl_handle: Default::default(), selected_room: Default::default(), + current_timeline: Default::default(), } } @@ -40,6 +52,49 @@ impl SlidingSyncState { } pub fn select_room(&self, r: Option) { + self.current_timeline.lock_mut().clear(); + if let Some(c) = self.tl_handle.lock_mut().take() { + c.abort(); + } + if let Some(room) = + r.as_ref().and_then(|room_id| self.view.rooms.lock_ref().get(room_id).cloned()) + { + let current_timeline = self.current_timeline.clone(); + let handle = tokio::spawn(async move { + let timeline = room.timeline().await; + let listener = timeline.stream(); + pin_mut!(listener); + while let Some(diff) = listener.next().await { + match diff { + VecDiff::Clear {} => { + current_timeline.lock_mut().clear(); + } + VecDiff::InsertAt { index, value } => { + current_timeline.lock_mut().insert_cloned(index, value); + } + VecDiff::Move { old_index, new_index } => { + current_timeline.lock_mut().move_from_to(old_index, new_index); + } + VecDiff::Pop {} => { + current_timeline.lock_mut().pop(); + } + VecDiff::Push { value } => { + current_timeline.lock_mut().push_cloned(value); + } + VecDiff::RemoveAt { index } => { + current_timeline.lock_mut().remove(index); + } + VecDiff::Replace { values } => { + current_timeline.lock_mut().replace_cloned(values); + } + VecDiff::UpdateAt { index, value } => { + current_timeline.lock_mut().set_cloned(index, value); + } + } + } + }); + *self.tl_handle.lock_mut() = Some(handle); + } self.selected_room.replace(r); } diff --git a/labs/jack-in/src/components/details.rs b/labs/jack-in/src/components/details.rs index bfea43336..dc8b2c570 100644 --- a/labs/jack-in/src/components/details.rs +++ b/labs/jack-in/src/components/details.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent}; +use chrono::{offset::Local, DateTime}; use tuirealm::{ command::{Cmd, CmdResult}, event::{Key, KeyEvent, KeyModifiers}, @@ -23,7 +23,7 @@ pub struct Details { liststate: ListState, name: Option, state_events_counts: Vec<(String, usize)>, - current_room_timeline: Vec, + current_room_timeline: Vec, } impl Details { @@ -68,15 +68,25 @@ impl Details { state_events.iter().map(|(k, l)| (k.clone(), l.len())).collect(); state_events_counts.sort_by_key(|(_, count)| *count); - let mut timeline: Vec = room_data - .timeline() + let timeline: Vec = self + .sstate + .current_timeline .lock_ref() .iter() - .filter_map(|d| d.event.deserialize().ok()) - .map(|e| e.into_full_event(room_id.clone())) + .filter_map(|t| t.as_event()) // we ignore virtual events + .filter_map(|e| e.content().as_message().map(|m| (e, m))) + .map(|(e, m)| { + format!( + "[{}] {}: {}", + e.origin_server_ts() + .and_then(|r| r.to_system_time()) + .map(|s| DateTime::::from(s).format("%Y-%m-%dT%T").to_string()) + .unwrap_or_default(), + e.sender(), + m.body() + ) + }) .collect(); - timeline.reverse(); - self.current_room_timeline = timeline; self.name = Some(name); self.state_events_counts = state_events_counts; @@ -166,28 +176,8 @@ impl MockComponent for Details { let title = (name.to_owned(), Alignment::Left); let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag(); - let mut details = vec![]; - - for e in self.current_room_timeline.iter() { - let body = { - match e { - AnyTimelineEvent::MessageLike(m) => { - if let AnyMessageLikeEvent::RoomMessage(MessageLikeEvent::Original(m)) = m { - m.content.body().to_owned() - } else { - m.event_type().to_string() - } - } - AnyTimelineEvent::State(s) => s.event_type().to_string(), - } - }; - details.push(ListItem::new(format!( - "[{}] {}: {}", - e.origin_server_ts().as_secs(), - e.sender().as_str(), - body - ))); - } + let details: Vec<_> = + self.current_room_timeline.iter().map(|e| ListItem::new(e.clone())).collect(); frame.render_stateful_widget( List::new(details) diff --git a/labs/jack-in/src/main.rs b/labs/jack-in/src/main.rs index 41e724dd3..9bad51eed 100644 --- a/labs/jack-in/src/main.rs +++ b/labs/jack-in/src/main.rs @@ -7,7 +7,9 @@ use std::path::{Path, PathBuf}; use app_dirs2::{app_root, AppDataType, AppInfo}; use dialoguer::{theme::ColorfulTheme, Password}; use eyre::{eyre, Result}; +use futures_signals::signal_vec::VecDiff; use matrix_sdk::{ + room::timeline::TimelineItem, ruma::{OwnedRoomId, OwnedUserId}, Client, }; @@ -45,6 +47,7 @@ pub enum Msg { pub enum JackInEvent { Any, // match all SyncUpdate(client::state::SlidingSyncState), + RoomDataUpdate(VecDiff), } impl PartialOrd for JackInEvent {