Move timeline API into a new crate

… aimed at interactive user interfaces.
This commit is contained in:
Jonas Platte
2023-05-16 14:12:51 +02:00
committed by Jonas Platte
parent 91d97cd588
commit cfc8effa66
49 changed files with 443 additions and 293 deletions
Generated
+28 -4
View File
@@ -1546,6 +1546,7 @@ dependencies = [
"clap 4.2.4",
"futures",
"matrix-sdk",
"matrix-sdk-ui",
"tokio",
"tracing-subscriber",
"url",
@@ -2615,7 +2616,6 @@ dependencies = [
"backoff",
"bytes",
"bytesize",
"chrono",
"ctor 0.2.0",
"dashmap",
"dirs",
@@ -2632,7 +2632,6 @@ dependencies = [
"hyper",
"image 0.24.6",
"imbl",
"indexmap",
"matrix-sdk-base",
"matrix-sdk-common",
"matrix-sdk-indexeddb",
@@ -2640,8 +2639,6 @@ dependencies = [
"matrix-sdk-test",
"mime",
"mime2ext",
"once_cell",
"pin-project-lite",
"rand 0.8.5",
"reqwest",
"ruma",
@@ -2861,6 +2858,7 @@ dependencies = [
"futures-util",
"log-panics",
"matrix-sdk",
"matrix-sdk-ui",
"mime",
"once_cell",
"opentelemetry",
@@ -3032,6 +3030,32 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "matrix-sdk-ui"
version = "0.6.0"
dependencies = [
"anyhow",
"assert_matches",
"async-trait",
"chrono",
"eyeball-im",
"futures-core",
"futures-util",
"imbl",
"indexmap",
"matrix-sdk",
"matrix-sdk-test",
"mime",
"once_cell",
"pin-project-lite",
"ruma",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "memchr"
version = "2.5.0"
+1 -2
View File
@@ -28,6 +28,7 @@ eyeball-im = { workspace = true }
extension-trait = "1.0.1"
futures-core = "0.3.17"
futures-util = { version = "0.3.17", default-features = false }
matrix-sdk-ui = { path = "../../crates/matrix-sdk-ui", features = ["experimental-sliding-sync"] }
mime = "0.3.16"
# FIXME: we currently can't feature flag anything in the api.udl, therefore we must enforce experimental-sliding-sync being exposed here..
# see https://github.com/matrix-org/matrix-rust-sdk/issues/1014
@@ -62,7 +63,6 @@ default-features = false
features = [
"anyhow",
"experimental-sliding-sync",
"experimental-timeline",
"e2e-encryption",
"markdown",
"socks",
@@ -76,7 +76,6 @@ default-features = false
features = [
"anyhow",
"experimental-sliding-sync",
"experimental-timeline",
"e2e-encryption",
"markdown",
"native-tls",
+3 -5
View File
@@ -45,10 +45,8 @@ use self::{client::Client, error::ClientError};
pub static RUNTIME: Lazy<Runtime> =
Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime"));
pub use matrix_sdk::{
room::timeline::PaginationOutcome,
ruma::{api::client::account::register, UserId},
};
pub use matrix_sdk::ruma::{api::client::account::register, UserId};
pub use matrix_sdk_ui::timeline::PaginationOutcome;
pub use platform::*;
pub use self::{
@@ -60,5 +58,5 @@ uniffi::include_scaffolding!("api");
#[uniffi::export]
pub fn sdk_git_sha() -> String {
env!("VERGEN_GIT_SHA").to_string()
env!("VERGEN_GIT_SHA").to_owned()
}
+4 -3
View File
@@ -11,7 +11,7 @@ use matrix_sdk::{
AttachmentConfig, AttachmentInfo, BaseAudioInfo, BaseFileInfo, BaseImageInfo,
BaseThumbnailInfo, BaseVideoInfo, Thumbnail,
},
room::{timeline::Timeline, Receipts, Room as SdkRoom},
room::{Receipts, Room as SdkRoom},
ruma::{
api::client::{receipt::create_receipt::v3::ReceiptType, room::report_content},
events::{
@@ -26,6 +26,7 @@ use matrix_sdk::{
},
RoomMemberships,
};
use matrix_sdk_ui::timeline::{RoomExt, Timeline};
use mime::Mime;
use tracing::error;
@@ -806,9 +807,9 @@ pub enum PaginationOptions {
UntilNumItems { event_limit: u16, items: u16 },
}
impl From<PaginationOptions> for matrix_sdk::room::timeline::PaginationOptions<'static> {
impl From<PaginationOptions> for matrix_sdk_ui::timeline::PaginationOptions<'static> {
fn from(value: PaginationOptions) -> Self {
use matrix_sdk::room::timeline::PaginationOptions as Opts;
use matrix_sdk_ui::timeline::PaginationOptions as Opts;
match value {
PaginationOptions::SingleRequest { event_limit } => Opts::single_request(event_limit),
PaginationOptions::UntilNumItems { event_limit, items } => {
+4 -3
View File
@@ -11,10 +11,11 @@ use matrix_sdk::ruma::{
assign, IdParseError, OwnedRoomId, RoomId,
};
pub use matrix_sdk::{
room::timeline::Timeline, ruma::api::client::sync::sync_events::v4::SyncRequestListFilters,
Client as MatrixClient, LoopCtrl, RoomListEntry as MatrixRoomEntry,
SlidingSyncBuilder as MatrixSlidingSyncBuilder, SlidingSyncMode, SlidingSyncState,
ruma::api::client::sync::sync_events::v4::SyncRequestListFilters, Client as MatrixClient,
LoopCtrl, RoomListEntry as MatrixRoomEntry, SlidingSyncBuilder as MatrixSlidingSyncBuilder,
SlidingSyncMode, SlidingSyncState,
};
use matrix_sdk_ui::timeline::SlidingSyncRoomExt;
use tokio::task::JoinHandle;
use tracing::{debug, error, warn};
use url::Url;
+29 -30
View File
@@ -3,11 +3,11 @@ use std::{collections::HashMap, sync::Arc, time::Duration};
use anyhow::bail;
use extension_trait::extension_trait;
use eyeball_im::VectorDiff;
pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource};
use matrix_sdk::{
attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo},
room::timeline::{Profile, TimelineDetails},
use matrix_sdk::attachment::{
BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseThumbnailInfo, BaseVideoInfo,
};
pub use matrix_sdk::ruma::events::room::{message::RoomMessageEventContent, MediaSource};
use matrix_sdk_ui::timeline::{Profile, TimelineDetails};
use ruma::UInt;
use tracing::warn;
@@ -45,7 +45,7 @@ pub enum TimelineDiff {
}
impl TimelineDiff {
pub(crate) fn new(inner: VectorDiff<Arc<matrix_sdk::room::timeline::TimelineItem>>) -> Self {
pub(crate) fn new(inner: VectorDiff<Arc<matrix_sdk_ui::timeline::TimelineItem>>) -> Self {
match inner {
VectorDiff::Append { values } => {
Self::Append { values: values.into_iter().map(TimelineItem::from_arc).collect() }
@@ -184,10 +184,10 @@ pub enum TimelineChange {
#[repr(transparent)]
#[derive(Clone)]
pub struct TimelineItem(pub(crate) matrix_sdk::room::timeline::TimelineItem);
pub struct TimelineItem(pub(crate) matrix_sdk_ui::timeline::TimelineItem);
impl TimelineItem {
pub(crate) fn from_arc(arc: Arc<matrix_sdk::room::timeline::TimelineItem>) -> Arc<Self> {
pub(crate) fn from_arc(arc: Arc<matrix_sdk_ui::timeline::TimelineItem>) -> Arc<Self> {
// SAFETY: This is valid because Self is a repr(transparent) wrapper
// around the other Timeline type.
unsafe { Arc::from_raw(Arc::into_raw(arc) as _) }
@@ -197,14 +197,14 @@ impl TimelineItem {
#[uniffi::export]
impl TimelineItem {
pub fn as_event(self: Arc<Self>) -> Option<Arc<EventTimelineItem>> {
use matrix_sdk::room::timeline::TimelineItem as Item;
use matrix_sdk_ui::timeline::TimelineItem as Item;
unwrap_or_clone_arc_into_variant!(self, .0, Item::Event(evt) => {
Arc::new(EventTimelineItem(evt))
})
}
pub fn as_virtual(self: Arc<Self>) -> Option<VirtualTimelineItem> {
use matrix_sdk::room::timeline::{TimelineItem as Item, VirtualTimelineItem as VItem};
use matrix_sdk_ui::timeline::{TimelineItem as Item, VirtualTimelineItem as VItem};
match &self.0 {
Item::Virtual(VItem::DayDivider(ts)) => {
Some(VirtualTimelineItem::DayDivider { ts: ts.0.into() })
@@ -233,9 +233,9 @@ pub enum EventSendState {
Sent { event_id: String },
}
impl From<&matrix_sdk::room::timeline::EventSendState> for EventSendState {
fn from(value: &matrix_sdk::room::timeline::EventSendState) -> Self {
use matrix_sdk::room::timeline::EventSendState::*;
impl From<&matrix_sdk_ui::timeline::EventSendState> for EventSendState {
fn from(value: &matrix_sdk_ui::timeline::EventSendState) -> Self {
use matrix_sdk_ui::timeline::EventSendState::*;
match value {
NotSentYet => Self::NotSentYet,
@@ -246,7 +246,7 @@ impl From<&matrix_sdk::room::timeline::EventSendState> for EventSendState {
}
#[derive(uniffi::Object)]
pub struct EventTimelineItem(pub(crate) matrix_sdk::room::timeline::EventTimelineItem);
pub struct EventTimelineItem(pub(crate) matrix_sdk_ui::timeline::EventTimelineItem);
#[uniffi::export]
impl EventTimelineItem {
@@ -357,12 +357,12 @@ impl From<&TimelineDetails<Profile>> for ProfileDetails {
}
#[derive(Clone, uniffi::Object)]
pub struct TimelineItemContent(matrix_sdk::room::timeline::TimelineItemContent);
pub struct TimelineItemContent(matrix_sdk_ui::timeline::TimelineItemContent);
#[uniffi::export]
impl TimelineItemContent {
pub fn kind(&self) -> TimelineItemContentKind {
use matrix_sdk::room::timeline::TimelineItemContent as Content;
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
match &self.0 {
Content::Message(_) => TimelineItemContentKind::Message,
@@ -424,7 +424,7 @@ impl TimelineItemContent {
}
pub fn as_message(self: Arc<Self>) -> Option<Arc<Message>> {
use matrix_sdk::room::timeline::TimelineItemContent as Content;
use matrix_sdk_ui::timeline::TimelineItemContent as Content;
unwrap_or_clone_arc_into_variant!(self, .0, Content::Message(msg) => Arc::new(Message(msg)))
}
}
@@ -467,7 +467,7 @@ pub enum TimelineItemContentKind {
}
#[derive(Clone, uniffi::Object)]
pub struct Message(matrix_sdk::room::timeline::Message);
pub struct Message(matrix_sdk_ui::timeline::Message);
#[uniffi::export]
impl Message {
@@ -838,8 +838,8 @@ pub struct InReplyToDetails {
event: RepliedToEventDetails,
}
impl From<&matrix_sdk::room::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk::room::timeline::InReplyToDetails) -> Self {
impl From<&matrix_sdk_ui::timeline::InReplyToDetails> for InReplyToDetails {
fn from(inner: &matrix_sdk_ui::timeline::InReplyToDetails) -> Self {
let event_id = inner.event_id.to_string();
let event = match &inner.event {
TimelineDetails::Unavailable => RepliedToEventDetails::Unavailable,
@@ -882,8 +882,8 @@ pub enum EncryptedMessage {
}
impl EncryptedMessage {
fn new(msg: &matrix_sdk::room::timeline::EncryptedMessage) -> Self {
use matrix_sdk::room::timeline::EncryptedMessage as Message;
fn new(msg: &matrix_sdk_ui::timeline::EncryptedMessage) -> Self {
use matrix_sdk_ui::timeline::EncryptedMessage as Message;
match msg {
Message::OlmV1Curve25519AesSha2 { sender_key } => {
@@ -975,9 +975,9 @@ pub enum MembershipChange {
NotImplemented,
}
impl From<matrix_sdk::room::timeline::MembershipChange> for MembershipChange {
fn from(membership_change: matrix_sdk::room::timeline::MembershipChange) -> Self {
use matrix_sdk::room::timeline::MembershipChange as Change;
impl From<matrix_sdk_ui::timeline::MembershipChange> for MembershipChange {
fn from(membership_change: matrix_sdk_ui::timeline::MembershipChange) -> Self {
use matrix_sdk_ui::timeline::MembershipChange as Change;
match membership_change {
Change::None => Self::None,
Change::Error => Self::Error,
@@ -1025,12 +1025,11 @@ pub enum OtherState {
Custom { event_type: String },
}
impl From<&matrix_sdk::room::timeline::AnyOtherFullStateEventContent> for OtherState {
fn from(content: &matrix_sdk::room::timeline::AnyOtherFullStateEventContent) -> Self {
use matrix_sdk::{
room::timeline::AnyOtherFullStateEventContent as Content,
ruma::events::FullStateEventContent as FullContent,
};
impl From<&matrix_sdk_ui::timeline::AnyOtherFullStateEventContent> for OtherState {
fn from(content: &matrix_sdk_ui::timeline::AnyOtherFullStateEventContent) -> Self {
use matrix_sdk::ruma::events::FullStateEventContent as FullContent;
use matrix_sdk_ui::timeline::AnyOtherFullStateEventContent as Content;
match content {
Content::PolicyRuleRoom(_) => Self::PolicyRuleRoom,
Content::PolicyRuleServer(_) => Self::PolicyRuleServer,
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "matrix-sdk-ui"
version = "0.6.0"
edition = "2021"
[features]
default = ["e2e-encryption"]
e2e-encryption = ["matrix-sdk/e2e-encryption"]
experimental-sliding-sync = ["matrix-sdk/experimental-sliding-sync"]
[dependencies]
async-trait = { workspace = true }
chrono = "0.4.23"
eyeball-im = { workspace = true }
futures-core = "0.3.21"
futures-util = { workspace = true }
imbl = { version = "2.0.0", features = ["serde"] }
indexmap = "1.9.1"
matrix-sdk = { version = "0.6.2", path = "../matrix-sdk", default-features = false }
mime = "0.3.16"
once_cell = { workspace = true }
pin-project-lite = "0.2.9"
ruma = { workspace = true, features = ["unstable-msc2677", "unstable-sanitize"] }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }
[dev-dependencies]
anyhow = { workspace = true }
assert_matches = { workspace = true }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
mod events;
pub mod timeline;
pub use self::timeline::Timeline;
@@ -15,7 +15,10 @@
use std::sync::Arc;
use imbl::Vector;
use matrix_sdk_base::deserialized_responses::{EncryptionInfo, SyncTimelineEvent};
use matrix_sdk::{
deserialized_responses::{EncryptionInfo, SyncTimelineEvent},
room,
};
use ruma::{
events::receipt::{ReceiptThread, ReceiptType, SyncReceiptEvent},
push::Action,
@@ -26,7 +29,6 @@ use tracing::error;
#[cfg(feature = "e2e-encryption")]
use super::to_device::{handle_forwarded_room_key_event, handle_room_key_event};
use super::{inner::TimelineInner, Timeline, TimelineEventHandlerHandles};
use crate::room;
/// Builder that allows creating and configuring various parts of a
/// [`Timeline`].
@@ -120,6 +122,7 @@ impl TimelineBuilder {
let inner = Arc::new(inner);
let room = inner.room();
let client = room.client();
let timeline_event_handle = room.add_event_handler({
let inner = inner.clone();
@@ -133,14 +136,15 @@ impl TimelineBuilder {
// Not using room.add_event_handler here because RoomKey events are
// to-device events that are not received in the context of a room.
#[cfg(feature = "e2e-encryption")]
let room_key_handle = room
.client
let room_key_handle = client
.add_event_handler(handle_room_key_event(inner.clone(), room.room_id().to_owned()));
#[cfg(feature = "e2e-encryption")]
let forwarded_room_key_handle = room.client.add_event_handler(
handle_forwarded_room_key_event(inner.clone(), room.room_id().to_owned()),
);
let forwarded_room_key_handle = client.add_event_handler(handle_forwarded_room_key_event(
inner.clone(),
room.room_id().to_owned(),
));
let mut handles = vec![
timeline_event_handle,
@@ -176,7 +180,6 @@ impl TimelineBuilder {
handles.push(read_receipts_handle);
}
let client = room.client.clone();
let timeline = Timeline {
inner,
start_token: Mutex::new(prev_token),
@@ -17,7 +17,7 @@ use std::{collections::HashMap, sync::Arc};
use chrono::{Datelike, Local, TimeZone};
use eyeball_im::{ObservableVector, Vector};
use indexmap::{map::Entry, IndexMap, IndexSet};
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
use matrix_sdk::deserialized_responses::EncryptionInfo;
use ruma::{
events::{
reaction::ReactionEventContent,
@@ -49,13 +49,11 @@ use super::{
},
find_read_marker,
read_receipts::maybe_add_implicit_read_receipt,
rfind_event_by_id, rfind_event_item, EventTimelineItem, Message, ReactionGroup,
TimelineDetails, TimelineInnerState, TimelineItem, TimelineItemContent, VirtualTimelineItem,
};
use crate::{
events::SyncTimelineEventWithoutContent,
room::timeline::{MembershipChange, DEFAULT_SANITIZER_MODE},
rfind_event_by_id, rfind_event_item, EventTimelineItem, MembershipChange, Message,
ReactionGroup, TimelineDetails, TimelineInnerState, TimelineItem, TimelineItemContent,
VirtualTimelineItem, DEFAULT_SANITIZER_MODE,
};
use crate::events::SyncTimelineEventWithoutContent;
pub(super) enum Flow {
Local {
@@ -2,7 +2,7 @@ use std::{fmt, ops::Deref, sync::Arc};
use imbl::{vector, Vector};
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::TimelineEvent;
use matrix_sdk::{deserialized_responses::TimelineEvent, Result};
use ruma::{
events::{
policy::rule::{
@@ -43,11 +43,8 @@ use ruma::{
use tracing::{debug, error};
use super::{EventTimelineItem, Profile, TimelineDetails};
use crate::{
room::timeline::{
inner::RoomDataProvider, Error as TimelineError, TimelineItem, DEFAULT_SANITIZER_MODE,
},
Result,
use crate::timeline::{
inner::RoomDataProvider, Error as TimelineError, TimelineItem, DEFAULT_SANITIZER_MODE,
};
/// The content of an [`EventTimelineItem`][super::EventTimelineItem].
@@ -123,14 +120,14 @@ impl TimelineItemContent {
/// An `m.room.message` event or extensible event, including edits.
#[derive(Clone)]
pub struct Message {
pub(in crate::room::timeline) msgtype: MessageType,
pub(in crate::room::timeline) in_reply_to: Option<InReplyToDetails>,
pub(in crate::room::timeline) edited: bool,
pub(in crate::timeline) msgtype: MessageType,
pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
pub(in crate::timeline) edited: bool,
}
impl Message {
/// Construct a `Message` from a `m.room.message` event.
pub(in crate::room::timeline) fn from_event(
pub(in crate::timeline) fn from_event(
c: RoomMessageEventContent,
relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
timeline_items: &Vector<Arc<TimelineItem>>,
@@ -216,10 +213,7 @@ impl Message {
self.edited
}
pub(in crate::room::timeline) fn with_in_reply_to(
&self,
in_reply_to: InReplyToDetails,
) -> Self {
pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
Self { in_reply_to: Some(in_reply_to), ..self.clone() }
}
}
@@ -248,16 +242,16 @@ pub struct InReplyToDetails {
/// Use [`Timeline::fetch_details_for_event`] to fetch the data if it is
/// unavailable.
///
/// [`Timeline::fetch_details_for_event`]: crate::room::timeline::Timeline::fetch_details_for_event
/// [`Timeline::fetch_details_for_event`]: crate::Timeline::fetch_details_for_event
pub event: TimelineDetails<Box<RepliedToEvent>>,
}
/// An event that is replied to.
#[derive(Clone, Debug)]
pub struct RepliedToEvent {
pub(in crate::room::timeline) message: Message,
pub(in crate::room::timeline) sender: OwnedUserId,
pub(in crate::room::timeline) sender_profile: TimelineDetails<Profile>,
pub(in crate::timeline) message: Message,
pub(in crate::timeline) sender: OwnedUserId,
pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
}
impl RepliedToEvent {
@@ -293,19 +287,19 @@ impl RepliedToEvent {
})
}
pub(in crate::room::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
timeline_event: TimelineEvent,
room_data_provider: &P,
) -> Result<Self> {
) -> Result<Self, TimelineError> {
let event = match timeline_event.event.deserialize() {
Ok(AnyTimelineEvent::MessageLike(event)) => event,
_ => {
return Err(TimelineError::UnsupportedEvent.into());
return Err(TimelineError::UnsupportedEvent);
}
};
let Some(AnyMessageLikeEventContent::RoomMessage(c)) = event.original_content() else {
return Err(TimelineError::UnsupportedEvent.into());
return Err(TimelineError::UnsupportedEvent);
};
let message = Message::from_event(c, event.relations(), &vector![]);
@@ -376,7 +370,7 @@ type ReactionGroupInner = IndexMap<(Option<OwnedTransactionId>, Option<OwnedEven
/// This is a map of the event ID or transaction ID of the reactions to the ID
/// of the sender of the reaction.
#[derive(Clone, Debug, Default)]
pub struct ReactionGroup(pub(in crate::room::timeline) ReactionGroupInner);
pub struct ReactionGroup(pub(in crate::timeline) ReactionGroupInner);
impl ReactionGroup {
/// The senders of the reactions in this group.
@@ -396,7 +390,7 @@ impl Deref for ReactionGroup {
/// An `m.sticker` event.
#[derive(Clone, Debug)]
pub struct Sticker {
pub(in crate::room::timeline) content: StickerEventContent,
pub(in crate::timeline) content: StickerEventContent,
}
impl Sticker {
@@ -409,9 +403,9 @@ impl Sticker {
/// An event changing a room membership.
#[derive(Clone, Debug)]
pub struct RoomMembershipChange {
pub(in crate::room::timeline) user_id: OwnedUserId,
pub(in crate::room::timeline) content: FullStateEventContent<RoomMemberEventContent>,
pub(in crate::room::timeline) change: Option<MembershipChange>,
pub(in crate::timeline) user_id: OwnedUserId,
pub(in crate::timeline) content: FullStateEventContent<RoomMemberEventContent>,
pub(in crate::timeline) change: Option<MembershipChange>,
}
impl RoomMembershipChange {
@@ -498,9 +492,9 @@ pub enum MembershipChange {
/// membership is already `join`.
#[derive(Clone, Debug)]
pub struct MemberProfileChange {
pub(in crate::room::timeline) user_id: OwnedUserId,
pub(in crate::room::timeline) displayname_change: Option<Change<Option<String>>>,
pub(in crate::room::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
pub(in crate::timeline) user_id: OwnedUserId,
pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
}
impl MemberProfileChange {
@@ -654,8 +648,8 @@ impl AnyOtherFullStateEventContent {
/// A state event that doesn't have its own variant.
#[derive(Clone, Debug)]
pub struct OtherState {
pub(in crate::room::timeline) state_key: String,
pub(in crate::room::timeline) content: AnyOtherFullStateEventContent,
pub(in crate::timeline) state_key: String,
pub(in crate::timeline) content: AnyOtherFullStateEventContent,
}
impl OtherState {
@@ -5,7 +5,7 @@ use super::EventSendState;
/// An item for an event that was created locally and not yet echoed back by
/// the homeserver.
#[derive(Debug, Clone)]
pub(in crate::room::timeline) struct LocalEventTimelineItem {
pub(in crate::timeline) struct LocalEventTimelineItem {
/// The send state of this local event.
pub send_state: EventSendState,
/// The transaction ID.
@@ -15,7 +15,7 @@
use std::sync::Arc;
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
use matrix_sdk::{deserialized_responses::EncryptionInfo, Error};
use once_cell::sync::Lazy;
use ruma::{
events::{receipt::Receipt, room::message::MessageType, AnySyncTimelineEvent},
@@ -24,8 +24,6 @@ use ruma::{
UserId,
};
use crate::Error;
mod content;
mod local;
mod remote;
@@ -1,7 +1,7 @@
use std::fmt;
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::EncryptionInfo;
use matrix_sdk::deserialized_responses::EncryptionInfo;
use ruma::{
events::{receipt::Receipt, AnySyncTimelineEvent},
serde::Raw,
@@ -12,7 +12,7 @@ use super::BundledReactions;
/// An item for an event that was received from the homeserver.
#[derive(Clone)]
pub(in crate::room::timeline) struct RemoteEventTimelineItem {
pub(in crate::timeline) struct RemoteEventTimelineItem {
/// The event ID.
pub event_id: OwnedEventId,
/// All bundled reactions about the event.
@@ -66,7 +66,7 @@ impl RemoteEventTimelineItem {
/// Where we got an event from.
#[derive(Clone, Copy, Debug)]
pub(in crate::room::timeline) enum RemoteEventOrigin {
pub(in crate::timeline) enum RemoteEventOrigin {
/// The event came from a cache.
Cache,
/// The event came from a sync response.
@@ -20,10 +20,13 @@ use async_trait::async_trait;
use eyeball_im::{ObservableVector, VectorSubscriber};
use imbl::Vector;
use indexmap::{IndexMap, IndexSet};
#[cfg(feature = "e2e-encryption")]
use matrix_sdk_base::crypto::OlmMachine;
use matrix_sdk_base::deserialized_responses::{EncryptionInfo, SyncTimelineEvent, TimelineEvent};
#[cfg(feature = "e2e-encryption")]
#[cfg(all(test, feature = "e2e-encryption"))]
use matrix_sdk::crypto::OlmMachine;
use matrix_sdk::{
deserialized_responses::{EncryptionInfo, SyncTimelineEvent, TimelineEvent},
room, Error, Result,
};
#[cfg(all(test, feature = "e2e-encryption"))]
use ruma::RoomId;
use ruma::{
api::client::receipt::create_receipt::v3::ReceiptType as SendReceiptType,
@@ -57,7 +60,7 @@ use super::{
Message, Profile, RelativePosition, RepliedToEvent, TimelineDetails, TimelineItem,
TimelineItemContent,
};
use crate::{events::SyncTimelineEventWithoutContent, room, Error, Result};
use crate::events::SyncTimelineEventWithoutContent;
#[derive(Debug)]
pub(super) struct TimelineInner<P: RoomDataProvider = room::Common> {
@@ -347,12 +350,30 @@ impl<P: RoomDataProvider> TimelineInner<P> {
}
#[cfg(feature = "e2e-encryption")]
#[instrument(skip(self, olm_machine))]
#[instrument(skip(self, room), fields(room_id = ?room.room_id()))]
pub(super) async fn retry_event_decryption(
&self,
room: &room::Common,
session_ids: Option<BTreeSet<&str>>,
) {
self.retry_event_decryption_inner(room, session_ids).await
}
#[cfg(all(test, feature = "e2e-encryption"))]
pub(super) async fn retry_event_decryption_test(
&self,
room_id: &RoomId,
olm_machine: &OlmMachine,
session_ids: Option<BTreeSet<&str>>,
) {
self.retry_event_decryption_inner((olm_machine, room_id), session_ids).await
}
#[cfg(feature = "e2e-encryption")]
async fn retry_event_decryption_inner(
&self,
decryptor: impl Decryptor,
session_ids: Option<BTreeSet<&str>>,
) {
use super::EncryptedMessage;
@@ -392,8 +413,7 @@ impl<P: RoomDataProvider> TimelineInner<P> {
tracing::Span::current().record("event_id", debug(&remote_event.event_id));
let raw = remote_event.original_json.cast_ref();
match olm_machine.decrypt_room_event(raw, room_id).await {
match decryptor.decrypt_event_impl(&remote_event.original_json).await {
Ok(event) => {
trace!("Successfully decrypted event that previously failed to decrypt");
Some(event)
@@ -544,7 +564,10 @@ impl TimelineInner {
}
#[instrument(skip(self))]
pub(super) async fn fetch_in_reply_to_details(&self, event_id: &EventId) -> Result<()> {
pub(super) async fn fetch_in_reply_to_details(
&self,
event_id: &EventId,
) -> Result<(), super::Error> {
let state = self.state.lock().await;
let (index, item) = rfind_event_by_id(&state.items, event_id)
.ok_or(super::Error::RemoteEventNotInTimeline)?;
@@ -572,7 +595,7 @@ impl TimelineInner {
&in_reply_to.event_id,
self.room(),
)
.await;
.await?;
// We need to be sure to have the latest position of the event as it might have
// changed while waiting for the request.
@@ -686,7 +709,7 @@ async fn fetch_replied_to_event(
message: &Message,
in_reply_to: &EventId,
room: &room::Common,
) -> TimelineDetails<Box<RepliedToEvent>> {
) -> Result<TimelineDetails<Box<RepliedToEvent>>, super::Error> {
if let Some((_, item)) = rfind_event_by_id(&state.items, in_reply_to) {
let details = match item.content() {
TimelineItemContent::Message(message) => {
@@ -696,11 +719,11 @@ async fn fetch_replied_to_event(
sender_profile: item.sender_profile().clone(),
}))
}
_ => TimelineDetails::Error(Arc::new(super::Error::UnsupportedEvent.into())),
_ => return Err(super::Error::UnsupportedEvent),
};
debug!("Found replied-to event locally");
return details;
return Ok(details);
};
trace!("Setting in-reply-to details to pending");
@@ -715,15 +738,13 @@ async fn fetch_replied_to_event(
drop(state);
trace!("Fetching replied-to event");
match room.event(in_reply_to).await {
Ok(timeline_event) => {
match RepliedToEvent::try_from_timeline_event(timeline_event, room).await {
Ok(event) => TimelineDetails::Ready(Box::new(event)),
Err(e) => TimelineDetails::Error(Arc::new(e)),
}
}
let res = match room.event(in_reply_to).await {
Ok(timeline_event) => TimelineDetails::Ready(Box::new(
RepliedToEvent::try_from_timeline_event(timeline_event, room).await?,
)),
Err(e) => TimelineDetails::Error(Arc::new(e)),
}
};
Ok(res)
}
#[async_trait]
@@ -850,3 +871,27 @@ async fn handle_remote_event<P: RoomDataProvider>(
TimelineEventHandler::new(event_meta, flow, timeline_state, track_read_receipts)
.handle_event(event_kind)
}
// Internal helper to make most of retry_event_decryption independent of a room
// object, which is annoying to create for testing and not really needed
#[async_trait]
trait Decryptor: Copy {
async fn decrypt_event_impl(&self, raw: &Raw<AnySyncTimelineEvent>) -> Result<TimelineEvent>;
}
#[async_trait]
impl Decryptor for &room::Common {
async fn decrypt_event_impl(&self, raw: &Raw<AnySyncTimelineEvent>) -> Result<TimelineEvent> {
self.decrypt_event(raw.cast_ref()).await
}
}
#[cfg(all(test, feature = "e2e-encryption"))]
#[async_trait]
impl Decryptor for (&OlmMachine, &RoomId) {
async fn decrypt_event_impl(&self, raw: &Raw<AnySyncTimelineEvent>) -> Result<TimelineEvent> {
let (olm_machine, room_id) = self;
let event = olm_machine.decrypt_room_event(raw.cast_ref(), room_id).await?;
Ok(event)
}
}
@@ -22,6 +22,12 @@ use eyeball_im::{VectorDiff, VectorSubscriber};
use futures_core::Stream;
use futures_util::TryFutureExt;
use imbl::Vector;
use matrix_sdk::{
attachment::AttachmentConfig,
event_handler::EventHandlerHandle,
room::{self, MessagesOptions, Receipts, Room},
Client, Result,
};
use mime::Mime;
use pin_project_lite::pin_project;
use ruma::{
@@ -38,20 +44,15 @@ use thiserror::Error;
use tokio::sync::Mutex;
use tracing::{error, instrument, warn};
use super::{Joined, Receipts};
use crate::{
attachment::AttachmentConfig,
event_handler::EventHandlerHandle,
room::{self, MessagesOptions},
Client, Result,
};
mod builder;
mod event_handler;
mod event_item;
mod inner;
mod pagination;
mod read_receipts;
mod room_ext;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync_ext;
#[cfg(test)]
mod tests;
#[cfg(feature = "e2e-encryption")]
@@ -60,6 +61,8 @@ mod virtual_item;
pub(crate) use self::builder::TimelineBuilder;
use self::inner::{TimelineInner, TimelineInnerState};
#[cfg(feature = "experimental-sliding-sync")]
pub use self::sliding_sync_ext::SlidingSyncRoomExt;
pub use self::{
event_item::{
AnyOtherFullStateEventContent, BundledReactions, EncryptedMessage, EventSendState,
@@ -68,6 +71,7 @@ pub use self::{
TimelineDetails, TimelineItemContent,
},
pagination::{PaginationOptions, PaginationOutcome},
room_ext::RoomExt,
virtual_item::VirtualTimelineItem,
};
@@ -180,10 +184,8 @@ impl Timeline {
///
/// ```no_run
/// # use std::{path::PathBuf, time::Duration};
/// # use matrix_sdk::{
/// # Client, config::SyncSettings,
/// # room::timeline::Timeline, ruma::room_id,
/// # };
/// # use matrix_sdk::{Client, config::SyncSettings, ruma::room_id};
/// # use matrix_sdk_ui::Timeline;
/// # async {
/// # let mut client: Client = todo!();
/// # let room_id = ruma::room_id!("!example:example.org");
@@ -206,8 +208,7 @@ impl Timeline {
) {
self.inner
.retry_event_decryption(
self.room().room_id(),
self.room().client.olm_machine().expect("Olm machine wasn't started"),
self.room(),
Some(session_ids.into_iter().map(AsRef::as_ref).collect()),
)
.await;
@@ -216,13 +217,7 @@ impl Timeline {
#[cfg(feature = "e2e-encryption")]
#[tracing::instrument(skip(self))]
async fn retry_decryption_for_all_events(&self) {
self.inner
.retry_event_decryption(
self.room().room_id(),
self.room().client.olm_machine().expect("Olm machine wasn't started"),
None,
)
.await;
self.inner.retry_event_decryption(self.room(), None).await;
}
/// Get the current list of timeline items. Do not use this in production!
@@ -282,16 +277,23 @@ impl Timeline {
let txn_id = txn_id.map_or_else(TransactionId::new, ToOwned::to_owned);
self.inner.handle_local_event(txn_id.clone(), content.clone()).await;
// If this room isn't actually in joined state, we'll get a server error.
// Not ideal, but works for now.
let room = Joined { inner: self.room().clone() };
let send_state = match Room::from(self.room().clone()) {
Room::Joined(room) => {
let response = room.send(content, Some(&txn_id)).await;
let response = room.send(content, Some(&txn_id)).await;
let send_state = match response {
Ok(response) => EventSendState::Sent { event_id: response.event_id },
Err(error) => EventSendState::SendingFailed { error: Arc::new(error) },
match response {
Ok(response) => EventSendState::Sent { event_id: response.event_id },
Err(error) => EventSendState::SendingFailed { error: Arc::new(error) },
}
}
_ => {
EventSendState::SendingFailed {
// FIXME: Probably not exactly right
error: Arc::new(matrix_sdk::Error::InconsistentState),
}
}
};
self.inner.update_event_send_state(&txn_id, send_state).await;
}
@@ -316,9 +318,9 @@ impl Timeline {
mime_type: Mime,
config: AttachmentConfig,
) -> Result<(), Error> {
// If this room isn't actually in joined state, we'll get a server error.
// Not ideal, but works for now.
let room = Joined { inner: self.room().clone() };
let Room::Joined(room) = Room::from(self.room().clone()) else {
return Err(Error::RoomNotJoined);
};
let body =
Path::new(&url).file_name().ok_or(Error::InvalidAttachmentFileName)?.to_str().unwrap();
@@ -351,7 +353,7 @@ impl Timeline {
/// echo in the timeline, or if the event is removed from the timeline
/// before all requests are handled.
#[instrument(skip(self), fields(room_id = ?self.room().room_id()))]
pub async fn fetch_details_for_event(&self, event_id: &EventId) -> Result<()> {
pub async fn fetch_details_for_event(&self, event_id: &EventId) -> Result<(), Error> {
self.inner.fetch_in_reply_to_details(event_id).await
}
@@ -377,7 +379,7 @@ impl Timeline {
/// Get the latest read receipt for the given user.
///
/// Contrary to [`Common::user_receipt()`](super::Common::user_receipt) that
/// Contrary to [`Common::user_receipt()`](room::Common::user_receipt) that
/// only keeps track of read receipts received from the homeserver, this
/// keeps also track of implicit read receipts in this timeline, i.e.
/// when a room member sends an event.
@@ -394,6 +396,8 @@ impl Timeline {
/// This uses [`Joined::send_single_receipt`] internally, but checks
/// first if the receipt points to an event in this timeline that is more
/// recent than the current ones, to avoid unnecessary requests.
///
/// [`Joined::send_single_receipt`]: room::Joined::send_single_receipt
#[instrument(skip(self))]
pub async fn send_single_receipt(
&self,
@@ -405,9 +409,10 @@ impl Timeline {
return Ok(());
}
// If this room isn't actually in joined state, we'll get a server error.
// Not ideal, but works for now.
let room = Joined { inner: self.room().clone() };
let Room::Joined(room) = Room::from(self.room().clone()) else {
// FIXME: Probably not exactly right
return Err(matrix_sdk::Error::InconsistentState);
};
room.send_single_receipt(receipt_type, thread, event_id).await
}
@@ -417,6 +422,8 @@ impl Timeline {
/// This uses [`Joined::send_multiple_receipts`] internally, but checks
/// first if the receipts point to events in this timeline that are more
/// recent than the current ones, to avoid unnecessary requests.
///
/// [`Joined::send_multiple_receipts`]: room::Joined::send_multiple_receipts
#[instrument(skip(self))]
pub async fn send_multiple_receipts(&self, mut receipts: Receipts) -> Result<()> {
if let Some(fully_read) = &receipts.fully_read {
@@ -433,13 +440,13 @@ impl Timeline {
}
}
if let Some(read_receipt) = &receipts.read_receipt {
if let Some(read_receipt) = &receipts.public_read_receipt {
if !self
.inner
.should_send_receipt(&ReceiptType::Read, &ReceiptThread::Unthreaded, read_receipt)
.await
{
receipts.read_receipt = None;
receipts.public_read_receipt = None;
}
}
@@ -457,9 +464,10 @@ impl Timeline {
}
}
// If this room isn't actually in joined state, we'll get a server error.
// Not ideal, but works for now.
let room = Joined { inner: self.room().clone() };
let Room::Joined(room) = Room::from(self.room().clone()) else {
// FIXME: Probably not exactly right
return Err(matrix_sdk::Error::InconsistentState);
};
room.send_multiple_receipts(receipts).await
}
@@ -633,6 +641,10 @@ pub enum Error {
/// The attachment could not be sent
#[error("Failed sending attachment")]
FailedSendingAttachment,
/// The room is not in a joined state.
#[error("Room is not joined")]
RoomNotJoined,
}
/// Result of comparing events position in the timeline.
@@ -16,6 +16,7 @@ use std::{collections::HashMap, sync::Arc};
use eyeball_im::ObservableVector;
use indexmap::IndexMap;
use matrix_sdk::room;
use ruma::{
events::receipt::{Receipt, ReceiptEventContent, ReceiptThread, ReceiptType},
EventId, OwnedEventId, OwnedUserId, UserId,
@@ -28,7 +29,6 @@ use super::{
inner::{RoomDataProvider, TimelineInnerState},
rfind_event_by_id, EventTimelineItem, RelativePosition, TimelineItem,
};
use crate::room;
struct FullReceipt<'a> {
event_id: &'a EventId,
@@ -0,0 +1,35 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use async_trait::async_trait;
use matrix_sdk::room;
use crate::timeline::Timeline;
#[async_trait]
pub trait RoomExt {
/// Get a [`Timeline`] for this room.
///
/// This offers a higher-level API than event handlers, in treating things
/// like edits and reactions as updates of existing items rather than new
/// independent events.
async fn timeline(&self) -> Timeline;
}
#[async_trait]
impl RoomExt for room::Common {
async fn timeline(&self) -> Timeline {
Timeline::builder(self).track_read_marker_and_receipts().build().await
}
}
@@ -0,0 +1,54 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use async_trait::async_trait;
use matrix_sdk::SlidingSyncRoom;
use tracing::{error, instrument};
use super::{EventTimelineItem, Timeline, TimelineBuilder};
#[async_trait]
pub trait SlidingSyncRoomExt {
/// Get a `Timeline` for this room.
async fn timeline(&self) -> Option<Timeline>;
/// Get the latest timeline item of this room.
///
/// Use `Timeline::latest_event` instead if you already have a timeline for
/// this `SlidingSyncRoom`.
async fn latest_event(&self) -> Option<EventTimelineItem>;
}
#[async_trait]
impl SlidingSyncRoomExt for SlidingSyncRoom {
async fn timeline(&self) -> Option<Timeline> {
Some(sliding_sync_timeline_builder(self)?.track_read_marker_and_receipts().build().await)
}
#[instrument(skip_all)]
async fn latest_event(&self) -> Option<EventTimelineItem> {
sliding_sync_timeline_builder(self)?.build().await.latest_event().await
}
}
fn sliding_sync_timeline_builder(room: &SlidingSyncRoom) -> Option<TimelineBuilder> {
let room_id = room.room_id();
match room.client().get_room(room_id) {
Some(r) => Some(Timeline::builder(&r).events(room.prev_batch(), room.timeline_queue())),
None => {
error!(?room_id, "Room not found in client. Can't provide a timeline for it");
None
}
}
}
@@ -16,7 +16,7 @@ use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use imbl::vector;
use matrix_sdk_base::deserialized_responses::SyncTimelineEvent;
use matrix_sdk::deserialized_responses::SyncTimelineEvent;
use matrix_sdk_test::async_test;
use ruma::{
assign,
@@ -34,7 +34,7 @@ use ruma::{
use serde_json::{json, Value as JsonValue};
use super::{TestTimeline, ALICE, BOB};
use crate::room::timeline::{
use crate::timeline::{
event_item::AnyOtherFullStateEventContent, MembershipChange, TimelineDetails, TimelineItem,
TimelineItemContent, VirtualTimelineItem,
};
@@ -17,6 +17,7 @@ use std::{io, sync::Arc};
use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk::Error;
use matrix_sdk_test::async_test;
use ruma::{
event_id,
@@ -25,7 +26,7 @@ use ruma::{
use serde_json::json;
use super::{TestTimeline, ALICE, BOB};
use crate::{room::timeline::event_item::EventSendState, Error};
use crate::timeline::event_item::EventSendState;
#[async_test]
async fn remote_echo_full_trip() {
@@ -30,7 +30,7 @@ use ruma::{
use serde_json::json;
use super::{TestTimeline, ALICE};
use crate::room::timeline::TimelineItemContent;
use crate::timeline::TimelineItemContent;
#[async_test]
async fn live_redacted() {
@@ -19,7 +19,7 @@ use std::{collections::BTreeSet, io::Cursor, iter};
use assert_matches::assert_matches;
use eyeball_im::VectorDiff;
use futures_util::StreamExt;
use matrix_sdk_base::crypto::{decrypt_room_key_export, OlmMachine};
use matrix_sdk::crypto::{decrypt_room_key_export, OlmMachine};
use matrix_sdk_test::async_test;
use ruma::{
assign,
@@ -31,7 +31,7 @@ use ruma::{
};
use super::{TestTimeline, BOB};
use crate::room::timeline::{EncryptedMessage, TimelineItemContent};
use crate::timeline::{EncryptedMessage, TimelineItemContent};
#[async_test]
async fn retry_message_decryption() {
@@ -100,7 +100,7 @@ async fn retry_message_decryption() {
timeline
.inner
.retry_event_decryption(
.retry_event_decryption_test(
room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost"),
&olm_machine,
Some(iter::once(SESSION_ID).collect()),
@@ -203,7 +203,7 @@ async fn retry_edit_decryption() {
timeline
.inner
.retry_event_decryption(
.retry_event_decryption_test(
room_id!("!bdsREiCPHyZAPkpXer:morpheus.localhost"),
&olm_machine,
None,
@@ -306,7 +306,7 @@ async fn retry_edit_and_more() {
timeline
.inner
.retry_event_decryption(
.retry_event_decryption_test(
room_id!("!wFnAUSQbxMcfIMgvNX:flipdot.org"),
&olm_machine,
Some(BTreeSet::from_iter([SESSION_ID])),
@@ -392,7 +392,7 @@ async fn retry_message_decryption_highlighted() {
timeline
.inner
.retry_event_decryption(
.retry_event_decryption_test(
room_id!("!rYtFvMGENJleNQVJzb:matrix.org"),
&olm_machine,
Some(iter::once(SESSION_ID).collect()),
@@ -28,7 +28,7 @@ use ruma::{
use serde_json::json;
use super::{TestTimeline, ALICE, BOB};
use crate::room::timeline::TimelineItemContent;
use crate::timeline::TimelineItemContent;
#[async_test]
async fn invalid_edit() {
@@ -26,7 +26,7 @@ use async_trait::async_trait;
use eyeball_im::VectorDiff;
use futures_core::Stream;
use indexmap::IndexMap;
use matrix_sdk_base::deserialized_responses::TimelineEvent;
use matrix_sdk::deserialized_responses::TimelineEvent;
use once_cell::sync::Lazy;
use ruma::{
events::{
@@ -23,7 +23,7 @@ use ruma::{
};
use super::{TestTimeline, ALICE, BOB};
use crate::room::timeline::{TimelineItem, VirtualTimelineItem};
use crate::timeline::{TimelineItem, VirtualTimelineItem};
#[async_test]
async fn day_divider() {
@@ -14,6 +14,7 @@
use std::{iter, sync::Arc};
use matrix_sdk::{event_handler::EventHandler, Client};
use ruma::{
events::{forwarded_room_key::ToDeviceForwardedRoomKeyEvent, room_key::ToDeviceRoomKeyEvent},
OwnedRoomId,
@@ -21,7 +22,6 @@ use ruma::{
use tracing::{debug_span, error, trace, Instrument};
use super::inner::TimelineInner;
use crate::{event_handler::EventHandler, Client};
pub(super) fn handle_room_key_event(
inner: Arc<TimelineInner>,
@@ -70,16 +70,10 @@ async fn retry_decryption(
return;
}
let Some(olm_machine) = client.olm_machine() else {
error!("The olm machine isn't yet available");
let Some(room) = client.get_room(&room_id) else {
error!("Failed to fetch room object");
return;
};
inner
.retry_event_decryption(
&room_id,
olm_machine,
Some(iter::once(session_id.as_str()).collect()),
)
.await;
inner.retry_event_decryption(&room, Some(iter::once(session_id.as_str()).collect())).await;
}
@@ -1,5 +1,3 @@
#![cfg(feature = "experimental-timeline")]
use std::{sync::Arc, time::Duration};
use assert_matches::assert_matches;
@@ -34,6 +32,8 @@ use wiremock::{
};
mod read_receipts;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;
use crate::{logged_in_client, mock_encryption_state, mock_sync};
@@ -5,10 +5,13 @@ use assert_matches::assert_matches;
use eyeball_im::{Vector, VectorDiff};
use futures::{pin_mut, Stream, StreamExt};
use matrix_sdk::{
room::timeline::{TimelineItem, VirtualTimelineItem},
SlidingSync, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode, UpdateSummary,
};
use matrix_sdk_test::async_test;
use matrix_sdk_ui::{
timeline::{TimelineItem, VirtualTimelineItem},
SlidingSyncRoomExt,
};
use ruma::{room_id, RoomId};
use serde_json::json;
use wiremock::{http::Method, Match, Mock, MockServer, Request, ResponseTemplate};
+1 -12
View File
@@ -47,13 +47,7 @@ appservice = ["ruma/appservice-api-s"]
image-proc = ["dep:image"]
image-rayon = ["image-proc", "image?/jpeg_rayon"]
experimental-timeline = ["ruma/unstable-msc2677", "ruma/unstable-sanitize", "dep:chrono"]
experimental-sliding-sync = [
"matrix-sdk-base/experimental-sliding-sync",
"experimental-timeline",
"reqwest/gzip",
]
experimental-sliding-sync = ["matrix-sdk-base/experimental-sliding-sync", "reqwest/gzip"]
docsrs = [
"e2e-encryption",
@@ -70,7 +64,6 @@ async-stream = { workspace = true }
async-trait = { workspace = true }
bytes = "1.1.0"
bytesize = "1.1"
chrono = { version = "0.4.23", optional = true }
dashmap = { workspace = true }
event-listener = "2.5.2"
eyeball = { workspace = true }
@@ -80,7 +73,6 @@ futures-core = "0.3.21"
futures-util = { workspace = true }
http = { workspace = true }
imbl = { version = "2.0.0", features = ["serde"] }
indexmap = "1.9.1"
hyper = { version = "0.14.20", features = ["http1", "http2", "server"], optional = true }
matrix-sdk-base = { version = "0.6.0", path = "../matrix-sdk-base", default_features = false }
matrix-sdk-common = { version = "0.6.0", path = "../matrix-sdk-common" }
@@ -88,8 +80,6 @@ matrix-sdk-indexeddb = { version = "0.2.0", path = "../matrix-sdk-indexeddb", de
matrix-sdk-sqlite = { version = "0.1.0", path = "../matrix-sdk-sqlite", default-features = false, optional = true }
mime = "0.3.16"
mime2ext = "0.1.52"
once_cell = { workspace = true }
pin-project-lite = "0.2.9"
rand = { version = "0.8.5", optional = true }
reqwest = { version = "0.11.10", default_features = false }
ruma = { workspace = true, features = ["rand", "unstable-msc2448", "unstable-msc2965"] }
@@ -137,7 +127,6 @@ assert_matches = { workspace = true }
dirs = "4.0.0"
futures = { version = "0.3.21", default-features = false, features = ["executor"] }
matrix-sdk-test = { version = "0.6.0", path = "../../testing/matrix-sdk-test" }
once_cell = { workspace = true }
tracing-subscriber = { version = "0.3.11", features = ["env-filter"] }
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
-5
View File
@@ -245,11 +245,6 @@ pub enum Error {
#[error(transparent)]
SlidingSync(#[from] crate::sliding_sync::Error),
/// An error occurred in the timeline.
#[cfg(feature = "experimental-timeline")]
#[error(transparent)]
Timeline(#[from] crate::room::timeline::Error),
/// The client is in inconsistent state. This happens when we set a room to
/// a specific type, but then cannot get it in this type.
#[error("The internal client state is inconsistent.")]
-2
View File
@@ -45,8 +45,6 @@ pub mod sliding_sync;
#[cfg(feature = "e2e-encryption")]
pub mod encryption;
#[cfg(feature = "experimental-timeline")]
mod events;
pub use account::Account;
#[cfg(feature = "sso-login")]
-12
View File
@@ -49,8 +49,6 @@ use serde::de::DeserializeOwned;
use tokio::sync::Mutex;
use tracing::{debug, instrument};
#[cfg(feature = "experimental-timeline")]
use super::timeline::Timeline;
use super::Joined;
use crate::{
event_handler::{EventHandler, EventHandlerHandle, SyncEvent},
@@ -269,16 +267,6 @@ impl Common {
self.client.add_room_event_handler(self.room_id(), handler)
}
/// Get a [`Timeline`] for this room.
///
/// This offers a higher-level API than event handlers, in treating things
/// like edits and reactions as updates of existing items rather than new
/// independent events.
#[cfg(feature = "experimental-timeline")]
pub async fn timeline(&self) -> Timeline {
Timeline::builder(self).track_read_marker_and_receipts().build().await
}
/// Fetch the event with the given `EventId` in this room.
pub async fn event(&self, event_id: &EventId) -> Result<TimelineEvent> {
let request =
+11 -7
View File
@@ -295,10 +295,10 @@ impl Joined {
return Ok(());
}
let Receipts { fully_read, read_receipt, private_read_receipt } = receipts;
let Receipts { fully_read, public_read_receipt, private_read_receipt } = receipts;
let request = assign!(set_read_marker::v3::Request::new(self.inner.room_id().to_owned()), {
fully_read,
read_receipt,
read_receipt: public_read_receipt,
private_read_receipt,
});
@@ -1139,10 +1139,14 @@ impl Joined {
/// Receipts to send all at once.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Receipts {
pub(super) fully_read: Option<OwnedEventId>,
pub(super) read_receipt: Option<OwnedEventId>,
pub(super) private_read_receipt: Option<OwnedEventId>,
/// Fully-read marker (room account data).
pub fully_read: Option<OwnedEventId>,
/// Read receipt (public ephemeral room event).
pub public_read_receipt: Option<OwnedEventId>,
/// Read receipt (private ephemeral room event).
pub private_read_receipt: Option<OwnedEventId>,
}
impl Receipts {
@@ -1170,7 +1174,7 @@ impl Receipts {
/// This is used to reset the unread messages/notification count and
/// advertise to other users the last event that the user has likely seen.
pub fn public_read_receipt(mut self, event_id: impl Into<Option<OwnedEventId>>) -> Self {
self.read_receipt = event_id.into();
self.public_read_receipt = event_id.into();
self
}
@@ -1185,7 +1189,7 @@ impl Receipts {
/// Whether this `Receipts` is empty.
pub fn is_empty(&self) -> bool {
self.fully_read.is_none()
&& self.read_receipt.is_none()
&& self.public_read_receipt.is_none()
&& self.private_read_receipt.is_none()
}
}
-2
View File
@@ -9,8 +9,6 @@ mod invited;
mod joined;
mod left;
mod member;
#[cfg(feature = "experimental-timeline")]
pub mod timeline;
pub use self::{
common::{Common, Messages, MessagesOptions},
+4 -4
View File
@@ -216,8 +216,8 @@ the [`BaseClient`][`matrix_sdk_base::BaseClient`] as in previous sync. This
allows for transparent decryption as well trigger the `client_handlers`.
The current and then following live events list can be queried via the
[`timeline` API](`SlidingSyncRoom::timeline). This is prefilled with already
received data.
`timeline` API from `matrix-sdk-ui`. This is prefilled with already received
data.
### Timeline trickling
@@ -349,8 +349,8 @@ copies accordingly. Because of where the loop sits in the stack, that can
be a bit tedious though, so lists and rooms have an additional way of
subscribing to updates via [`eyeball`].
The `Timeline` one can receive per room by calling
[`.timeline()`][`SlidingSyncRoom::timeline`] will be populated with the
The `Timeline` one can receive per room by calling `.timeline()` (from
`matrix_sdk_ui::timeline::SlidingSyncRoomExt`) will be populated with the
currently cached timeline events.
## Caching
+20 -53
View File
@@ -13,12 +13,8 @@ use ruma::{
OwnedRoomId, RoomId,
};
use serde::{Deserialize, Serialize};
use tracing::{error, instrument};
use crate::{
room::timeline::{EventTimelineItem, Timeline, TimelineBuilder},
Client,
};
use crate::Client;
/// The state of a [`SlidingSyncRoom`].
#[derive(Copy, Clone, Debug, Default, PartialEq)]
@@ -108,23 +104,25 @@ impl SlidingSyncRoom {
/// Get the required state.
pub fn required_state(&self) -> Vec<Raw<AnySyncStateEvent>> {
let inner = self.inner.inner.read().unwrap();
inner.required_state.clone()
self.inner.inner.read().unwrap().required_state.clone()
}
/// `Timeline` of this room
pub async fn timeline(&self) -> Option<Timeline> {
Some(self.inner.timeline_builder()?.track_read_marker_and_receipts().build().await)
/// Get the token for back-pagination.
pub fn prev_batch(&self) -> Option<String> {
self.inner.inner.read().unwrap().prev_batch.clone()
}
/// The latest timeline item of this room.
/// Get a copy of the cached timeline events.
///
/// Use `Timeline::latest_event` instead if you already have a timeline for
/// this `SlidingSyncRoom`.
#[instrument(skip_all)]
pub async fn latest_event(&self) -> Option<EventTimelineItem> {
self.inner.timeline_builder()?.build().await.latest_event().await
/// Note: This API only exists temporarily, it *will* be removed in the
/// future.
pub fn timeline_queue(&self) -> Vector<SyncTimelineEvent> {
self.inner.timeline_queue.read().unwrap().clone()
}
/// Get a clone of the associated client.
pub fn client(&self) -> Client {
self.inner.client.clone()
}
pub(super) fn update(
@@ -233,10 +231,6 @@ impl SlidingSyncRoom {
fn set_state(&mut self, state: SlidingSyncRoomState) {
*self.inner.state.write().unwrap() = state;
}
fn timeline_queue(&self) -> std::sync::RwLockReadGuard<'_, Vector<SyncTimelineEvent>> {
self.inner.timeline_queue.read().unwrap()
}
}
#[derive(Debug)]
@@ -260,33 +254,6 @@ struct SlidingSyncRoomInner {
timeline_queue: RwLock<Vector<SyncTimelineEvent>>,
}
impl SlidingSyncRoomInner {
/// Get the previous batch.
fn prev_batch(&self) -> Option<String> {
let inner = self.inner.read().unwrap();
inner.prev_batch.clone()
}
fn timeline_builder(&self) -> Option<TimelineBuilder> {
if let Some(room) = self.client.get_room(&self.room_id) {
Some(
Timeline::builder(&room)
.events(self.prev_batch(), self.timeline_queue.read().unwrap().clone()),
)
} else if let Some(invited_room) = self.client.get_invited_room(&self.room_id) {
Some(Timeline::builder(&invited_room).events(None, Vector::new()))
} else {
error!(
room_id = ?self.room_id,
"Room not found in client. Can't provide a timeline for it"
);
None
}
}
}
/// A “frozen” [`SlidingSyncRoom`], i.e. that can be written into, or read from
/// a store.
#[derive(Debug, Serialize, Deserialize)]
@@ -515,7 +482,7 @@ mod tests {
{
let room = new_room(room_id!("!foo:bar.org"), room_response!({})).await;
assert_eq!(room.inner.prev_batch(), None);
assert_eq!(room.prev_batch(), None);
}
// Some value when initializing.
@@ -524,20 +491,20 @@ mod tests {
new_room(room_id!("!foo:bar.org"), room_response!({"prev_batch": "t111_222_333"}))
.await;
assert_eq!(room.inner.prev_batch(), Some("t111_222_333".to_owned()));
assert_eq!(room.prev_batch(), Some("t111_222_333".to_owned()));
}
// Some value when updating.
{
let mut room = new_room(room_id!("!foo:bar.org"), room_response!({})).await;
assert_eq!(room.inner.prev_batch(), None);
assert_eq!(room.prev_batch(), None);
room.update(room_response!({"prev_batch": "t111_222_333"}), vec![]);
assert_eq!(room.inner.prev_batch(), Some("t111_222_333".to_owned()));
assert_eq!(room.prev_batch(), Some("t111_222_333".to_owned()));
room.update(room_response!({}), vec![]);
assert_eq!(room.inner.prev_batch(), Some("t111_222_333".to_owned()));
assert_eq!(room.prev_batch(), Some("t111_222_333".to_owned()));
}
}
@@ -16,8 +16,6 @@ use wiremock::{
mod client;
mod refresh_token;
mod room;
#[cfg(feature = "experimental-sliding-sync")]
mod sliding_sync;
#[cfg(all(test, not(target_arch = "wasm32")))]
#[ctor::ctor]
@@ -1,4 +1,3 @@
mod common;
mod joined;
mod left;
mod timeline;
+4 -1
View File
@@ -18,5 +18,8 @@ url = "2.2.2"
[dependencies.matrix-sdk]
path = "../../crates/matrix-sdk"
features = ["experimental-timeline"]
version = "0.6.0"
[dependencies.matrix-sdk-ui]
path = "../../crates/matrix-sdk-ui"
version = "0.6.0"
+1
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
use clap::Parser;
use futures::StreamExt;
use matrix_sdk::{self, config::SyncSettings, ruma::OwnedRoomId, Client};
use matrix_sdk_ui::timeline::RoomExt;
use url::Url;
#[derive(Parser, Debug)]
+1 -1
View File
@@ -174,7 +174,7 @@ fn check_clippy() -> Result<()> {
"rustup run {NIGHTLY} cargo clippy --workspace --all-targets
--exclude matrix-sdk-crypto --exclude xtask
--no-default-features
--features native-tls,experimental-sliding-sync,sso-login,experimental-timeline
--features native-tls,experimental-sliding-sync,sso-login
-- -D warnings"
)
.run()?;